From 4999db33f4e984582b85887853c884c0ba514186 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Sat, 8 May 2021 22:48:43 -0400 Subject: [PATCH] Tablet UI for manga details It returns for some reason... even I'm trying to figure out why enjoy my 8 tablet users Also kinda fix the view scrolling down a bit when tapping on the expanded summary when the summary is extremely long --- .../ui/manga/MangaDetailsController.kt | 94 +++++++--- .../ui/manga/MangaDetailsPresenter.kt | 7 + .../tachiyomi/ui/manga/MangaHeaderHolder.kt | 175 ++++++++++++------ .../tachiyomi/ui/manga/MangaHeaderItem.kt | 3 +- .../main/res/layout/chapter_header_item.xml | 4 +- .../res/layout/manga_details_controller.xml | 48 ++++- app/src/main/res/layout/manga_header_item.xml | 1 + 7 files changed, 243 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt index 45ed91bad9..1d0e873b77 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt @@ -11,6 +11,7 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent +import android.content.res.Configuration import android.graphics.Color import android.graphics.Rect import android.graphics.drawable.BitmapDrawable @@ -30,6 +31,7 @@ import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.SearchView import androidx.core.graphics.ColorUtils +import androidx.core.view.isVisible import androidx.palette.graphics.Palette import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -95,15 +97,18 @@ import eu.kanade.tachiyomi.util.system.getPrefTheme import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.isInNightMode import eu.kanade.tachiyomi.util.system.isOnline +import eu.kanade.tachiyomi.util.system.isTablet import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.activityBinding +import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets import eu.kanade.tachiyomi.util.view.getText import eu.kanade.tachiyomi.util.view.requestPermissionsSafe import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener import eu.kanade.tachiyomi.util.view.setStyle import eu.kanade.tachiyomi.util.view.snack +import eu.kanade.tachiyomi.util.view.toolbarHeight import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.util.view.updatePaddingRelative import eu.kanade.tachiyomi.util.view.withFadeTransaction @@ -172,6 +177,10 @@ class MangaDetailsController : var refreshTracker: Int? = null var chapterPopupMenu: Pair? = null + // Tablet Layout + var isTablet = false + private var tabletAdapter: MangaDetailsAdapter? = null + private var query = "" private var adapter: MangaDetailsAdapter? = null @@ -195,6 +204,7 @@ class MangaDetailsController : coverColor = null fullCoverActive = false + setTabletMode(view) setRecycler(view) setPaletteColor() adapter?.fastScroller = binding.fastScroller @@ -208,6 +218,21 @@ class MangaDetailsController : requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) } + /** Check if device is tablet, and use a second recycler to hold the details header if so */ + private fun setTabletMode(view: View) { + isTablet = view.context.isTablet() && + view.context.resources.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE + binding.tabletOverlay.isVisible = isTablet + binding.tabletRecycler.isVisible = isTablet + binding.tabletDivider.isVisible = isTablet + if (isTablet) { + binding.recycler.updateLayoutParams { width = 0 } + tabletAdapter = MangaDetailsAdapter(this) + binding.tabletRecycler.adapter = tabletAdapter + binding.tabletRecycler.layoutManager = LinearLayoutManager(view.context) + } + } + override fun onDestroyView(view: View) { snack?.dismiss() presenter.onDestroy() @@ -236,26 +261,37 @@ class MangaDetailsController : binding.swipeRefresh.setDistanceToTriggerSync(70.dpToPx) activityBinding!!.appBar.elevation = 0f - scrollViewWith( - binding.recycler, - padBottom = true, - customPadding = true, - afterInsets = { insets -> + if (isTablet) { + val tHeight = toolbarHeight.takeIf { it ?: 0 > 0 } ?: appbarHeight + headerHeight = tHeight + (activityBinding?.root?.rootWindowInsets?.systemWindowInsetTop ?: 0) + binding.recycler.updatePaddingRelative(top = headerHeight + 4.dpToPx) + binding.recycler.doOnApplyWindowInsets { _, insets, _ -> setInsets(insets, appbarHeight, offset) - }, - liftOnScroll = { - colorToolbar(it) } - ) + } else { + scrollViewWith( + binding.recycler, + padBottom = true, + customPadding = true, + afterInsets = { insets -> + setInsets(insets, appbarHeight, offset) + }, + liftOnScroll = { + colorToolbar(it) + } + ) + } binding.recycler.addOnScrollListener( object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) - val atTop = !recyclerView.canScrollVertically(-1) - val tY = getHeader()?.binding?.backdrop?.translationY ?: 0f - getHeader()?.binding?.backdrop?.translationY = max(0f, tY + dy * 0.25f) - if (atTop) getHeader()?.binding?.backdrop?.translationY = 0f + if (!isTablet) { + val atTop = !recyclerView.canScrollVertically(-1) + val tY = getHeader()?.binding?.backdrop?.translationY ?: 0f + getHeader()?.binding?.backdrop?.translationY = max(0f, tY + dy * 0.25f) + if (atTop) getHeader()?.binding?.backdrop?.translationY = 0f + } } override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { @@ -268,9 +304,15 @@ class MangaDetailsController : private fun setInsets(insets: WindowInsets, appbarHeight: Int, offset: Int) { binding.recycler.updatePaddingRelative(bottom = insets.systemWindowInsetBottom) - headerHeight = appbarHeight + insets.systemWindowInsetTop + binding.tabletRecycler.updatePaddingRelative(bottom = insets.systemWindowInsetBottom) + val tHeight = toolbarHeight.takeIf { it ?: 0 > 0 } ?: appbarHeight + headerHeight = tHeight + insets.systemWindowInsetTop binding.swipeRefresh.setProgressViewOffset(false, (-40).dpToPx, headerHeight + offset) - // 1dp extra to line up chapter header and manga header + if (isTablet) { + binding.tabletOverlay.updateLayoutParams { height = headerHeight } + // 4dp extra to line up chapter header and manga header + binding.recycler.updatePaddingRelative(top = headerHeight + 4.dpToPx) + } getHeader()?.setTopHeight(headerHeight) binding.fastScroller.updateLayoutParams { topMargin = headerHeight @@ -280,8 +322,8 @@ class MangaDetailsController : } /** Set the toolbar to fully transparent or colored and translucent */ - fun colorToolbar(isColor: Boolean, animate: Boolean = true) { - if (isColor == toolbarIsColored) return + private fun colorToolbar(isColor: Boolean, animate: Boolean = true) { + if (isColor == toolbarIsColored || (isTablet && isColor)) return toolbarIsColored = isColor val isCurrentController = router?.backstack?.lastOrNull()?.controller == this@MangaDetailsController @@ -538,12 +580,14 @@ class MangaDetailsController : } private fun getHeader(): MangaHeaderHolder? { - return binding.recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder + return if (isTablet) binding.tabletRecycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder + else binding.recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder } fun updateHeader() { binding.swipeRefresh.isRefreshing = presenter.isLoading adapter?.setChapters(presenter.chapters) + tabletAdapter?.notifyDataSetChanged() addMangaHeader() activity?.invalidateOptionsMenu() } @@ -555,6 +599,7 @@ class MangaDetailsController : launchUI { binding.swipeRefresh.isRefreshing = true } presenter.fetchChaptersFromSource() } + tabletAdapter?.notifyDataSetChanged() adapter?.setChapters(chapters) addMangaHeader() colorToolbar(binding.recycler.canScrollVertically(-1)) @@ -562,7 +607,12 @@ class MangaDetailsController : } private fun addMangaHeader() { - if (adapter?.scrollableHeaders?.isEmpty() == true) { + if (tabletAdapter?.scrollableHeaders?.isEmpty() == true) { + tabletAdapter?.removeAllScrollableHeaders() + tabletAdapter?.addScrollableHeader(presenter.headerItem) + adapter?.removeAllScrollableHeaders() + adapter?.addScrollableHeader(presenter.tabletChapterHeaderItem!!) + } else if (!isTablet && adapter?.scrollableHeaders?.isEmpty() == true) { adapter?.removeAllScrollableHeaders() adapter?.addScrollableHeader(presenter.headerItem) } @@ -819,8 +869,10 @@ class MangaDetailsController : setOnQueryTextChangeListener(searchView) { query = it ?: "" - if (query.isNotEmpty()) getHeader()?.collapse() - else getHeader()?.expand() + if (!isTablet) { + if (query.isNotEmpty()) getHeader()?.collapse() + else getHeader()?.expand() + } adapter?.setFilter(query) adapter?.performFilter() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt index 916f8f7b84..e22d969e97 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt @@ -46,6 +46,7 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.manga.MangaShortcutManager import eu.kanade.tachiyomi.util.system.executeOnIO +import eu.kanade.tachiyomi.util.system.isTablet import eu.kanade.tachiyomi.util.system.launchIO import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -97,8 +98,14 @@ class MangaDetailsPresenter( private set var headerItem = MangaHeaderItem(manga, controller.fromCatalogue) + var tabletChapterHeaderItem: MangaHeaderItem? = null fun onCreate() { + headerItem.isTablet = controller.isTablet + if (controller.isTablet) { + tabletChapterHeaderItem = MangaHeaderItem(manga, false) + tabletChapterHeaderItem?.isChapterHeader = true + } isLockedFromSearch = SecureActivityDelegate.shouldBeLocked() headerItem.isLocked = isLockedFromSearch downloadManager.addListener(this) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt index 1646e99b7e..88aa8d1b2d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt @@ -15,6 +15,7 @@ import com.google.android.material.button.MaterialButton import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.image.coil.loadManga +import eu.kanade.tachiyomi.databinding.ChapterHeaderItemBinding import eu.kanade.tachiyomi.databinding.MangaHeaderItemBinding import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.model.SManga @@ -28,81 +29,110 @@ import eu.kanade.tachiyomi.util.view.updateLayoutParams class MangaHeaderHolder( private val view: View, private val adapter: MangaDetailsAdapter, - startExpanded: Boolean + startExpanded: Boolean, + isTablet: Boolean = false ) : BaseFlexibleViewHolder(view, adapter) { - val binding = MangaHeaderItemBinding.bind(view) + val binding: MangaHeaderItemBinding? = try { + MangaHeaderItemBinding.bind(view) + } catch (e: Exception) { + null + } + private val chapterBinding: ChapterHeaderItemBinding? = try { + ChapterHeaderItemBinding.bind(view) + } catch (e: Exception) { + null + } private var showReadingButton = true private var showMoreButton = true var hadSelection = false init { - binding.chapterLayout.setOnClickListener { adapter.delegate.showChapterFilter() } - binding.startReadingButton.setOnClickListener { adapter.delegate.readNextChapter() } - binding.topView.updateLayoutParams { - height = adapter.delegate.topCoverHeight() - } - binding.moreButton.setOnClickListener { expandDesc() } - binding.mangaSummary.setOnClickListener { - if (binding.moreButton.isVisible) { - expandDesc() - } else if (!hadSelection) { - collapseDesc() - } else { - hadSelection = false + + if (binding == null) { + with(chapterBinding) { + this ?: return@with + chapterLayout.setOnClickListener { adapter.delegate.showChapterFilter() } } } - binding.mangaSummary.setOnLongClickListener { - if (binding.mangaSummary.isTextSelectable && !adapter.recyclerView.canScrollVertically(-1)) { - (adapter.delegate as MangaDetailsController).binding.swipeRefresh.isEnabled = false + with(binding) { + this ?: return@with + chapterLayout.setOnClickListener { adapter.delegate.showChapterFilter() } + startReadingButton.setOnClickListener { adapter.delegate.readNextChapter() } + topView.updateLayoutParams { + height = adapter.delegate.topCoverHeight() } - false - } - binding.mangaSummary.setOnTouchListener { _, event -> - if (event.action == MotionEvent.ACTION_DOWN) { - view.requestFocus() + moreButton.setOnClickListener { expandDesc() } + mangaSummary.setOnClickListener { + if (moreButton.isVisible) { + expandDesc() + } else if (!hadSelection) { + collapseDesc() + } else { + hadSelection = false + } } - if (event.actionMasked == MotionEvent.ACTION_UP) { - hadSelection = binding.mangaSummary.hasSelection() - (adapter.delegate as MangaDetailsController).binding.swipeRefresh.isEnabled = - true + mangaSummary.setOnLongClickListener { + if (mangaSummary.isTextSelectable && !adapter.recyclerView.canScrollVertically( + -1 + ) + ) { + (adapter.delegate as MangaDetailsController).binding.swipeRefresh.isEnabled = + false + } + false } - false + mangaSummary.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + view.requestFocus() + } + if (event.actionMasked == MotionEvent.ACTION_UP) { + hadSelection = mangaSummary.hasSelection() + (adapter.delegate as MangaDetailsController).binding.swipeRefresh.isEnabled = + true + } + false + } + if (!itemView.resources.isLTR) { + moreBgGradient.rotation = 180f + } + lessButton.setOnClickListener { collapseDesc() } + mangaGenresTags.setOnTagClickListener { + adapter.delegate.tagClicked(it) + } + webviewButton.setOnClickListener { adapter.delegate.openInWebView() } + shareButton.setOnClickListener { adapter.delegate.prepareToShareManga() } + favoriteButton.setOnClickListener { + adapter.delegate.favoriteManga(false) + } + title.setOnClickListener { + title.text?.let { adapter.delegate.globalSearch(it.toString()) } + } + title.setOnLongClickListener { + adapter.delegate.copyToClipboard(title.text.toString(), R.string.title) + true + } + mangaAuthor.setOnClickListener { + mangaAuthor.text?.let { adapter.delegate.globalSearch(it.toString()) } + } + mangaAuthor.setOnLongClickListener { + adapter.delegate.copyToClipboard( + mangaAuthor.text.toString(), + R.string.author + ) + true + } + mangaCover.setOnClickListener { adapter.delegate.zoomImageFromThumb(coverCard) } + trackButton.setOnClickListener { adapter.delegate.showTrackingSheet() } + if (startExpanded) expandDesc() + else collapseDesc() + if (isTablet) chapterLayout.isVisible = false } - if (!itemView.resources.isLTR) { - binding.moreBgGradient.rotation = 180f - } - binding.lessButton.setOnClickListener { collapseDesc() } - binding.mangaGenresTags.setOnTagClickListener { - adapter.delegate.tagClicked(it) - } - binding.webviewButton.setOnClickListener { adapter.delegate.openInWebView() } - binding.shareButton.setOnClickListener { adapter.delegate.prepareToShareManga() } - binding.favoriteButton.setOnClickListener { - adapter.delegate.favoriteManga(false) - } - binding.title.setOnClickListener { - binding.title.text?.let { adapter.delegate.globalSearch(it.toString()) } - } - binding.title.setOnLongClickListener { - adapter.delegate.copyToClipboard(binding.title.text.toString(), R.string.title) - true - } - binding.mangaAuthor.setOnClickListener { - binding.mangaAuthor.text?.let { adapter.delegate.globalSearch(it.toString()) } - } - binding.mangaAuthor.setOnLongClickListener { - adapter.delegate.copyToClipboard(binding.mangaAuthor.text.toString(), R.string.author) - true - } - binding.mangaCover.setOnClickListener { adapter.delegate.zoomImageFromThumb(binding.coverCard) } - binding.trackButton.setOnClickListener { adapter.delegate.showTrackingSheet() } - if (startExpanded) expandDesc() - else collapseDesc() } private fun expandDesc() { + binding ?: return if (binding.moreButton.visibility == View.VISIBLE) { binding.mangaSummary.maxLines = Integer.MAX_VALUE binding.mangaSummary.setTextIsSelectable(true) @@ -110,10 +140,12 @@ class MangaHeaderHolder( binding.lessButton.isVisible = true binding.moreButtonGroup.isVisible = false binding.title.maxLines = Integer.MAX_VALUE + binding.mangaSummary.requestFocus() } } private fun collapseDesc() { + binding ?: return binding.mangaSummary.setTextIsSelectable(false) binding.mangaSummary.isClickable = true binding.mangaSummary.maxLines = 3 @@ -129,13 +161,29 @@ class MangaHeaderHolder( fun bindChapters() { val presenter = adapter.delegate.mangaPresenter() val count = presenter.chapters.size - binding.chaptersTitle.text = itemView.resources.getQuantityString(R.plurals.chapters_plural, count, count) - binding.filtersText.text = presenter.currentFilters() + if (binding != null) { + binding.chaptersTitle.text = + itemView.resources.getQuantityString(R.plurals.chapters_plural, count, count) + binding.filtersText.text = presenter.currentFilters() + } else if (chapterBinding != null) { + chapterBinding.chaptersTitle.text = + itemView.resources.getQuantityString(R.plurals.chapters_plural, count, count) + chapterBinding.filtersText.text = presenter.currentFilters() + } } @SuppressLint("SetTextI18n") fun bind(item: MangaHeaderItem, manga: Manga) { val presenter = adapter.delegate.mangaPresenter() + if (binding == null) { + if (chapterBinding != null) { + val count = presenter.chapters.size + chapterBinding.chaptersTitle.text = + itemView.resources.getQuantityString(R.plurals.chapters_plural, count, count) + chapterBinding.filtersText.text = presenter.currentFilters() + } + return + } binding.title.text = manga.title if (manga.genre.isNullOrBlank().not()) binding.mangaGenresTags.setTags( @@ -281,16 +329,20 @@ class MangaHeaderHolder( } fun setTopHeight(newHeight: Int) { + binding ?: return + if (newHeight == binding.topView.height) return binding.topView.updateLayoutParams { height = newHeight } } fun setBackDrop(color: Int) { + binding ?: return binding.trueBackdrop.setBackgroundColor(color) } fun updateTracking() { + binding ?: return val presenter = adapter.delegate.mangaPresenter() val tracked = presenter.isTracked() with(binding.trackButton) { @@ -309,6 +361,7 @@ class MangaHeaderHolder( } fun collapse() { + binding ?: return binding.subItemGroup.isVisible = false binding.startReadingButton.isVisible = false if (binding.moreButton.isVisible || binding.moreButton.isInvisible) { @@ -320,6 +373,7 @@ class MangaHeaderHolder( } fun updateCover(manga: Manga) { + binding ?: return if (!manga.initialized) return val drawable = adapter.controller.binding.mangaCoverFull.drawable binding.mangaCover.loadManga( @@ -343,6 +397,7 @@ class MangaHeaderHolder( } fun expand() { + binding ?: return binding.subItemGroup.isVisible = true if (!showMoreButton) binding.moreButtonGroup.isVisible = false else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderItem.kt index 3e1c9c6984..336f6674da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderItem.kt @@ -13,6 +13,7 @@ class MangaHeaderItem(val manga: Manga, var startExpanded: Boolean) : var isChapterHeader = false var isLocked = false + var isTablet = false override fun getLayoutRes(): Int { return if (isChapterHeader) R.layout.chapter_header_item else R.layout.manga_header_item @@ -27,7 +28,7 @@ class MangaHeaderItem(val manga: Manga, var startExpanded: Boolean) : } override fun createViewHolder(view: View, adapter: FlexibleAdapter>): MangaHeaderHolder { - return MangaHeaderHolder(view, adapter as MangaDetailsAdapter, startExpanded) + return MangaHeaderHolder(view, adapter as MangaDetailsAdapter, startExpanded, isTablet) } override fun bindViewHolder( diff --git a/app/src/main/res/layout/chapter_header_item.xml b/app/src/main/res/layout/chapter_header_item.xml index 76f2560cf2..8adbd85bee 100644 --- a/app/src/main/res/layout/chapter_header_item.xml +++ b/app/src/main/res/layout/chapter_header_item.xml @@ -31,11 +31,11 @@ android:id="@+id/webview_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginEnd="20dp" + android:layout_marginEnd="12dp" android:background="@null" android:padding="5dp" android:src="@drawable/ic_filter_list_24dp" - android:tint="?colorAccent" + app:tint="?colorAccent" app:layout_constraintBottom_toBottomOf="@id/chapters_title" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/chapters_title" /> diff --git a/app/src/main/res/layout/manga_details_controller.xml b/app/src/main/res/layout/manga_details_controller.xml index 4316202a78..c7c2f98e04 100644 --- a/app/src/main/res/layout/manga_details_controller.xml +++ b/app/src/main/res/layout/manga_details_controller.xml @@ -13,19 +13,58 @@ android:layout_height="match_parent" android:background="?android:colorBackground"> - - + android:layout_height="match_parent" + android:orientation="horizontal"> + + - + + + + + -