From 656826c41b6e8eaf86720599fc594c8d4c4655d3 Mon Sep 17 00:00:00 2001
From: renhaoting <370797079@qq.com>
Date: Thu, 30 Oct 2025 18:00:41 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AD=97=E4=BD=93=E9=80=89=E6=8B=A9=20?=
=?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89view?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../visualnovel/ui/chat/ui/LevelSeekbar.kt | 302 ++++++++++++++++++
.../com/remax/visualnovel/utils/ResUtil.kt | 138 ++++++++
.../main/res/layout/layout_chat_menu_view.xml | 16 +-
.../main/res/layout/layout_font_set_view.xml | 105 +++---
VisualNovel/app/src/main/res/values/attrs.xml | 18 +-
.../app/src/main/res/values/colors.xml | 6 +
6 files changed, 530 insertions(+), 55 deletions(-)
create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/LevelSeekbar.kt
create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/utils/ResUtil.kt
diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/LevelSeekbar.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/LevelSeekbar.kt
new file mode 100644
index 0000000..176c750
--- /dev/null
+++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/LevelSeekbar.kt
@@ -0,0 +1,302 @@
+package com.remax.visualnovel.ui.chat.ui
+
+
+import android.content.Context
+import android.graphics.*
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import com.remax.visualnovel.R
+import com.remax.visualnovel.utils.ResUtil
+import com.remax.visualnovel.utils.spannablex.utils.dp
+
+
+class LevelSeekBar @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+
+ private var totalLevels = 5
+ private var currentLevel = 2
+
+ // 尺寸
+ private var trackHeight = ResUtil.getPixelSize(R.dimen.dp_5).toFloat()
+ private var trackEndRadius = ResUtil.getPixelSize(R.dimen.dp_8).toFloat()
+ private var thumbRadius = ResUtil.getPixelSize(R.dimen.dp_9).toFloat()
+ private var nodeRadius = ResUtil.getPixelSize(R.dimen.dp_3).toFloat()
+ private val nodeWidth = ResUtil.getPixelSize(R.dimen.dp_5)
+ private val nodeHeight= ResUtil.getPixelSize(R.dimen.dp_11)
+
+
+ // 颜色
+ private var trackColor = ResUtil.getColor(R.color.seekbar_color)
+ private var thumbColor = ResUtil.getColor(R.color.white)
+ private var activeTrackColor = trackColor
+ private var thumbBorderColor = trackColor
+ private var nodeColor = trackColor
+ private var activeNodeColor = trackColor
+
+
+ // 画笔
+ private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private val thumbPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private val thumbBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private val nodePaint = Paint(Paint.ANTI_ALIAS_FLAG)
+
+ // 监听器
+ private var onLevelChangeListener: OnLevelChangeListener? = null
+ private var isDragging = false
+
+
+ // 触摸相关
+ private var lastTouchX = 0f
+
+
+
+ interface OnLevelChangeListener {
+ fun onLevelChanged(seekBar: LevelSeekBar, level: Int, fromUser: Boolean)
+ fun onStartTrackingTouch(seekBar: LevelSeekBar)
+ fun onStopTrackingTouch(seekBar: LevelSeekBar)
+ }
+
+ init {
+ setupAttributes(attrs)
+ setupPaints()
+ }
+
+ private fun setupAttributes(attrs: AttributeSet?) {
+ attrs?.let {
+ val typedArray = context.obtainStyledAttributes(it, R.styleable.CustomLevelSeekBar)
+
+ totalLevels = typedArray.getInt(R.styleable.CustomLevelSeekBar_totalLevels, totalLevels)
+ currentLevel = typedArray.getInt(R.styleable.CustomLevelSeekBar_currentLevel, currentLevel)
+ .coerceIn(0, totalLevels - 1)
+
+ trackColor = typedArray.getColor(R.styleable.CustomLevelSeekBar_trackColor, trackColor)
+ activeTrackColor = typedArray.getColor(R.styleable.CustomLevelSeekBar_activeTrackColor, activeTrackColor)
+ thumbColor = typedArray.getColor(R.styleable.CustomLevelSeekBar_thumbColor, thumbColor)
+ thumbBorderColor = typedArray.getColor(R.styleable.CustomLevelSeekBar_thumbBorderColor, thumbBorderColor)
+ nodeColor = typedArray.getColor(R.styleable.CustomLevelSeekBar_nodeColor, nodeColor)
+ activeNodeColor = typedArray.getColor(R.styleable.CustomLevelSeekBar_activeNodeColor, activeNodeColor)
+
+ trackHeight = typedArray.getDimension(R.styleable.CustomLevelSeekBar_trackHeight, trackHeight)
+ trackEndRadius = typedArray.getDimension(R.styleable.CustomLevelSeekBar_trackEndRadius, trackEndRadius)
+ thumbRadius = typedArray.getDimension(R.styleable.CustomLevelSeekBar_thumbRadius, thumbRadius)
+ nodeRadius = typedArray.getDimension(R.styleable.CustomLevelSeekBar_nodeRadius, nodeRadius)
+
+ typedArray.recycle()
+ }
+ }
+
+ private fun setupPaints() {
+ trackPaint.style = Paint.Style.FILL
+
+ thumbBorderPaint.style = Paint.Style.STROKE
+ //thumbBorderPaint.strokeWidth = 1F.dp.toFloat()
+ thumbBorderPaint.color = thumbBorderColor
+
+ thumbPaint.style = Paint.Style.FILL
+ thumbPaint.color = thumbColor
+
+ nodePaint.style = Paint.Style.FILL
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ drawTrack(canvas)
+ drawNodes(canvas)
+ drawThumb(canvas)
+ }
+
+ private fun drawTrack(canvas: Canvas) {
+ val centerY = height / 2f
+ val trackTop = centerY - trackHeight / 2
+ val trackBottom = centerY + trackHeight / 2
+
+ trackPaint.color = trackColor
+ val trackRect = RectF(0f + nodeWidth/2, trackTop, width.toFloat(), trackBottom - nodeWidth/2)
+ canvas.drawRoundRect(trackRect, trackEndRadius, trackEndRadius, trackPaint)
+ }
+
+ private fun drawNodes(canvas: Canvas) {
+ if (totalLevels <= 1) return
+
+ val centerY = height / 2f
+
+ for (i in 0 until totalLevels) {
+ val x = calculatePositionForLevel(i)
+
+ nodePaint.color = if (i <= currentLevel) activeNodeColor else nodeColor
+ //canvas.drawCircle(x, centerY, nodeRadius, nodePaint)
+ val trackRect = RectF(x - thumbRadius/2, centerY - nodeHeight/2 + 6, x + thumbRadius/2, centerY + nodeHeight/2)
+ canvas.drawRoundRect(trackRect, trackEndRadius, trackEndRadius, nodePaint)
+ }
+ }
+
+ private fun drawThumb(canvas: Canvas) {
+ if (totalLevels <= 1) return
+
+ val centerY = height / 2f
+ val thumbX = calculatePositionForLevel(currentLevel)
+
+ canvas.drawCircle(thumbX, centerY, thumbRadius, thumbBorderPaint)
+ canvas.drawCircle(thumbX, centerY, thumbRadius - 1F.dp.toFloat(), thumbPaint)
+ }
+
+ private fun calculatePositionForLevel(level: Int): Float {
+ if (totalLevels <= 1) return width / 2f
+
+ val availableWidth = width - 2 * thumbRadius
+ return thumbRadius + (availableWidth * level.toFloat() / (totalLevels - 1))
+ }
+
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ if (isPointInThumb(event.x, event.y) || isPointInTrack(event.x, event.y)) {
+ isDragging = true
+ lastTouchX = event.x
+ onLevelChangeListener?.onStartTrackingTouch(this)
+ handleTouch(event.x)
+ parent?.requestDisallowInterceptTouchEvent(true)
+ return true
+ }
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ if (isDragging) {
+ lastTouchX = event.x
+ handleTouch(event.x)
+ return true
+ }
+ }
+
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ if (isDragging) {
+ isDragging = false
+ snapToNearestLevel(lastTouchX)
+ onLevelChangeListener?.onStopTrackingTouch(this)
+ parent?.requestDisallowInterceptTouchEvent(false)
+ return true
+ }
+ }
+ }
+ return super.onTouchEvent(event)
+ }
+
+ private fun isPointInThumb(x: Float, y: Float): Boolean {
+ val thumbX = calculatePositionForLevel(currentLevel)
+ val centerY = height / 2f
+ val distance = Math.sqrt(
+ (x - thumbX) * (x - thumbX) + (y - centerY) * (y - centerY).toDouble()
+ )
+ return distance <= thumbRadius
+ }
+
+ private fun isPointInTrack(x: Float, y: Float): Boolean {
+ val centerY = height / 2f
+ val trackTop = centerY - trackHeight / 2 - thumbRadius // 扩大触摸区域
+ val trackBottom = centerY + trackHeight / 2 + thumbRadius
+ return x in 0f..width.toFloat() && y in trackTop..trackBottom
+ }
+
+ private fun handleTouch(x: Float) {
+ if (totalLevels <= 1) return
+
+ val newLevel = calculateLevelForPosition(x)
+ if (newLevel != currentLevel) {
+ currentLevel = newLevel
+ invalidate()
+ onLevelChangeListener?.onLevelChanged(this, currentLevel, true)
+ }
+ }
+
+ private fun snapToNearestLevel(x: Float) {
+ if (totalLevels <= 1) return
+
+ val exactLevel = calculateExactLevelForPosition(x)
+ val newLevel = (exactLevel + 0.5f).toInt().coerceIn(0, totalLevels - 1)
+
+ if (newLevel != currentLevel) {
+ currentLevel = newLevel
+ invalidate()
+ onLevelChangeListener?.onLevelChanged(this, currentLevel, true)
+ }
+ }
+
+ private fun calculateLevelForPosition(x: Float): Int {
+ if (totalLevels <= 1) return 0
+
+ val availableWidth = width - 2 * thumbRadius
+ val progress = ((x - thumbRadius) / availableWidth).coerceIn(0f, 1f)
+ return (progress * (totalLevels - 1)).toInt().coerceIn(0, totalLevels - 1)
+ }
+
+ private fun calculateExactLevelForPosition(x: Float): Float {
+ if (totalLevels <= 1) return 0f
+
+ val availableWidth = width - 2 * thumbRadius
+ val progress = ((x - thumbRadius) / availableWidth).coerceIn(0f, 1f)
+ return progress * (totalLevels - 1)
+ }
+
+
+
+
+ //---------------------------- public 设置方法 ---------------------------------//
+ fun setLevel(level: Int, fromUser: Boolean = false) {
+ val newLevel = level.coerceIn(0, totalLevels - 1)
+ if (newLevel != currentLevel) {
+ currentLevel = newLevel
+ invalidate()
+ onLevelChangeListener?.onLevelChanged(this, currentLevel, fromUser)
+ }
+ }
+
+ fun getLevel(): Int = currentLevel
+
+ fun setTotalLevels(levels: Int) {
+ if (levels > 0 && levels != totalLevels) {
+ totalLevels = levels
+ currentLevel = currentLevel.coerceIn(0, totalLevels - 1)
+ invalidate()
+ }
+ }
+
+ fun getTotalLevels(): Int = totalLevels
+
+ fun setOnLevelChangeListener(listener: OnLevelChangeListener) {
+ this.onLevelChangeListener = listener
+ }
+
+
+ fun setOnLevelChangeListener(
+ onLevelChanged: (LevelSeekBar, Int, Boolean) -> Unit = { _, _, _ -> },
+ onStartTrackingTouch: (LevelSeekBar) -> Unit = {},
+ onStopTrackingTouch: (LevelSeekBar) -> Unit = {}
+ ) {
+ this.onLevelChangeListener = object : OnLevelChangeListener {
+ override fun onLevelChanged(seekBar: LevelSeekBar, level: Int, fromUser: Boolean) {
+ onLevelChanged(seekBar, level, fromUser)
+ }
+ override fun onStartTrackingTouch(seekBar: LevelSeekBar) {
+ onStartTrackingTouch(seekBar)
+ }
+ override fun onStopTrackingTouch(seekBar: LevelSeekBar) {
+ onStopTrackingTouch(seekBar)
+ }
+ }
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val minWidth = suggestedMinimumWidth + paddingLeft + paddingRight
+ val minHeight = (thumbRadius * 2 + paddingTop + paddingBottom).toInt()
+
+ val width = resolveSizeAndState(minWidth, widthMeasureSpec, 1)
+ val height = resolveSizeAndState(minHeight, heightMeasureSpec, 1)
+
+ setMeasuredDimension(width, height)
+ }
+
+}
\ No newline at end of file
diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/ResUtil.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/ResUtil.kt
new file mode 100644
index 0000000..4d24259
--- /dev/null
+++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/ResUtil.kt
@@ -0,0 +1,138 @@
+package com.remax.visualnovel.utils
+
+
+
+import android.graphics.Color
+import android.util.TypedValue
+import androidx.annotation.*
+import androidx.core.content.ContextCompat
+import com.remax.visualnovel.configs.NovelApplication
+
+object ResUtil {
+
+ private val appContext = NovelApplication.appContext()
+
+
+ // ==================== Dimen相关 ====================
+ /**
+ * 获取dp值对应的像素值
+ */
+ fun dp(dpValue: Float): Float {
+ return dpValue * appContext.resources.displayMetrics.density
+ }
+
+ fun dp(dpValue: Int): Float {
+ return dp(dpValue.toFloat())
+ }
+
+ /**
+ * 获取dp值对应的像素值(取整)
+ */
+ fun dpToPx(dpValue: Float): Int {
+ return (dp(dpValue) + 0.5f).toInt()
+ }
+
+ fun dpToPx(dpValue: Int): Int {
+ return dpToPx(dpValue.toFloat())
+ }
+
+ /**
+ * 获取sp值对应的像素值
+ */
+ fun sp(spValue: Float): Float {
+ return spValue * appContext.resources.displayMetrics.scaledDensity
+ }
+
+ fun sp(spValue: Int): Float {
+ return sp(spValue.toFloat())
+ }
+
+ /**
+ * 从dimen资源获取像素值
+ */
+ fun getPixelSize(@DimenRes dimenRes: Int): Int {
+ return appContext.resources.getDimensionPixelSize(dimenRes)
+ }
+
+ fun getDimension(@DimenRes dimenRes: Int): Float {
+ return appContext.resources.getDimension(dimenRes)
+ }
+
+ // ==================== 颜色相关 ====================
+
+ /**
+ * 从颜色资源获取颜色值
+ */
+ fun getColor(@ColorRes colorRes: Int): Int {
+ return ContextCompat.getColor(appContext, colorRes)
+ }
+
+ /**
+ * 从颜色资源获取颜色值(带透明度)
+ */
+ fun getColor(@ColorRes colorRes: Int, alpha: Float): Int {
+ val color = getColor(colorRes)
+ return applyAlphaToColor(color, alpha)
+ }
+
+ /**
+ * 解析颜色字符串
+ */
+ fun parseColor(colorString: String): Int {
+ return try {
+ Color.parseColor(colorString)
+ } catch (e: IllegalArgumentException) {
+ Color.BLACK // 默认颜色
+ }
+ }
+
+ /**
+ * 给颜色应用透明度
+ */
+ fun applyAlphaToColor(color: Int, alpha: Float): Int {
+ val alphaValue = (alpha.coerceIn(0f, 1f) * 255).toInt()
+ return color and 0x00FFFFFF or (alphaValue shl 24)
+ }
+
+ /**
+ * 获取主题颜色属性
+ */
+ fun getColorAttr(@AttrRes attrRes: Int): Int {
+ val typedValue = TypedValue()
+ appContext.theme.resolveAttribute(attrRes, typedValue, true)
+ return typedValue.data
+ }
+
+
+
+ //==================== 扩展函数 ====================
+ /**
+ * Float的扩展函数:转换为dp像素值
+ */
+ val Float.dp: Float
+ get() = ResUtil.dp(this)
+
+ val Float.dpToPx: Int
+ get() = ResUtil.dpToPx(this)
+
+ /**
+ * Int的扩展函数:转换为dp像素值
+ */
+ val Int.dp: Float
+ get() = ResUtil.dp(this)
+
+ val Int.dpToPx: Int
+ get() = ResUtil.dpToPx(this)
+
+ /**
+ * Float的扩展函数:转换为sp像素值
+ */
+ val Float.sp: Float
+ get() = ResUtil.sp(this)
+
+ /**
+ * 字符串的扩展函数:解析颜色
+ */
+ val String.colorInt: Int
+ get() = ResUtil.parseColor(this)
+}
\ No newline at end of file
diff --git a/VisualNovel/app/src/main/res/layout/layout_chat_menu_view.xml b/VisualNovel/app/src/main/res/layout/layout_chat_menu_view.xml
index 14019f9..022cd40 100644
--- a/VisualNovel/app/src/main/res/layout/layout_chat_menu_view.xml
+++ b/VisualNovel/app/src/main/res/layout/layout_chat_menu_view.xml
@@ -1,5 +1,5 @@
-
+ app:advStrokeWidth="@dimen/dp_3"
+ app:advStrokeColor="@color/red_ff3b30"
+ app:advRadius="@dimen/dp_25"
+ >
@@ -324,4 +326,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/VisualNovel/app/src/main/res/layout/layout_font_set_view.xml b/VisualNovel/app/src/main/res/layout/layout_font_set_view.xml
index f1d1893..1114bd9 100644
--- a/VisualNovel/app/src/main/res/layout/layout_font_set_view.xml
+++ b/VisualNovel/app/src/main/res/layout/layout_font_set_view.xml
@@ -1,75 +1,96 @@
-
-
-
-
-
-
-
-
-
+ android:layout_height="wrap_content" >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/VisualNovel/app/src/main/res/values/attrs.xml b/VisualNovel/app/src/main/res/values/attrs.xml
index ecf7673..b6d23d3 100644
--- a/VisualNovel/app/src/main/res/values/attrs.xml
+++ b/VisualNovel/app/src/main/res/values/attrs.xml
@@ -1530,23 +1530,29 @@
-
+
+
-
+
+
-
-
-
+
+
+
+
+
+
+
-
+
diff --git a/VisualNovel/app/src/main/res/values/colors.xml b/VisualNovel/app/src/main/res/values/colors.xml
index 0ac51fa..822ac50 100644
--- a/VisualNovel/app/src/main/res/values/colors.xml
+++ b/VisualNovel/app/src/main/res/values/colors.xml
@@ -194,6 +194,8 @@
#fff6f6f6
#ff282828
+ #ffff3b30
+
#F6F6F6
#FFFFFF
@@ -211,6 +213,10 @@
#ffc7dbff
#66eaeeff
+
+ #ffa4a8b7
+
+