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
(cherry picked from commit f32f1eeaa5)
# Conflicts:
# app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt
# app/src/main/res/layout-sw720dp/manga_info_header.xml
# app/src/main/res/layout/manga_info_header.xml
This commit is contained in:
@@ -8,7 +8,6 @@ import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@@ -21,17 +20,14 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
|
||||
import eu.kanade.tachiyomi.util.view.setChips
|
||||
import exh.merged.sql.models.MergedMangaReference
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import exh.source.getMainSource
|
||||
import exh.util.SourceTagsUtil
|
||||
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
|
||||
@@ -58,17 +54,17 @@ class MangaInfoHeaderAdapter(
|
||||
// SY <--
|
||||
private var trackCount: Int = 0
|
||||
private var metaInfoAdapter: RecyclerView.Adapter<*>? = null
|
||||
private var mangaTagsInfoAdapter: NamespaceTagsAdapter? = NamespaceTagsAdapter(controller, source)
|
||||
|
||||
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
|
||||
|
||||
// SY -->
|
||||
metaInfoAdapter = source.getMainSource<MetadataSource<*, *>>()?.getDescriptionAdapter(controller)
|
||||
binding.metadataView.isVisible = if (metaInfoAdapter != null) {
|
||||
@@ -78,9 +74,6 @@ class MangaInfoHeaderAdapter(
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
binding.genreGroups.layoutManager = LinearLayoutManager(binding.root.context)
|
||||
binding.genreGroups.adapter = mangaTagsInfoAdapter
|
||||
// SY <--
|
||||
|
||||
return HeaderViewHolder(binding.root)
|
||||
@@ -132,15 +125,6 @@ class MangaInfoHeaderAdapter(
|
||||
// For rounded corners
|
||||
binding.mangaCover.clipToOutline = true
|
||||
|
||||
// SY -->
|
||||
mangaTagsInfoAdapter?.mItemClickListener = FlexibleAdapter.OnItemClickListener { _, _ ->
|
||||
controller.viewScope.launchUI {
|
||||
toggleMangaInfo()
|
||||
}
|
||||
false
|
||||
}
|
||||
// SY <--
|
||||
|
||||
binding.btnFavorite.clicks()
|
||||
.onEach { controller.onFavoriteClick() }
|
||||
.launchIn(controller.viewScope)
|
||||
@@ -252,15 +236,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()
|
||||
@@ -273,7 +248,7 @@ class MangaInfoHeaderAdapter(
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
setMangaInfo(manga, source, meta)
|
||||
setMangaInfo()
|
||||
}
|
||||
|
||||
private fun showCoverOptionsDialog() {
|
||||
@@ -303,7 +278,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?, meta: RaisedSearchMetadata?) {
|
||||
private fun setMangaInfo() {
|
||||
// Update full title TextView.
|
||||
binding.mangaFullTitle.text = if (manga.title.isBlank()) {
|
||||
view.context.getString(R.string.unknown)
|
||||
@@ -326,40 +301,36 @@ 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") }
|
||||
|
||||
// SY -->
|
||||
val isMergedSource = source?.id == MERGED_SOURCE_ID
|
||||
// SY <--
|
||||
val hasOneActiveLanguages = enabledLanguages.size == 1
|
||||
val isInEnabledLanguages = source.lang in enabledLanguages
|
||||
text = when {
|
||||
// SY -->
|
||||
val isMergedSource = source.id == MERGED_SOURCE_ID
|
||||
isMergedSource && hasOneActiveLanguages -> getMergedSourcesString(
|
||||
enabledLanguages,
|
||||
true
|
||||
)
|
||||
isMergedSource -> getMergedSourcesString(
|
||||
enabledLanguages,
|
||||
false
|
||||
)
|
||||
// SY <--
|
||||
val hasOneActiveLanguages = enabledLanguages.size == 1
|
||||
val isInEnabledLanguages = source.lang in enabledLanguages
|
||||
text = when {
|
||||
// SY -->
|
||||
isMergedSource && hasOneActiveLanguages -> getMergedSourcesString(
|
||||
enabledLanguages,
|
||||
true
|
||||
)
|
||||
isMergedSource -> getMergedSourcesString(
|
||||
enabledLanguages,
|
||||
false
|
||||
)
|
||||
// SY <--
|
||||
// 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
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,102 +357,35 @@ 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
|
||||
)
|
||||
// SY -->
|
||||
// if (source?.getMainSource<NamespaceSource>() != null) {
|
||||
setChipsWithNamespace(
|
||||
manga.getGenres(),
|
||||
meta
|
||||
)
|
||||
// binding.mangaGenresTagsFullChips.isVisible = false
|
||||
/*} else {
|
||||
binding.mangaGenresTagsFullChips.setChips(
|
||||
manga.getGenres(),
|
||||
controller::performGenreSearch
|
||||
)
|
||||
binding.genreGroups.isVisible = false
|
||||
}*/
|
||||
// SY <--
|
||||
} else {
|
||||
binding.mangaGenresTagsCompact.isVisible = false
|
||||
binding.mangaGenresTagsCompactChips.isVisible = false
|
||||
// binding.mangaGenresTagsFullChips.isVisible = false
|
||||
// SY -->
|
||||
binding.genreGroups.isVisible = false
|
||||
// SY <--
|
||||
}
|
||||
|
||||
// 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
|
||||
// SY --> binding.mangaGenresTagsFullChips.isVisible = !isCurrentlyExpanded
|
||||
binding.genreGroups.isVisible = !isCurrentlyExpanded
|
||||
binding.mangaSummarySection.isVisible = !manga.description.isNullOrBlank() || !manga.genre.isNullOrBlank()
|
||||
binding.mangaSummarySection.description = manga.description
|
||||
// SY -->
|
||||
binding.mangaSummarySection.setTags(
|
||||
manga.getGenres(),
|
||||
meta,
|
||||
controller::performGenreSearch,
|
||||
controller::performGlobalSearch,
|
||||
source
|
||||
)
|
||||
// SY <--
|
||||
|
||||
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)
|
||||
// SY -->
|
||||
description == "meta" -> ""
|
||||
// SY <--
|
||||
isCurrentlyExpanded ->
|
||||
description
|
||||
.replace(Regex(" +\$", setOf(RegexOption.MULTILINE)), "")
|
||||
.replace(Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)), "\n")
|
||||
else -> description
|
||||
/**
|
||||
* Update favorite button with correct drawable and text.
|
||||
*
|
||||
* @param isFavorite determines if manga is favorite or not.
|
||||
*/
|
||||
private fun setFavoriteButtonState(isFavorite: Boolean) {
|
||||
// Set the Favorite drawable to the correct one.
|
||||
// Border drawable if false, filled drawable if true.
|
||||
val (iconResource, stringResource) = when (isFavorite) {
|
||||
true -> R.drawable.ic_favorite_24dp to R.string.in_library
|
||||
false -> R.drawable.ic_favorite_border_24dp to R.string.add_to_library
|
||||
}
|
||||
binding.btnFavorite.apply {
|
||||
setIconResource(iconResource)
|
||||
text = context.getString(stringResource)
|
||||
isActivated = isFavorite
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,66 +406,6 @@ class MangaInfoHeaderAdapter(
|
||||
}.distinct().joinToString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setChipsWithNamespace(genre: List<String>?, meta: RaisedSearchMetadata?) {
|
||||
val namespaceTags = when {
|
||||
meta != null -> {
|
||||
meta.tags
|
||||
.filterNot { it.type == RaisedSearchMetadata.TAG_TYPE_VIRTUAL }
|
||||
.groupBy { it.namespace }
|
||||
.map { (namespace, tags) ->
|
||||
NamespaceTagsItem(
|
||||
namespace,
|
||||
tags.map {
|
||||
it.name to it.type
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
genre != null -> {
|
||||
if (genre.all { it.contains(':') }) {
|
||||
genre
|
||||
.map { tag ->
|
||||
val index = tag.indexOf(':')
|
||||
tag.substring(0, index).trim() to tag.substring(index + 1).trim()
|
||||
}
|
||||
.groupBy {
|
||||
it.first
|
||||
}
|
||||
.mapValues { group ->
|
||||
group.value.map { it.second to 0 }
|
||||
}
|
||||
.map { (namespace, tags) ->
|
||||
NamespaceTagsItem(namespace, tags)
|
||||
}
|
||||
} else {
|
||||
listOf(NamespaceTagsItem(null, genre.map { it to null }))
|
||||
}
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
mangaTagsInfoAdapter?.updateDataSet(namespaceTags)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Update favorite button with correct drawable and text.
|
||||
*
|
||||
* @param isFavorite determines if manga is favorite or not.
|
||||
*/
|
||||
private fun setFavoriteButtonState(isFavorite: Boolean) {
|
||||
// Set the Favorite drawable to the correct one.
|
||||
// Border drawable if false, filled drawable if true.
|
||||
val (iconResource, stringResource) = when (isFavorite) {
|
||||
true -> R.drawable.ic_favorite_24dp to R.string.in_library
|
||||
false -> R.drawable.ic_favorite_border_24dp to R.string.add_to_library
|
||||
}
|
||||
binding.btnFavorite.apply {
|
||||
setIconResource(iconResource)
|
||||
text = context.getString(stringResource)
|
||||
isActivated = isFavorite
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.info
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
|
||||
class NamespaceTagsAdapter(val controller: MangaController, val source: Source) :
|
||||
FlexibleAdapter<NamespaceTagsItem>(null, controller, true)
|
||||
@@ -5,6 +5,7 @@ import android.widget.LinearLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.google.android.material.chip.Chip
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.databinding.MangaInfoGenreGroupingBinding
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
@@ -12,7 +13,7 @@ import exh.util.makeSearchChip
|
||||
|
||||
class NamespaceTagsHolder(
|
||||
view: View,
|
||||
val adapter: NamespaceTagsAdapter
|
||||
adapter: FlexibleAdapter<*>
|
||||
) : FlexibleViewHolder(view, adapter) {
|
||||
val binding = MangaInfoGenreGroupingBinding.bind(view)
|
||||
|
||||
@@ -40,9 +41,9 @@ class NamespaceTagsHolder(
|
||||
item.tags.map { (tag, type) ->
|
||||
binding.root.context.makeSearchChip(
|
||||
tag,
|
||||
adapter.controller::performSearch,
|
||||
adapter.controller::performGlobalSearch,
|
||||
adapter.source.id,
|
||||
item.onClick,
|
||||
item.onLongClick,
|
||||
item.source.id,
|
||||
namespace,
|
||||
type
|
||||
)
|
||||
|
||||
@@ -6,8 +6,15 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.source.Source
|
||||
|
||||
class NamespaceTagsItem(val namespace: String?, val tags: List<Pair<String, Int?>>) :
|
||||
class NamespaceTagsItem(
|
||||
val namespace: String?,
|
||||
val tags: List<Pair<String, Int?>>,
|
||||
val onClick: (item: String) -> Unit,
|
||||
val onLongClick: (item: String) -> Unit,
|
||||
val source: Source
|
||||
) :
|
||||
AbstractFlexibleItem<NamespaceTagsHolder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
@@ -15,7 +22,7 @@ class NamespaceTagsItem(val namespace: String?, val tags: List<Pair<String, Int?
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): NamespaceTagsHolder {
|
||||
return NamespaceTagsHolder(view, adapter as NamespaceTagsAdapter)
|
||||
return NamespaceTagsHolder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
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 androidx.recyclerview.widget.LinearLayoutManager
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.MangaSummaryBinding
|
||||
import eu.kanade.tachiyomi.ui.manga.info.NamespaceTagsItem
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.util.setChipsExtended
|
||||
import tachiyomi.source.Source
|
||||
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
|
||||
|
||||
// SY -->
|
||||
private var mangaTagsInfoAdapter = FlexibleAdapter<NamespaceTagsItem>(emptyList())
|
||||
// SY <--
|
||||
|
||||
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)
|
||||
// SY -->
|
||||
} else if (value == "meta") {
|
||||
""
|
||||
// SY <--
|
||||
} else {
|
||||
value
|
||||
}
|
||||
binding.descriptionText.text = field
|
||||
recalculateHeights = true
|
||||
doOnNextLayout {
|
||||
updateExpandState()
|
||||
}
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun setTags(
|
||||
items: List<String>?,
|
||||
meta: RaisedSearchMetadata?,
|
||||
onClick: (item: String) -> Unit,
|
||||
onLongClick: (item: String) -> Unit,
|
||||
source: Source
|
||||
) {
|
||||
binding.tagChipsShrunk.setChipsExtended(
|
||||
items,
|
||||
onClick,
|
||||
onLongClick,
|
||||
source.id
|
||||
)
|
||||
// binding.tagChipsExpanded.setChips(items, onClick)
|
||||
setChipsWithNamespace(
|
||||
items,
|
||||
meta,
|
||||
onClick,
|
||||
onLongClick,
|
||||
source
|
||||
)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun setChipsWithNamespace(
|
||||
genre: List<String>?,
|
||||
meta: RaisedSearchMetadata?,
|
||||
onClick: (item: String) -> Unit,
|
||||
onLongClick: (item: String) -> Unit,
|
||||
source: Source
|
||||
) {
|
||||
val namespaceTags = when {
|
||||
meta != null -> {
|
||||
meta.tags
|
||||
.filterNot { it.type == RaisedSearchMetadata.TAG_TYPE_VIRTUAL }
|
||||
.groupBy { it.namespace }
|
||||
.map { (namespace, tags) ->
|
||||
NamespaceTagsItem(
|
||||
namespace,
|
||||
tags.map {
|
||||
it.name to it.type
|
||||
},
|
||||
onClick,
|
||||
onLongClick,
|
||||
source
|
||||
)
|
||||
}
|
||||
}
|
||||
genre != null -> {
|
||||
if (genre.all { it.contains(':') }) {
|
||||
genre
|
||||
.map { tag ->
|
||||
val index = tag.indexOf(':')
|
||||
tag.substring(0, index).trim() to tag.substring(index + 1).trim()
|
||||
}
|
||||
.groupBy {
|
||||
it.first
|
||||
}
|
||||
.mapValues { group ->
|
||||
group.value.map { it.second to 0 }
|
||||
}
|
||||
.map { (namespace, tags) ->
|
||||
NamespaceTagsItem(
|
||||
namespace,
|
||||
tags,
|
||||
onClick,
|
||||
onLongClick,
|
||||
source
|
||||
)
|
||||
}
|
||||
} else {
|
||||
listOf(
|
||||
NamespaceTagsItem(
|
||||
null,
|
||||
genre.map { it to null },
|
||||
onClick,
|
||||
onLongClick,
|
||||
source
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
mangaTagsInfoAdapter.updateDataSet(namespaceTags)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
binding.tagChipsExpanded.layoutManager = LinearLayoutManager(binding.root.context)
|
||||
binding.tagChipsExpanded.adapter = mangaTagsInfoAdapter
|
||||
|
||||
mangaTagsInfoAdapter.mItemClickListener = FlexibleAdapter.OnItemClickListener { _, _ ->
|
||||
expanded = !expanded
|
||||
false
|
||||
}
|
||||
// SY <--
|
||||
|
||||
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