mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Manga description adjustments (#6011)
* Manga description adjustments - Animated state changes - Adjust scrim position to fully show 2 lines when shrunk - Set minLines to avoid scrim hiding oneliner * Change icon and adjust animation * Revert fancy scrim animation
This commit is contained in:
		| @@ -20,9 +20,7 @@ import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaController | ||||
| import eu.kanade.tachiyomi.util.system.copyToClipboard | ||||
| import eu.kanade.tachiyomi.util.view.loadAnyAutoPause | ||||
| import eu.kanade.tachiyomi.util.view.setChips | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.merge | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import reactivecircus.flowbinding.android.view.clicks | ||||
| import reactivecircus.flowbinding.android.view.longClicks | ||||
| @@ -45,13 +43,14 @@ class MangaInfoHeaderAdapter( | ||||
|  | ||||
|     private lateinit var binding: MangaInfoHeaderBinding | ||||
|  | ||||
|     private var initialLoad: Boolean = true | ||||
|  | ||||
|     private val maxLines = 3 | ||||
|  | ||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder { | ||||
|         binding = MangaInfoHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||||
|         updateCoverPosition() | ||||
|  | ||||
|         // Expand manga info if navigated from source listing or explicitly set to | ||||
|         // (e.g. on tablets) | ||||
|         binding.mangaSummarySection.expanded = fromSource || isTablet | ||||
|  | ||||
|         return HeaderViewHolder(binding.root) | ||||
|     } | ||||
|  | ||||
| @@ -180,15 +179,6 @@ class MangaInfoHeaderAdapter( | ||||
|                 } | ||||
|                 .launchIn(controller.viewScope) | ||||
|  | ||||
|             binding.mangaSummaryText.longClicks() | ||||
|                 .onEach { | ||||
|                     controller.activity?.copyToClipboard( | ||||
|                         view.context.getString(R.string.description), | ||||
|                         binding.mangaSummaryText.text.toString() | ||||
|                     ) | ||||
|                 } | ||||
|                 .launchIn(controller.viewScope) | ||||
|  | ||||
|             binding.mangaCover.clicks() | ||||
|                 .onEach { | ||||
|                     controller.showFullCoverDialog() | ||||
| @@ -201,7 +191,7 @@ class MangaInfoHeaderAdapter( | ||||
|                 } | ||||
|                 .launchIn(controller.viewScope) | ||||
|  | ||||
|             setMangaInfo(manga, source) | ||||
|             setMangaInfo() | ||||
|         } | ||||
|  | ||||
|         private fun showCoverOptionsDialog() { | ||||
| @@ -231,7 +221,7 @@ class MangaInfoHeaderAdapter( | ||||
|          * @param manga manga object containing information about manga. | ||||
|          * @param source the source of the manga. | ||||
|          */ | ||||
|         private fun setMangaInfo(manga: Manga, source: Source?) { | ||||
|         private fun setMangaInfo() { | ||||
|             // Update full title TextView. | ||||
|             binding.mangaFullTitle.text = if (manga.title.isBlank()) { | ||||
|                 view.context.getString(R.string.unknown) | ||||
| @@ -254,27 +244,23 @@ class MangaInfoHeaderAdapter( | ||||
|             } | ||||
|  | ||||
|             // If manga source is known update source TextView. | ||||
|             val mangaSource = source?.toString() | ||||
|             val mangaSource = source.toString() | ||||
|             with(binding.mangaSource) { | ||||
|                 if (mangaSource != null) { | ||||
|                     val enabledLanguages = preferences.enabledLanguages().get() | ||||
|                         .filterNot { it in listOf("all", "other") } | ||||
|                 val enabledLanguages = preferences.enabledLanguages().get() | ||||
|                     .filterNot { it in listOf("all", "other") } | ||||
|  | ||||
|                     val hasOneActiveLanguages = enabledLanguages.size == 1 | ||||
|                     val isInEnabledLanguages = source.lang in enabledLanguages | ||||
|                     text = when { | ||||
|                         // For edge cases where user disables a source they got manga of in their library. | ||||
|                         hasOneActiveLanguages && !isInEnabledLanguages -> mangaSource | ||||
|                         // Hide the language tag when only one language is used. | ||||
|                         hasOneActiveLanguages && isInEnabledLanguages -> source.name | ||||
|                         else -> mangaSource | ||||
|                     } | ||||
|                 val hasOneActiveLanguages = enabledLanguages.size == 1 | ||||
|                 val isInEnabledLanguages = source.lang in enabledLanguages | ||||
|                 text = when { | ||||
|                     // For edge cases where user disables a source they got manga of in their library. | ||||
|                     hasOneActiveLanguages && !isInEnabledLanguages -> mangaSource | ||||
|                     // Hide the language tag when only one language is used. | ||||
|                     hasOneActiveLanguages && isInEnabledLanguages -> source.name | ||||
|                     else -> mangaSource | ||||
|                 } | ||||
|  | ||||
|                     setOnClickListener { | ||||
|                         controller.performSearch(sourceManager.getOrStub(source.id).name) | ||||
|                     } | ||||
|                 } else { | ||||
|                     text = view.context.getString(R.string.unknown) | ||||
|                 setOnClickListener { | ||||
|                     controller.performSearch(sourceManager.getOrStub(source.id).name) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
| @@ -296,84 +282,9 @@ class MangaInfoHeaderAdapter( | ||||
|             binding.mangaCover.loadAnyAutoPause(manga) | ||||
|  | ||||
|             // Manga info section | ||||
|             val hasInfoContent = !manga.description.isNullOrBlank() || !manga.genre.isNullOrBlank() | ||||
|             showMangaInfo(hasInfoContent) | ||||
|             if (hasInfoContent) { | ||||
|                 // Update description TextView. | ||||
|                 binding.mangaSummaryText.text = updateDescription(manga.description, (fromSource || isTablet).not()) | ||||
|  | ||||
|                 // Update genres list | ||||
|                 if (!manga.genre.isNullOrBlank()) { | ||||
|                     binding.mangaGenresTagsCompactChips.setChips( | ||||
|                         manga.getGenres(), | ||||
|                         controller::performGenreSearch | ||||
|                     ) | ||||
|                     binding.mangaGenresTagsFullChips.setChips( | ||||
|                         manga.getGenres(), | ||||
|                         controller::performGenreSearch | ||||
|                     ) | ||||
|                 } else { | ||||
|                     binding.mangaGenresTagsCompact.isVisible = false | ||||
|                     binding.mangaGenresTagsCompactChips.isVisible = false | ||||
|                     binding.mangaGenresTagsFullChips.isVisible = false | ||||
|                 } | ||||
|  | ||||
|                 // Handle showing more or less info | ||||
|                 merge( | ||||
|                     binding.mangaSummaryText.clicks(), | ||||
|                     binding.mangaInfoToggleMore.clicks(), | ||||
|                     binding.mangaInfoToggleLess.clicks(), | ||||
|                     binding.mangaSummarySection.clicks(), | ||||
|                 ) | ||||
|                     .onEach { toggleMangaInfo() } | ||||
|                     .launchIn(controller.viewScope) | ||||
|  | ||||
|                 if (initialLoad) { | ||||
|                     binding.mangaGenresTagsCompact.requestLayout() | ||||
|                 } | ||||
|  | ||||
|                 // Expand manga info if navigated from source listing or explicitly set to | ||||
|                 // (e.g. on tablets) | ||||
|                 if (initialLoad && (fromSource || isTablet)) { | ||||
|                     toggleMangaInfo() | ||||
|                     initialLoad = false | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private fun showMangaInfo(visible: Boolean) { | ||||
|             binding.mangaSummarySection.isVisible = visible | ||||
|         } | ||||
|  | ||||
|         private fun toggleMangaInfo() { | ||||
|             val isCurrentlyExpanded = binding.mangaSummaryText.maxLines != maxLines | ||||
|  | ||||
|             binding.mangaInfoToggleMore.isVisible = isCurrentlyExpanded | ||||
|             binding.mangaInfoScrim.isVisible = isCurrentlyExpanded | ||||
|             binding.mangaInfoToggleMoreScrim.isVisible = isCurrentlyExpanded | ||||
|             binding.mangaGenresTagsCompact.isVisible = isCurrentlyExpanded | ||||
|             binding.mangaGenresTagsCompactChips.isVisible = isCurrentlyExpanded | ||||
|  | ||||
|             binding.mangaInfoToggleLess.isVisible = !isCurrentlyExpanded | ||||
|             binding.mangaGenresTagsFullChips.isVisible = !isCurrentlyExpanded | ||||
|  | ||||
|             binding.mangaSummaryText.text = updateDescription(manga.description, isCurrentlyExpanded) | ||||
|  | ||||
|             binding.mangaSummaryText.maxLines = when { | ||||
|                 isCurrentlyExpanded -> maxLines | ||||
|                 else -> Int.MAX_VALUE | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private fun updateDescription(description: String?, isCurrentlyExpanded: Boolean): CharSequence { | ||||
|             return when { | ||||
|                 description.isNullOrBlank() -> view.context.getString(R.string.unknown) | ||||
|                 isCurrentlyExpanded -> | ||||
|                     description | ||||
|                         .replace(Regex(" +\$", setOf(RegexOption.MULTILINE)), "") | ||||
|                         .replace(Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)), "\n") | ||||
|                 else -> description | ||||
|             } | ||||
|             binding.mangaSummarySection.isVisible = !manga.description.isNullOrBlank() || !manga.genre.isNullOrBlank() | ||||
|             binding.mangaSummarySection.description = manga.description | ||||
|             binding.mangaSummarySection.setTags(manga.getGenres(), controller::performGenreSearch) | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|   | ||||
							
								
								
									
										188
									
								
								app/src/main/java/eu/kanade/tachiyomi/widget/MangaSummaryView.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								app/src/main/java/eu/kanade/tachiyomi/widget/MangaSummaryView.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,188 @@ | ||||
| package eu.kanade.tachiyomi.widget | ||||
|  | ||||
| import android.animation.AnimatorSet | ||||
| import android.animation.ValueAnimator | ||||
| import android.content.Context | ||||
| import android.graphics.drawable.Animatable | ||||
| import android.util.AttributeSet | ||||
| import android.view.LayoutInflater | ||||
| import android.widget.FrameLayout | ||||
| import androidx.annotation.AttrRes | ||||
| import androidx.annotation.StyleRes | ||||
| import androidx.constraintlayout.widget.ConstraintLayout | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.core.view.doOnNextLayout | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.core.view.updateLayoutParams | ||||
| import androidx.interpolator.view.animation.FastOutSlowInInterpolator | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.databinding.MangaSummaryBinding | ||||
| import eu.kanade.tachiyomi.util.system.animatorDurationScale | ||||
| import eu.kanade.tachiyomi.util.system.copyToClipboard | ||||
| import eu.kanade.tachiyomi.util.view.setChips | ||||
| import kotlin.math.roundToInt | ||||
| import kotlin.math.roundToLong | ||||
|  | ||||
| class MangaSummaryView @JvmOverloads constructor( | ||||
|     context: Context, | ||||
|     attrs: AttributeSet? = null, | ||||
|     @AttrRes defStyleAttr: Int = 0, | ||||
|     @StyleRes defStyleRes: Int = 0 | ||||
| ) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) { | ||||
|  | ||||
|     private val binding = MangaSummaryBinding.inflate(LayoutInflater.from(context), this, true) | ||||
|  | ||||
|     private var animatorSet: AnimatorSet? = null | ||||
|  | ||||
|     private var recalculateHeights = false | ||||
|     private var descExpandedHeight = -1 | ||||
|     private var descShrunkHeight = -1 | ||||
|  | ||||
|     var expanded = false | ||||
|         set(value) { | ||||
|             if (field != value) { | ||||
|                 field = value | ||||
|                 updateExpandState() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|     var description: CharSequence? = null | ||||
|         set(value) { | ||||
|             if (field != value) { | ||||
|                 field = if (value.isNullOrBlank()) { | ||||
|                     context.getString(R.string.unknown) | ||||
|                 } else { | ||||
|                     value | ||||
|                 } | ||||
|                 binding.descriptionText.text = field | ||||
|                 recalculateHeights = true | ||||
|                 doOnNextLayout { | ||||
|                     updateExpandState() | ||||
|                 } | ||||
|                 requestLayout() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|     fun setTags(items: List<String>?, onClick: (item: String) -> Unit) { | ||||
|         binding.tagChipsShrunk.setChips(items, onClick) | ||||
|         binding.tagChipsExpanded.setChips(items, onClick) | ||||
|     } | ||||
|  | ||||
|     private fun updateExpandState() = binding.apply { | ||||
|         val initialSetup = descriptionText.maxHeight < 0 | ||||
|  | ||||
|         val maxHeightTarget = if (expanded) descExpandedHeight else descShrunkHeight | ||||
|         val maxHeightStart = if (initialSetup) maxHeightTarget else descriptionText.maxHeight | ||||
|         val descMaxHeightAnimator = ValueAnimator().apply { | ||||
|             setIntValues(maxHeightStart, maxHeightTarget) | ||||
|             addUpdateListener { | ||||
|                 descriptionText.maxHeight = it.animatedValue as Int | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         val toggleDrawable = ContextCompat.getDrawable( | ||||
|             context, | ||||
|             if (expanded) R.drawable.anim_caret_up else R.drawable.anim_caret_down | ||||
|         ) | ||||
|         toggleMore.setImageDrawable(toggleDrawable) | ||||
|  | ||||
|         var pastHalf = false | ||||
|         val toggleTarget = if (expanded) 1F else 0F | ||||
|         val toggleStart = if (initialSetup) { | ||||
|             toggleTarget | ||||
|         } else { | ||||
|             toggleMore.translationY / toggleMore.height | ||||
|         } | ||||
|         val toggleAnimator = ValueAnimator().apply { | ||||
|             setFloatValues(toggleStart, toggleTarget) | ||||
|             addUpdateListener { | ||||
|                 val value = it.animatedValue as Float | ||||
|  | ||||
|                 toggleMore.translationY = toggleMore.height * value | ||||
|                 descriptionScrim.translationY = toggleMore.translationY | ||||
|                 toggleMoreScrim.translationY = toggleMore.translationY | ||||
|                 tagChipsShrunkContainer.updateLayoutParams<ConstraintLayout.LayoutParams> { | ||||
|                     topMargin = toggleMore.translationY.roundToInt() | ||||
|                 } | ||||
|                 tagChipsExpanded.updateLayoutParams<ConstraintLayout.LayoutParams> { | ||||
|                     topMargin = toggleMore.translationY.roundToInt() | ||||
|                 } | ||||
|  | ||||
|                 // Update non-animatable objects mid-animation makes it feel less abrupt | ||||
|                 if (it.animatedFraction >= 0.5F && !pastHalf) { | ||||
|                     pastHalf = true | ||||
|                     descriptionText.text = trimWhenNeeded(description) | ||||
|                     tagChipsShrunkContainer.scrollX = 0 | ||||
|                     tagChipsShrunkContainer.isVisible = !expanded | ||||
|                     tagChipsExpanded.isVisible = expanded | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         animatorSet?.cancel() | ||||
|         animatorSet = AnimatorSet().apply { | ||||
|             interpolator = FastOutSlowInInterpolator() | ||||
|             duration = (TOGGLE_ANIM_DURATION * context.animatorDurationScale).roundToLong() | ||||
|             playTogether(toggleAnimator, descMaxHeightAnimator) | ||||
|             start() | ||||
|         } | ||||
|         (toggleDrawable as? Animatable)?.start() | ||||
|     } | ||||
|  | ||||
|     private fun trimWhenNeeded(text: CharSequence?): CharSequence? { | ||||
|         return if (!expanded) { | ||||
|             text | ||||
|                 ?.replace(Regex(" +\$", setOf(RegexOption.MULTILINE)), "") | ||||
|                 ?.replace(Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)), "\n") | ||||
|         } else { | ||||
|             text | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { | ||||
|         if (!recalculateHeights) { | ||||
|             super.onMeasure(widthMeasureSpec, heightMeasureSpec) | ||||
|             return | ||||
|         } | ||||
|         recalculateHeights = false | ||||
|  | ||||
|         // Measure with expanded lines | ||||
|         binding.descriptionText.maxLines = Int.MAX_VALUE | ||||
|         super.onMeasure(widthMeasureSpec, heightMeasureSpec) | ||||
|         descExpandedHeight = binding.descriptionText.measuredHeight | ||||
|  | ||||
|         // Measure with shrunk lines | ||||
|         binding.descriptionText.maxLines = SHRUNK_DESC_MAX_LINES | ||||
|         super.onMeasure(widthMeasureSpec, heightMeasureSpec) | ||||
|         descShrunkHeight = binding.descriptionText.measuredHeight | ||||
|     } | ||||
|  | ||||
|     init { | ||||
|         binding.descriptionText.apply { | ||||
|             // So that 1 line of text won't be hidden by scrim | ||||
|             minLines = DESC_MIN_LINES | ||||
|  | ||||
|             setOnLongClickListener { | ||||
|                 context.copyToClipboard( | ||||
|                     context.getString(R.string.description), | ||||
|                     text.toString() | ||||
|                 ) | ||||
|                 true | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         arrayOf( | ||||
|             binding.descriptionText, | ||||
|             binding.descriptionScrim, | ||||
|             binding.toggleMoreScrim, | ||||
|             binding.toggleMore | ||||
|         ).forEach { | ||||
|             it.setOnClickListener { expanded = !expanded } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| private const val TOGGLE_ANIM_DURATION = 300L | ||||
|  | ||||
| private const val DESC_MIN_LINES = 2 | ||||
| private const val SHRUNK_DESC_MAX_LINES = 3 | ||||
		Reference in New Issue
	
	Block a user