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 + +