Use shared element transition for manga details to reader

expands on tachiyomiorg/tachiyomi@bdef2cfdfb by supporting opening chapters and closing the reader on the chapter they finished on (if it was visible)

Co-Authored-By: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
This commit is contained in:
Jays2Kings 2022-04-24 03:49:23 -04:00
parent 9ade0d68f9
commit 2f16e01513
7 changed files with 142 additions and 23 deletions

View file

@ -20,6 +20,7 @@ import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.annotation.IdRes
import androidx.appcompat.view.menu.ActionMenuItemView
@ -47,6 +48,7 @@ import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView
import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R
@ -179,6 +181,27 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), DownloadServiceLi
}
override fun onCreate(savedInstanceState: Bundle?) {
// Set up shared element transition and disable overlay so views don't show above system bars
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
setExitSharedElementCallback(object : MaterialContainerTransformSharedElementCallback() {
override fun onMapSharedElements(
names: MutableList<String>,
sharedElements: MutableMap<String, View>
) {
val mangaController = router.backstack.lastOrNull()?.controller as? MangaDetailsController
if (mangaController == null || chapterIdToExitTo == 0L) {
super.onMapSharedElements(names, sharedElements)
return
}
val recyclerView = mangaController.binding.recycler
val selectedViewHolder =
recyclerView.findViewHolderForItemId(chapterIdToExitTo) ?: return
sharedElements[names[0]] = selectedViewHolder.itemView
chapterIdToExitTo = 0L
}
})
window.sharedElementsUseOverlay = false
super.onCreate(savedInstanceState)
// Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
@ -1244,6 +1267,8 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), DownloadServiceLi
const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH"
const val INTENT_SEARCH_QUERY = "query"
const val INTENT_SEARCH_FILTER = "filter"
var chapterIdToExitTo = 0L
}
}

View file

@ -129,7 +129,7 @@ class MangaDetailsAdapter(
fun prepareToShareManga()
fun openInWebView()
fun startDownloadRange(position: Int)
fun readNextChapter()
fun readNextChapter(readingButton: View)
fun topCoverHeight(): Int
fun tagClicked(text: String)
fun globalSearch(text: String)

View file

@ -24,6 +24,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
import androidx.core.app.ActivityOptionsCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars
@ -197,7 +198,8 @@ class MangaDetailsController :
return manga?.title
}
override fun createBinding(inflater: LayoutInflater) = MangaDetailsControllerBinding.inflate(inflater)
override fun createBinding(inflater: LayoutInflater) =
MangaDetailsControllerBinding.inflate(inflater)
//region UI Methods
override fun onViewCreated(view: View) {
@ -379,7 +381,8 @@ class MangaDetailsController :
if (isTablet) {
val tHeight = toolbarHeight.takeIf { it ?: 0 > 0 } ?: appbarHeight
val insetsCompat = view.rootWindowInsetsCompat ?: activityBinding?.root?.rootWindowInsetsCompat
val insetsCompat =
view.rootWindowInsetsCompat ?: activityBinding?.root?.rootWindowInsetsCompat
headerHeight = tHeight + (insetsCompat?.getInsets(systemBars())?.top ?: 0)
binding.recycler.updatePaddingRelative(top = headerHeight + 4.dpToPx)
}
@ -418,20 +421,28 @@ class MangaDetailsController :
}
private fun setInsets(insets: WindowInsetsCompat, appbarHeight: Int, offset: Int) {
binding.recycler.updatePaddingRelative(bottom = insets.getInsets(systemBars()).bottom)
binding.tabletRecycler.updatePaddingRelative(bottom = insets.getInsets(systemBars()).bottom)
val systemInsets =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
insets.getInsetsIgnoringVisibility(systemBars())
} else {
insets.getInsets(systemBars())
}
binding.recycler.updatePaddingRelative(bottom = systemInsets.bottom)
binding.tabletRecycler.updatePaddingRelative(bottom = systemInsets.bottom)
val tHeight = toolbarHeight.takeIf { it ?: 0 > 0 } ?: appbarHeight
headerHeight = tHeight + insets.getInsets(systemBars()).top
headerHeight = tHeight + systemInsets.top
binding.swipeRefresh.setProgressViewOffset(false, (-40).dpToPx, headerHeight + offset)
if (isTablet) {
binding.tabletOverlay.updateLayoutParams<ViewGroup.LayoutParams> { height = headerHeight }
binding.tabletOverlay.updateLayoutParams<ViewGroup.LayoutParams> {
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<ViewGroup.MarginLayoutParams> {
topMargin = headerHeight
bottomMargin = insets.getInsets(systemBars()).bottom
bottomMargin = systemInsets.bottom
}
binding.fastScroller.scrollOffset = headerHeight
}
@ -449,7 +460,8 @@ class MangaDetailsController :
}
val scrollingColor = headerColor ?: activity.getResourceColor(R.attr.colorPrimaryVariant)
val topColor = ColorUtils.setAlphaComponent(scrollingColor, 0)
val scrollingStatusColor = ColorUtils.setAlphaComponent(scrollingColor, (0.87f * 255).roundToInt())
val scrollingStatusColor =
ColorUtils.setAlphaComponent(scrollingColor, (0.87f * 255).roundToInt())
colorAnimator?.cancel()
if (animate) {
val cA = ValueAnimator.ofFloat(
@ -479,7 +491,8 @@ class MangaDetailsController :
cA.start()
} else {
activityBinding?.appBar?.setBackgroundColor(if (toolbarIsColored) scrollingColor else topColor)
activity.window?.statusBarColor = if (toolbarIsColored) scrollingStatusColor else topColor
activity.window?.statusBarColor =
if (toolbarIsColored) scrollingStatusColor else topColor
}
}
@ -487,7 +500,8 @@ class MangaDetailsController :
fun setPaletteColor() {
val view = view ?: return
val request = ImageRequest.Builder(view.context).data(presenter.manga).allowHardware(false).memoryCacheKey(presenter.manga.key())
val request = ImageRequest.Builder(view.context).data(presenter.manga).allowHardware(false)
.memoryCacheKey(presenter.manga.key())
.target(
onSuccess = { drawable ->
val bitmap = (drawable as? BitmapDrawable)?.bitmap
@ -527,7 +541,8 @@ class MangaDetailsController :
private fun setStatusBarAndToolbar() {
val topColor = Color.TRANSPARENT
val scrollingColor = headerColor ?: activity!!.getResourceColor(R.attr.colorPrimaryVariant)
val scrollingStatusColor = ColorUtils.setAlphaComponent(scrollingColor, (0.87f * 255).roundToInt())
val scrollingStatusColor =
ColorUtils.setAlphaComponent(scrollingColor, (0.87f * 255).roundToInt())
activity?.window?.statusBarColor = if (toolbarIsColored) scrollingStatusColor else topColor
activityBinding?.appBar?.setBackgroundColor(
if (toolbarIsColored) scrollingColor else topColor
@ -542,12 +557,14 @@ class MangaDetailsController :
presenter.isLockedFromSearch = shouldLockIfNeeded && SecureActivityDelegate.shouldBeLocked()
presenter.headerItem.isLocked = presenter.isLockedFromSearch
manga!!.thumbnail_url = presenter.refreshMangaFromDb().thumbnail_url
presenter.fetchChapters(refreshTracker == null)
// presenter.fetchChapters(refreshTracker == null)
if (refreshTracker != null) {
trackingBottomSheet?.refreshItem(refreshTracker ?: 0)
presenter.refreshTracking()
refreshTracker = null
}
// activity.postponeEnterTransition()
// listenForChange()
// fetch cover again in case the user set a new cover while reading
setPaletteColor()
val isCurrentController = router?.backstack?.lastOrNull()?.controller ==
@ -755,7 +772,7 @@ class MangaDetailsController :
}
return false
}
openChapter(chapter)
openChapter(chapter, view)
return false
}
@ -915,10 +932,33 @@ class MangaDetailsController :
presenter.markChaptersRead(chapters, false)
}
private fun openChapter(chapter: Chapter) {
val activity = activity ?: return
val intent = ReaderActivity.newIntent(activity, manga!!, chapter)
startActivity(intent)
private fun openChapter(chapter: Chapter, sharedElement: View? = null) {
MainActivity.chapterIdToExitTo = 0L
(activity as? AppCompatActivity)?.apply {
val intent = ReaderActivity.newIntent(this, manga!!, chapter)
if (sharedElement != null) {
val activityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation(
this, sharedElement, sharedElement.transitionName
)
val firstPos = (binding.recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
val lastPos = (binding.recycler.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
val chapterRange = if (firstPos > -1 && lastPos > -1) {
(firstPos..lastPos).mapNotNull {
(adapter?.getItem(it) as? ChapterItem)?.chapter?.id
}.toLongArray()
} else longArrayOf()
startActivity(
intent.apply {
putExtra(ReaderActivity.TRANSITION_NAME, sharedElement.transitionName)
putExtra(ReaderActivity.VISIBLE_CHAPTERS, chapterRange)
},
activityOptions.toBundle(),
)
} else {
startActivity(intent)
}
}
}
//region action bar menu methods
@ -1243,14 +1283,14 @@ class MangaDetailsController :
onItemClick(null, position)
}
override fun readNextChapter() {
override fun readNextChapter(readingButton: View) {
if (activity is SearchActivity && presenter.isLockedFromSearch) {
SecureActivityDelegate.promptLockIfNeeded(activity)
return
}
val item = presenter.getNextUnreadChapter()
if (item != null) {
openChapter(item.chapter)
openChapter(item.chapter, readingButton)
} else if (snack == null ||
snack?.getText() != view?.context?.getString(R.string.next_chapter_not_found)
) {

View file

@ -77,7 +77,7 @@ class MangaHeaderHolder(
with(binding) {
this ?: return@with
chapterLayout.setOnClickListener { adapter.delegate.showChapterFilter() }
startReadingButton.setOnClickListener { adapter.delegate.readNextChapter() }
startReadingButton.setOnClickListener { adapter.delegate.readNextChapter(it) }
topView.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = adapter.delegate.topCoverHeight()
}

View file

@ -36,6 +36,7 @@ class ChapterHolder(
fun bind(item: ChapterItem, manga: Manga) {
val chapter = item.chapter
val isLocked = item.isLocked
itemView.transitionName = "details chapter ${chapter.id ?: 0L} transition"
binding.chapterTitle.text = if (manga.hideChapterTitle(adapter.preferences)) {
val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
itemView.context.getString(R.string.chapter_, number)

View file

@ -23,11 +23,13 @@ import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.transition.addListener
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
@ -47,6 +49,9 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.slider.Slider
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.platform.MaterialContainerTransform
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
@ -189,12 +194,18 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
val isSplitScreen: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode
var didTransistionFromChapter = false
var visibleChapterRange = longArrayOf()
companion object {
const val SHIFT_DOUBLE_PAGES = "shiftingDoublePages"
const val SHIFTED_PAGE_INDEX = "shiftedPageIndex"
const val SHIFTED_CHAP_INDEX = "shiftedChapterIndex"
const val TRANSITION_NAME = "${BuildConfig.APPLICATION_ID}.TRANSITION_NAME"
const val VISIBLE_CHAPTERS = "${BuildConfig.APPLICATION_ID}.VISIBLE_CHAPTERS"
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
val intent = Intent(context, ReaderActivity::class.java)
intent.putExtra("manga", manga.id)
@ -208,6 +219,22 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
* Called when the activity is created. Initializes the presenter and configuration.
*/
override fun onCreate(savedInstanceState: Bundle?) {
// Setup shared element transitions
if (intent.extras?.getString(TRANSITION_NAME) != null) {
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
findViewById<View>(android.R.id.content)?.let { contentView ->
MainActivity.chapterIdToExitTo = 0L
contentView.transitionName = intent.extras?.getString(TRANSITION_NAME)
visibleChapterRange = intent.extras?.getLongArray(VISIBLE_CHAPTERS) ?: longArrayOf()
didTransistionFromChapter = !contentView.transitionName.contains("start")
setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback())
window.sharedElementEnterTransition = buildContainerTransform(true)
window.sharedElementReturnTransition = buildContainerTransform(false)
// Postpone custom transition until manga ready
postponeEnterTransition()
}
}
super.onCreate(savedInstanceState)
binding = ReaderActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
@ -459,7 +486,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
return
}
presenter.onBackPressed()
finish()
if (didTransistionFromChapter && visibleChapterRange.isNotEmpty() && MainActivity.chapterIdToExitTo !in visibleChapterRange) {
finish()
} else {
super.onBackPressed()
}
}
/**
@ -517,6 +548,13 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
return handled || super.dispatchGenericMotionEvent(event)
}
private fun buildContainerTransform(entering: Boolean): MaterialContainerTransform {
return MaterialContainerTransform(this, entering).apply {
duration = 350 // ms
addTarget(android.R.id.content)
}
}
/**
* Initializes the reader menu. It sets up click listeners and the initial visibility.
*/
@ -983,7 +1021,16 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
}
}
setOrientation(presenter.getMangaOrientationType())
if (window.sharedElementEnterTransition is MaterialContainerTransform &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.S
) {
// Wait until transition is complete to avoid crash on API 26
window.sharedElementEnterTransition.addListener(
onEnd = { setOrientation(presenter.getMangaOrientationType()) },
)
} else {
setOrientation(presenter.getMangaOrientationType())
}
// Destroy previous viewer if there was one
if (prevViewer != null) {
@ -1030,6 +1077,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
updateBottomShortcuts()
val viewerMode = ReadingModeType.fromPreference(presenter?.manga?.readingModeType ?: 0)
binding.chaptersSheet.readingMode.setImageResource(viewerMode.iconRes)
startPostponedEnterTransition()
}
override fun onPause() {
@ -1070,6 +1118,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
* method to the current viewer, but also set the subtitle on the binding.toolbar.
*/
fun setChapters(viewerChapters: ViewerChapters) {
binding.pleaseWait.clearAnimation()
binding.pleaseWait.isVisible = false
if (indexChapterToShift != null && indexPageToShift != null) {
viewerChapters.currChapter.pages?.find { it.index == indexPageToShift && it.chapter.chapter.id == indexChapterToShift }?.let {
@ -1108,6 +1157,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
binding.readerNav.rightChapter.alpha = if (viewerChapters.nextChapter != null) 1f else 0.5f
binding.readerNav.leftChapter.alpha = if (viewerChapters.prevChapter != null) 1f else 0.5f
}
if (didTransistionFromChapter) {
MainActivity.chapterIdToExitTo = viewerChapters.currChapter.chapter.id ?: 0L
}
}
/**

View file

@ -357,6 +357,7 @@
style="@style/Theme.Widget.Button.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:transitionName="details start reading transition"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"