Merge branch 'hinge-support'

This commit is contained in:
Jays2Kings 2022-12-16 01:58:07 -05:00
commit 067f6997a5
12 changed files with 242 additions and 23 deletions

View file

@ -136,6 +136,7 @@ dependencies {
implementation("androidx.core:core-ktx:1.8.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
implementation("com.google.android.flexbox:flexbox:3.0.0")
implementation("androidx.window:window:1.0.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
@ -144,7 +145,7 @@ dependencies {
implementation("com.google.firebase:firebase-core:21.1.0")
implementation("com.google.firebase:firebase-analytics-ktx:21.1.0")
val lifecycleVersion = "2.4.0-rc01"
val lifecycleVersion = "2.5.1"
kapt("androidx.lifecycle:lifecycle-compiler:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")

View file

@ -78,8 +78,6 @@ open class App : Application(), DefaultLifecycleObserver {
setupAcra()
setupNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
MangaCoverMetadata.load()
preferences.nightMode()
.asImmediateFlow { AppCompatDelegate.setDefaultNightMode(it) }

View file

@ -43,7 +43,12 @@ import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
@ -152,6 +157,8 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), DownloadServiceLi
private var overflowDialog: Dialog? = null
var currentToolbar: Toolbar? = null
var ogWidth: Int = Int.MAX_VALUE
var hingeGapSize = 0
private set
private val actionButtonSize: Pair<Int, Int> by lazy {
val attrs = intArrayOf(android.R.attr.minWidth, android.R.attr.minHeight)
@ -522,6 +529,25 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), DownloadServiceLi
}
}
setFloatingToolbar(canShowFloatingToolbar(router.backstack.lastOrNull()?.controller), changeBG = false)
lifecycleScope.launchUI {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(this@MainActivity).windowLayoutInfo(this@MainActivity)
.collect { newLayoutInfo ->
hingeGapSize = 0
for (displayFeature: DisplayFeature in newLayoutInfo.displayFeatures) {
if (displayFeature is FoldingFeature && displayFeature.occlusionType == FoldingFeature.OcclusionType.FULL &&
displayFeature.isSeparating && displayFeature.orientation == FoldingFeature.Orientation.VERTICAL
) {
hingeGapSize = displayFeature.bounds.width()
}
}
if (hingeGapSize > 0) {
(router.backstack.lastOrNull()?.controller as? HingeSupportedController)?.updateForHinge()
}
}
}
}
}
fun reEnableBackPressedCallBack() {
@ -1388,6 +1414,10 @@ interface RootSearchInterface {
interface TabbedInterface
interface HingeSupportedController {
fun updateForHinge()
}
interface FloatingSearchInterface {
fun searchTitle(title: String?): String? {
if (this is Controller) {

View file

@ -190,8 +190,7 @@ class FullCoverDialog(val controller: MangaDetailsController, drawable: Drawable
}
override fun cancel() {
super.cancel()
thumbView.alpha = 1f
animateBack()
}
override fun dismiss() {

View file

@ -19,6 +19,7 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.FloatRange
@ -26,6 +27,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.graphics.ColorUtils
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars
@ -68,6 +70,7 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface
import eu.kanade.tachiyomi.ui.main.HingeSupportedController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.main.SearchActivity
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterHolder
@ -104,6 +107,7 @@ import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat
import eu.kanade.tachiyomi.util.system.setCustomTitleAndMessage
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.activityBinding
import eu.kanade.tachiyomi.util.view.findChild
import eu.kanade.tachiyomi.util.view.getText
import eu.kanade.tachiyomi.util.view.isControllerVisible
import eu.kanade.tachiyomi.util.view.previousController
@ -134,6 +138,7 @@ class MangaDetailsController :
ActionMode.Callback,
MangaDetailsAdapter.MangaDetailsInterface,
SmallToolbarInterface,
HingeSupportedController,
FlexibleAdapter.OnItemMoveListener {
constructor(
@ -370,6 +375,30 @@ class MangaDetailsController :
tabletAdapter = MangaDetailsAdapter(this)
binding.tabletRecycler.adapter = tabletAdapter
binding.tabletRecycler.layoutManager = LinearLayoutManager(view.context)
updateForHinge()
}
}
override fun updateForHinge() {
if (isTablet) {
val hingeGapSize = (activity as? MainActivity)?.hingeGapSize?.takeIf { it > 0 }
if (hingeGapSize != null) {
binding.tabletDivider.updateLayoutParams<ViewGroup.LayoutParams> {
width = hingeGapSize
}
binding.tabletRecycler.updateLayoutParams<ConstraintLayout.LayoutParams> {
matchConstraintPercentWidth = 1f
width = 0
matchConstraintDefaultWidth =
ConstraintLayout.LayoutParams.MATCH_CONSTRAINT_SPREAD
}
val swipeCircle = binding.swipeRefresh.findChild<ImageView>()
swipeCircle?.translationX =
(activity!!.window.decorView.width / 2 + hingeGapSize) /
2f
} else {
binding.tabletRecycler.updateLayoutParams<ConstraintLayout.LayoutParams> { matchConstraintPercentWidth = 0.4f }
}
}
}
@ -440,6 +469,12 @@ class MangaDetailsController :
binding.touchView.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
finishFloatingActionMode()
val hingeGapSize = (activity as? MainActivity)?.hingeGapSize?.takeIf { it > 0 }
if (hingeGapSize != null) {
val swipeCircle = binding.swipeRefresh.findChild<ImageView>()
swipeCircle?.translationX = (binding.root.width / 2 + hingeGapSize) / 2 *
(if (event.x > binding.root.width / 2) 1 else -1).toFloat()
}
}
false
}

View file

@ -17,6 +17,7 @@ import android.graphics.drawable.LayerDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.Gravity
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.Menu
@ -29,6 +30,7 @@ import android.view.WindowManager
import android.view.animation.AnimationUtils
import androidx.activity.OnBackPressedCallback
import androidx.activity.addCallback
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
@ -46,7 +48,11 @@ import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePaddingRelative
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
@ -117,6 +123,7 @@ import eu.kanade.tachiyomi.widget.doOnStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.merge
@ -211,6 +218,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
}
var isScrollingThroughPagesOrChapters = false
private var hingeGapSize = 0
set(value) {
field = value
(viewer as? PagerViewer)?.config?.hingeGapSize = value
}
companion object {
@ -315,6 +327,35 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
SecureActivityDelegate.setSecure(this)
}
reEnableBackPressedCallBack()
lifecycleScope.launchUI {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(this@ReaderActivity).windowLayoutInfo(this@ReaderActivity)
.collect { newLayoutInfo ->
hingeGapSize = 0
for (displayFeature: DisplayFeature in newLayoutInfo.displayFeatures) {
if (displayFeature is FoldingFeature && displayFeature.occlusionType == FoldingFeature.OcclusionType.FULL &&
displayFeature.isSeparating && displayFeature.orientation == FoldingFeature.Orientation.VERTICAL
) {
hingeGapSize = displayFeature.bounds.width()
}
}
if (hingeGapSize > 0) {
binding.navLayout.updateLayoutParams<CoordinatorLayout.LayoutParams> {
gravity = Gravity.TOP or Gravity.CENTER
anchorGravity = Gravity.TOP or Gravity.CENTER
width = (binding.root.width - hingeGapSize) / 2 - 24.dpToPx
}
binding.chaptersSheet.root.updateLayoutParams<CoordinatorLayout.LayoutParams> {
gravity = Gravity.END
width = (binding.root.width - hingeGapSize) / 2
}
binding.pleaseWait.updateLayoutParams<CoordinatorLayout.LayoutParams> {
marginStart = binding.root.width / 2 + hingeGapSize
}
}
}
}
}
}
/**
@ -1097,6 +1138,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
}
if (newViewer is PagerViewer) {
newViewer.config.hingeGapSize = hingeGapSize
if (preferences.pageLayout().get() == PageLayout.AUTOMATIC.value) {
setDoublePageMode(newViewer)
}
@ -1287,6 +1329,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
}
val totalPages = pages.size.toString()
if (hingeGapSize > 0) {
binding.pageNumber.updateLayoutParams<CoordinatorLayout.LayoutParams> {
marginStart = (binding.root.width) / 2 + hingeGapSize
}
}
binding.pageNumber.text = if (resources.isLTR) "$currentPage/$totalPages" else "$totalPages/$currentPage"
if (viewer is R2LPagerViewer) {
binding.readerNav.rightPageText.text = currentPage

View file

@ -312,6 +312,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
val zoomStartPosition: PagerConfig.ZoomType = PagerConfig.ZoomType.Center,
val landscapeZoom: Boolean = false,
val insetInfo: InsetInfo? = null,
val hingeGapSize: Int = 0,
)
data class InsetInfo(

View file

@ -64,6 +64,8 @@ class PagerConfig(
}
}
var hingeGapSize = 0
var invertDoublePages = false
var autoDoublePages = preferences.pageLayout().get() == PageLayout.AUTOMATIC.value

View file

@ -18,6 +18,7 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -120,6 +121,11 @@ class PagerPageHolder(
init {
addView(progressBar)
if (viewer.config.hingeGapSize > 0) {
progressBar.updateLayoutParams<MarginLayoutParams> {
marginStart = ((context.resources.displayMetrics.widthPixels) / 2 + viewer.config.hingeGapSize) / 2
}
}
scope = CoroutineScope(Job() + Default)
observeStatus()
setBackgroundColor(
@ -566,6 +572,7 @@ class PagerPageHolder(
isSplitScreen = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && viewer.activity.isInMultiWindowMode,
insets = viewer.activity.window.decorView.rootWindowInsets,
),
hingeGapSize = viewer.config.hingeGapSize,
)
private suspend fun setBG(bytesArray: ByteArray): Drawable {
@ -771,18 +778,18 @@ class PagerPageHolder(
imageBytes.inputStream()
}
}
return imageStream
return supportHingeIfThere(imageStream)
}
if (page.fullPage == true) return imageStream
if (page.fullPage == true) return supportHingeIfThere(imageStream)
if (ImageUtil.isAnimatedAndSupported(imageStream)) {
page.fullPage = true
splitDoublePages()
return imageStream
} else if (ImageUtil.isAnimatedAndSupported(imageStream)) {
} else if (ImageUtil.isAnimatedAndSupported(imageStream2)) {
page.isolatedPage = true
extraPage?.fullPage = true
splitDoublePages()
return imageStream
return supportHingeIfThere(imageStream)
}
val imageBytes = imageStream.readBytes()
val imageBitmap = try {
@ -804,7 +811,7 @@ class PagerPageHolder(
imageStream.close()
page.fullPage = true
splitDoublePages()
return imageBytes.inputStream()
return supportHingeIfThere(imageBytes.inputStream())
}
val imageBytes2 = imageStream2.readBytes()
@ -829,7 +836,7 @@ class PagerPageHolder(
extraPage?.fullPage = true
page.isolatedPage = true
splitDoublePages()
return imageBytes.inputStream()
return supportHingeIfThere(imageBytes.inputStream())
}
val isLTR = (viewer !is R2LPagerViewer).xor(viewer.config.invertDoublePages)
val bg = if (viewer.config.readerTheme >= 2 || viewer.config.readerTheme == 0) {
@ -840,7 +847,7 @@ class PagerPageHolder(
imageStream.close()
imageStream2.close()
return ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, bg) {
return ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, bg, viewer.config.hingeGapSize, context) {
scope?.launchUI {
if (it == 100) {
progressBar.completeAndFadeOut()
@ -851,6 +858,38 @@ class PagerPageHolder(
}
}
private fun supportHingeIfThere(imageStream: InputStream): InputStream {
if (viewer.config.hingeGapSize > 0 && !ImageUtil.isAnimatedAndSupported(imageStream)) {
val imageBytes = imageStream.readBytes()
val imageBitmap = try {
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
} catch (e: Exception) {
imageStream.close()
val wasNotFullPage = page.fullPage != true
page.fullPage = true
if (wasNotFullPage) {
splitDoublePages()
}
return imageBytes.inputStream()
}
val isLTR = (viewer !is R2LPagerViewer).xor(viewer.config.invertDoublePages)
val bg = if (viewer.config.readerTheme >= 2 || viewer.config.readerTheme == 0) {
Color.WHITE
} else {
Color.BLACK
}
return ImageUtil.padSingleImage(
imageBitmap = imageBitmap,
isLTR = isLTR,
atBeginning = if (viewer.config.doublePages) page.index == 0 else null,
background = bg,
hingeGap = viewer.config.hingeGapSize,
context = context,
)
}
return imageStream
}
private fun splitDoublePages() {
// extraPage ?: return
scope?.launchUI {

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint
import android.app.Activity
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
@ -9,6 +10,7 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.LinearLayout
import android.widget.ProgressBar
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.view.updatePaddingRelative
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
@ -59,6 +61,12 @@ class PagerTransitionHolder(
transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga)
transition.to?.let { observeStatus(it) }
if (viewer.config.hingeGapSize > 0) {
val fullWidth = (context as? Activity)?.window?.decorView?.width
?: context.resources.displayMetrics.widthPixels
updatePaddingRelative(start = sidePadding + fullWidth / 2 + viewer.config.hingeGapSize)
}
}
/**

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.util.system
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
@ -429,6 +430,8 @@ object ImageUtil {
imageBitmap2: Bitmap,
isLTR: Boolean,
@ColorInt background: Int = Color.WHITE,
hingeGap: Int = 0,
context: Context? = null,
progressCallback: ((Int) -> Unit)? = null,
): ByteArrayInputStream {
val height = imageBitmap.height
@ -436,21 +439,27 @@ object ImageUtil {
val height2 = imageBitmap2.height
val width2 = imageBitmap2.width
val maxHeight = max(height, height2)
val result = Bitmap.createBitmap(width + width2, max(height, height2), Bitmap.Config.ARGB_8888)
val maxWidth = max(width, width2)
val adjustedHingeGap = context?.let {
val fullHeight = (context as? Activity)?.window?.decorView?.height
?: context.resources.displayMetrics.heightPixels
(maxHeight.toFloat() / fullHeight * hingeGap).toInt()
} ?: hingeGap
val result = Bitmap.createBitmap((maxWidth * 2) + adjustedHingeGap, maxHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
canvas.drawColor(background)
val upperPart = Rect(
if (isLTR) 0 else width2,
if (isLTR) max(maxWidth - imageBitmap.width, 0) else maxWidth + adjustedHingeGap,
(maxHeight - imageBitmap.height) / 2,
(if (isLTR) 0 else width2) + imageBitmap.width,
(if (isLTR) max(maxWidth - imageBitmap.width, 0) else maxWidth + adjustedHingeGap) + imageBitmap.width,
imageBitmap.height + (maxHeight - imageBitmap.height) / 2,
)
canvas.drawBitmap(imageBitmap, imageBitmap.rect, upperPart, null)
progressCallback?.invoke(98)
val bottomPart = Rect(
if (!isLTR) 0 else width,
if (!isLTR) max(maxWidth - imageBitmap2.width, 0) else maxWidth + adjustedHingeGap,
(maxHeight - imageBitmap2.height) / 2,
(if (!isLTR) 0 else width) + imageBitmap2.width,
(if (!isLTR) max(maxWidth - imageBitmap2.width, 0) else maxWidth + adjustedHingeGap) + imageBitmap2.width,
imageBitmap2.height + (maxHeight - imageBitmap2.height) / 2,
)
canvas.drawBitmap(imageBitmap2, imageBitmap2.rect, bottomPart, null)
@ -462,6 +471,56 @@ object ImageUtil {
return ByteArrayInputStream(output.toByteArray())
}
fun padSingleImage(
imageBitmap: Bitmap,
isLTR: Boolean,
atBeginning: Boolean?,
@ColorInt background: Int,
hingeGap: Int,
context: Context,
progressCallback: ((Int) -> Unit)? = null,
): ByteArrayInputStream {
val height = imageBitmap.height
val width = imageBitmap.width
val isFullPageSpread = height < width
val fullHeight = (context as? Activity)?.window?.decorView?.height
?: context.resources.displayMetrics.heightPixels
val adjustedHingeGap = (height.toFloat() / fullHeight * hingeGap).toInt()
val result = Bitmap.createBitmap(
(if (isFullPageSpread) width else (width * 2)) + adjustedHingeGap,
height,
Bitmap.Config.ARGB_8888,
)
val canvas = Canvas(result)
canvas.drawColor(background)
if (isFullPageSpread) {
val leftPart = Rect(0, 0, width / 2, height)
canvas.drawBitmap(imageBitmap, imageBitmap.rect.also { it.right /= 2 }, leftPart, null)
progressCallback?.invoke(98)
val rightPart = Rect(
width / 2 + adjustedHingeGap,
0,
width + adjustedHingeGap,
height,
)
canvas.drawBitmap(imageBitmap, imageBitmap.rect.also { it.left = width / 2 }, rightPart, null)
} else {
val placeOnLeft = if (atBeginning == null) true else isLTR.xor(atBeginning)
val upperPart = Rect(
if (placeOnLeft) 0 else width + adjustedHingeGap,
0,
(if (placeOnLeft) 0 else width + adjustedHingeGap) + width,
height,
)
canvas.drawBitmap(imageBitmap, imageBitmap.rect, upperPart, null)
}
progressCallback?.invoke(99)
val output = ByteArrayOutputStream()
result.compress(Bitmap.CompressFormat.JPEG, 100, output)
progressCallback?.invoke(100)
return ByteArrayInputStream(output.toByteArray())
}
/**
* Check whether the image is considered a tall image.
*

View file

@ -28,8 +28,7 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/recycler"
app:layout_constraintWidth_percent="0.4"
app:layout_constraintEnd_toStartOf="@id/tablet_divider"
tools:itemCount="1"
tools:listitem="@layout/manga_header_item" />
@ -41,7 +40,7 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tablet_recycler"
app:layout_constraintStart_toEndOf="@id/tablet_divider"
tools:listitem="@layout/chapters_item" />
<View
@ -51,17 +50,18 @@
android:alpha=".80"
android:layout_width="0dp"
android:layout_height="20dp"
app:layout_constraintWidth_percent=".6"
app:layout_constraintStart_toStartOf="@id/recycler"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<View
android:id="@+id/tablet_divider"
android:visibility="gone"
android:layout_width="1dp"
android:visibility="gone"
android:layout_height="match_parent"
android:background="@color/divider"
app:layout_constraintEnd_toStartOf="@id/tablet_overlay"
app:layout_constraintStart_toEndOf="@id/tablet_recycler"
app:layout_constraintEnd_toStartOf="@id/recycler"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>