diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/ChatSettingView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/ChatSettingView.kt index e5c7b25..4453d83 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/ChatSettingView.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/ChatSettingView.kt @@ -27,8 +27,7 @@ import java.util.Date class ChatSettingView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : android.widget.LinearLayout(context, attrs, defStyleAttr) { + defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) { private var mBinding = LayoutChatMenuViewBinding.inflate(LayoutInflater.from(context), this, true) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/history/HistoryFragment.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/history/HistoryFragment.kt index f4bb89b..b555def 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/history/HistoryFragment.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/history/HistoryFragment.kt @@ -5,9 +5,10 @@ import androidx.fragment.app.viewModels import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.dylanc.loadingstateview.BgColorType -import com.gyf.immersionbar.ImmersionBar +import com.remax.visualnovel.R import com.remax.visualnovel.app.base.BaseBindingFragment import com.remax.visualnovel.databinding.FragmentMainHistoryBinding +import com.remax.visualnovel.ui.main.history.customui.beans.TabItem import com.remax.visualnovel.utils.Routers import com.remax.visualnovel.utils.StatusBarUtil3 import dagger.hilt.android.AndroidEntryPoint @@ -33,6 +34,27 @@ class HistoryFragment : BaseBindingFragment() { with (binding.toolbar) { setPadding(paddingLeft, paddingTop + StatusBarUtil3.getStatusBarHeight(context), paddingRight, paddingBottom) } + + initTabLayout() + } + + private fun initTabLayout() { + with (binding.tabLayout) { + val tabItems = listOf( + TabItem("Manga", R.mipmap.history_tab_manga), + TabItem("Motion Comics", R.mipmap.history_tab_comics), + TabItem("Character", R.mipmap.history_tab_books) + ) + setTabItems(tabItems) + setOnTabSelectedListener { position -> + switchFragment(position) + } + setSelectedTab(0) + } + } + + private fun switchFragment(fragmentIndex: Int) { + } companion object { diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/history/customui/RoundAnimTabLayout.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/history/customui/RoundAnimTabLayout.kt new file mode 100644 index 0000000..175a263 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/history/customui/RoundAnimTabLayout.kt @@ -0,0 +1,239 @@ +package com.remax.visualnovel.ui.main.history.customui + +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import com.remax.visualnovel.R +import com.remax.visualnovel.ui.main.history.customui.beans.TabConfig +import com.remax.visualnovel.ui.main.history.customui.beans.TabItem + +class RoundAnimTabLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + private var tabConfig = TabConfig() + private val tabItems = mutableListOf() + private var selectedTabPosition = 0 + private var onTabSelectedListener: ((Int) -> Unit)? = null + + // 选中的Tab背景(渐变) + private val selectedTabGradient = GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + cornerRadius = tabConfig.cornerRadius + colors = intArrayOf( + 0xFF6366F1.toInt(), // 紫色 + 0xFF3B82F6.toInt() // 蓝色 + ) + orientation = GradientDrawable.Orientation.LEFT_RIGHT + } + + init { + orientation = HORIZONTAL + setWillNotDraw(false) + initAttributes(attrs) + setupLayout() + } + + private fun initAttributes(attrs: AttributeSet?) { + attrs?.let { + val typedArray = context.obtainStyledAttributes(it, R.styleable.CustomTabLayout) + + tabConfig = tabConfig.copy( + normalTextColor = typedArray.getColor( + R.styleable.CustomTabLayout_textColorNormal, Color.GRAY + ), + selectedTextColor = typedArray.getColor( + R.styleable.CustomTabLayout_textColorSelected, Color.WHITE + ), + textSize = typedArray.getDimension( + R.styleable.CustomTabLayout_textSize, 14f + ), + iconSize = typedArray.getDimensionPixelSize( + R.styleable.CustomTabLayout_iconSize, 20 + ), + cornerRadius = typedArray.getDimension( + R.styleable.CustomTabLayout_tabCornerRadius, 20f + ), + animationDuration = typedArray.getInt( + R.styleable.CustomTabLayout_animDuration, 300 + ).toLong(), + tabSpacing = typedArray.getDimensionPixelSize( + R.styleable.CustomTabLayout_tabSpacing, 8 + ) + ) + + typedArray.recycle() + } + + // 更新渐变背景的圆角 + selectedTabGradient.cornerRadius = tabConfig.cornerRadius + } + + private fun setupLayout() { + background = createNormalBackground() + setPadding(tabConfig.tabSpacing, tabConfig.tabSpacing, + tabConfig.tabSpacing, tabConfig.tabSpacing) + } + + private fun createNormalBackground(): Drawable { + return GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + cornerRadius = tabConfig.cornerRadius + setColor(Color.WHITE) + setStroke(1, Color.LTGRAY) + } + } + + fun setTabItems(items: List) { + tabItems.clear() + tabItems.addAll(items) + removeAllViews() + createTabViews() + } + + private fun createTabViews() { + tabItems.forEachIndexed { index, tabItem -> + val tabView = createTabView(tabItem, index) + addView(tabView) + + // 设置权重,使tab均匀分布 + val params = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f) + params.marginEnd = if (index < tabItems.size - 1) tabConfig.tabSpacing else 0 + tabView.layoutParams = params + } + + // 设置初始选中状态 + setSelectedTab(selectedTabPosition, animate = false) + } + + private fun createTabView(tabItem: TabItem, position: Int): View { + return LayoutInflater.from(context).inflate(R.layout.item_smooth_tab, this, false).apply { + val iconView = findViewById(R.id.ivTabIcon) + val textView = findViewById(R.id.tvTabText) + + // 设置图标和文字 + iconView.setImageResource(tabItem.iconRes) + textView.text = tabItem.text + textView.setTextColor(tabConfig.normalTextColor) + textView.textSize = tabConfig.textSize / resources.displayMetrics.density + + // 设置图标大小 + iconView.layoutParams = iconView.layoutParams.apply { + width = tabConfig.iconSize + height = tabConfig.iconSize + } + + // 设置点击事件 + setOnClickListener { + setSelectedTab(position) + onTabSelectedListener?.invoke(position) + } + + // 设置标签以便后续查找 + tag = position + } + } + + fun setSelectedTab(position: Int, animate: Boolean = true) { + if (position !in 0 until tabItems.size) return + + val previousPosition = selectedTabPosition + selectedTabPosition = position + + // 更新所有tab的状态 + updateTabAppearance(previousPosition, position, animate) + } + + private fun updateTabAppearance(previousPos: Int, newPos: Int, animate: Boolean) { + // 更新之前的选中tab + getTabViewAt(previousPos)?.let { previousView -> + updateTabViewAppearance(previousView, previousPos, isSelected = false, animate) + } + + // 更新新的选中tab + getTabViewAt(newPos)?.let { newView -> + updateTabViewAppearance(newView, newPos, isSelected = true, animate) + } + } + + private fun getTabViewAt(position: Int): View? { + return findViewWithTag(position) + } + + private fun updateTabViewAppearance(tabView: View, position: Int, + isSelected: Boolean, animate: Boolean) { + val iconView = tabView.findViewById(R.id.ivTabIcon) + val textView = tabView.findViewById(R.id.tvTabText) + + tabItems[position].isSelected = isSelected + + val targetBackground = if (isSelected) selectedTabGradient else null + val targetTextColor = if (isSelected) tabConfig.selectedTextColor else tabConfig.normalTextColor + val targetAlpha = if (isSelected) 1.0f else 0.6f + + if (animate) { + // 背景颜色动画 + val backgroundAnim = ValueAnimator.ofInt(0, 255).apply { + duration = tabConfig.animationDuration + addUpdateListener { animator -> + val alpha = animator.animatedValue as Int + if (isSelected) { + selectedTabGradient.alpha = alpha + tabView.background = selectedTabGradient + } else { + tabView.background = null + } + } + } + + // 文字颜色动画 + val textColorAnim = ValueAnimator.ofArgb( + textView.currentTextColor, targetTextColor + ).apply { + duration = tabConfig.animationDuration + addUpdateListener { animator -> + textView.setTextColor(animator.animatedValue as Int) + } + } + + // 透明度动画 + val alphaAnim = ValueAnimator.ofFloat(iconView.alpha, targetAlpha).apply { + duration = tabConfig.animationDuration + addUpdateListener { animator -> + val alpha = animator.animatedValue as Float + iconView.alpha = alpha + } + } + + // 同时执行所有动画 + AnimatorSet().apply { + playTogether(backgroundAnim, textColorAnim, alphaAnim) + start() + } + } else { + // 无动画直接设置 + tabView.background = targetBackground + textView.setTextColor(targetTextColor) + iconView.alpha = targetAlpha + } + } + + fun setOnTabSelectedListener(listener: (Int) -> Unit) { + this.onTabSelectedListener = listener + } + + fun getSelectedTabPosition(): Int = selectedTabPosition + + fun getTabCount(): Int = tabItems.size +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/history/customui/beans/TabItem.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/history/customui/beans/TabItem.kt new file mode 100644 index 0000000..cf6307b --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/history/customui/beans/TabItem.kt @@ -0,0 +1,26 @@ +package com.remax.visualnovel.ui.main.history.customui.beans + +import android.graphics.Color +import androidx.annotation.DrawableRes +import com.remax.visualnovel.R +import com.remax.visualnovel.utils.ResUtil +import com.remax.visualnovel.utils.ResUtil.dpToPx + + +data class TabItem( + val text: String, + @DrawableRes val iconRes: Int = 0, + var isSelected: Boolean = false +) + +data class TabConfig( + val normalTextColor: Int = ResUtil.getColor(R.color.gray6), + val selectedTextColor: Int = Color.WHITE, + val textSize: Float = 14f, + val iconSize: Int = 20.dpToPx, + val cornerRadius: Float = 20f.dpToPx.toFloat(), + val animationDuration: Long = 300, + val tabSpacing: Int = 8.dpToPx, + val tabPadding: Int = 16.dpToPx, + val selectedBackground: Int = Color.BLUE +) diff --git a/VisualNovel/app/src/main/res/layout/item_smooth_tab.xml b/VisualNovel/app/src/main/res/layout/item_smooth_tab.xml new file mode 100644 index 0000000..6634e7d --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/item_smooth_tab.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/mipmap-xxhdpi/history_tab_bg.webp b/VisualNovel/app/src/main/res/mipmap-xxhdpi/history_tab_bg.webp new file mode 100644 index 0000000..a34ab29 Binary files /dev/null and b/VisualNovel/app/src/main/res/mipmap-xxhdpi/history_tab_bg.webp differ diff --git a/VisualNovel/app/src/main/res/mipmap-xxhdpi/history_tab_books.webp b/VisualNovel/app/src/main/res/mipmap-xxhdpi/history_tab_books.webp new file mode 100644 index 0000000..dc0eb19 Binary files /dev/null and b/VisualNovel/app/src/main/res/mipmap-xxhdpi/history_tab_books.webp differ diff --git a/VisualNovel/app/src/main/res/mipmap-xxhdpi/history_tab_comics.webp b/VisualNovel/app/src/main/res/mipmap-xxhdpi/history_tab_comics.webp new file mode 100644 index 0000000..30e6d63 Binary files /dev/null and b/VisualNovel/app/src/main/res/mipmap-xxhdpi/history_tab_comics.webp differ diff --git a/VisualNovel/app/src/main/res/mipmap-xxhdpi/history_tab_manga.webp b/VisualNovel/app/src/main/res/mipmap-xxhdpi/history_tab_manga.webp new file mode 100644 index 0000000..f637012 Binary files /dev/null and b/VisualNovel/app/src/main/res/mipmap-xxhdpi/history_tab_manga.webp differ