mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
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:
parent
9ade0d68f9
commit
2f16e01513
7 changed files with 142 additions and 23 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue