feat(自定义控件):

新增自定义开关控件
This commit is contained in:
糕小菜 2024-10-23 19:50:29 +08:00
parent eb94db1fe3
commit 19f5b006a6
3 changed files with 606 additions and 0 deletions

View File

@ -0,0 +1,601 @@
package com.kaixed.kchat.view.widget
import android.animation.ValueAnimator
import android.animation.ValueAnimator.AnimatorUpdateListener
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.AttributeSet
import android.util.TypedValue
import android.view.GestureDetector
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.MotionEvent
import android.view.View
import android.view.animation.DecelerateInterpolator
import androidx.core.content.ContextCompat
import com.kaixed.kchat.R
/**
* @Author: kaixed
* @Date: 2024/10/23 19:29
*/
class ShSwitchView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) :
View(context, attrs, defStyle) {
private var innerContentAnimator: ValueAnimator? = null
private var knobExpandAnimator: ValueAnimator? = null
private var knobMoveAnimator: ValueAnimator? = null
private val gestureDetector: GestureDetector
private val gestureListener: SimpleOnGestureListener = object : SimpleOnGestureListener() {
override fun onDown(event: MotionEvent): Boolean {
if (!isEnabled) {
return false
}
preIsOn = isOn
innerContentAnimator!!.setFloatValues(innerContentRate, 0.0f)
innerContentAnimator!!.start()
knobExpandAnimator!!.setFloatValues(knobExpandRate, 1.0f)
knobExpandAnimator!!.start()
return true
}
override fun onShowPress(event: MotionEvent) {
}
override fun onSingleTapUp(event: MotionEvent): Boolean {
isOn = knobState
if (preIsOn == isOn) {
isOn = !isOn
knobState = !knobState
}
if (knobState) {
knobMoveAnimator!!.setFloatValues(knobMoveRate, 1.0f)
knobMoveAnimator!!.start()
innerContentAnimator!!.setFloatValues(innerContentRate, 0.0f)
innerContentAnimator!!.start()
} else {
knobMoveAnimator!!.setFloatValues(knobMoveRate, 0.0f)
knobMoveAnimator!!.start()
innerContentAnimator!!.setFloatValues(innerContentRate, 1.0f)
innerContentAnimator!!.start()
}
knobExpandAnimator!!.setFloatValues(knobExpandRate, 0.0f)
knobExpandAnimator!!.start()
if (this@ShSwitchView.onSwitchStateChangeListener != null && isOn != preIsOn) {
onSwitchStateChangeListener!!.onSwitchStateChange(isOn)
}
return true
}
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (e2.x > centerX) {
if (!knobState) {
knobState = !knobState
knobMoveAnimator!!.setFloatValues(knobMoveRate, 1.0f)
knobMoveAnimator!!.start()
innerContentAnimator!!.setFloatValues(innerContentRate, 0.0f)
innerContentAnimator!!.start()
}
} else {
if (knobState) {
knobState = !knobState
knobMoveAnimator!!.setFloatValues(knobMoveRate, 0.0f)
knobMoveAnimator!!.start()
}
}
return true
}
}
private var width = 0
private var height = 0
private var centerX = 0
private var centerY = 0
private var cornerRadius = 0f
private val shadowSpace: Int
private val outerStrokeWidth: Int
private val shadowDrawable: Drawable
private val knobBound: RectF
private var knobMaxExpandWidth = 0f
private var intrinsicKnobWidth = 0f
private var knobExpandRate = 0f
private var knobMoveRate = 0f
private var knobState = false
private var isOn = false
private var preIsOn = false
private val innerContentBound: RectF
private var innerContentRate = 1.0f
private var intrinsicInnerWidth = 0f
private var intrinsicInnerHeight = 0f
private var tintColor: Int
private var tempTintColor: Int
private var colorStep = backgroundColor
private val paint: Paint
private val ovalForPath: RectF
private val roundRectPath: Path
private val tempForRoundRect: RectF
private var dirtyAnimation = false
private var isAttachedToWindow = false
interface OnSwitchStateChangeListener {
fun onSwitchStateChange(isOn: Boolean)
}
var onSwitchStateChangeListener: OnSwitchStateChangeListener? = null
init {
val ta = context.obtainStyledAttributes(attrs, R.styleable.ShSwitchView)
tintColor = ta.getColor(R.styleable.ShSwitchView_tintColor, -0x6316b7)
tempTintColor = tintColor
val defaultOuterStrokeWidth = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
1.5f,
context.resources.displayMetrics
).toInt()
val defaultShadowSpace = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
5f,
context.resources.displayMetrics
).toInt()
outerStrokeWidth = ta.getDimensionPixelOffset(
R.styleable.ShSwitchView_outerStrokeWidth,
defaultOuterStrokeWidth
)
shadowSpace =
ta.getDimensionPixelOffset(R.styleable.ShSwitchView_shadowSpace, defaultShadowSpace)
ta.recycle()
knobBound = RectF()
innerContentBound = RectF()
ovalForPath = RectF()
tempForRoundRect = RectF()
paint = Paint(Paint.ANTI_ALIAS_FLAG)
roundRectPath = Path()
gestureDetector = GestureDetector(context, gestureListener)
gestureDetector.setIsLongpressEnabled(false)
setLayerType(LAYER_TYPE_SOFTWARE, null)
initAnimators()
shadowDrawable = ContextCompat.getDrawable(context, R.drawable.shadow)!!
}
private fun initAnimators() {
innerContentAnimator = ValueAnimator.ofFloat(innerContentRate, 1.0f).apply {
duration = commonDuration
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
(animation as? ValueAnimator)?.let {
setInnerContentRate(it.animatedValue as Float)
}
}
}
knobExpandAnimator = ValueAnimator.ofFloat(knobExpandRate, 1.0f).apply {
duration = commonDuration
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
(animation as? ValueAnimator)?.let {
setKnobExpandRate(it.animatedValue as Float)
}
}
}
knobMoveAnimator = ValueAnimator.ofFloat(knobMoveRate, 1.0f).apply {
duration = commonDuration
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
(animation as? ValueAnimator)?.let {
setKnobMoveRate(it.animatedValue as Float)
}
}
}
}
fun setInnerContentRate(rate: Float) {
this.innerContentRate = rate
invalidate()
}
fun getInnerContentRate(): Float {
return this.innerContentRate
}
fun setKnobExpandRate(rate: Float) {
this.knobExpandRate = rate
invalidate()
}
fun getKnobExpandRate(): Float {
return this.knobExpandRate
}
fun setKnobMoveRate(rate: Float) {
this.knobMoveRate = rate
invalidate()
}
fun getKnobMoveRate(): Float {
return knobMoveRate
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
isAttachedToWindow = true
if (dirtyAnimation) {
knobState = this.isOn
if (knobState) {
knobMoveAnimator!!.setFloatValues(knobMoveRate, 1.0f)
knobMoveAnimator!!.start()
innerContentAnimator!!.setFloatValues(innerContentRate, 0.0f)
innerContentAnimator!!.start()
} else {
knobMoveAnimator!!.setFloatValues(knobMoveRate, 0.0f)
knobMoveAnimator!!.start()
innerContentAnimator!!.setFloatValues(innerContentRate, 1.0f)
innerContentAnimator!!.start()
}
knobExpandAnimator!!.setFloatValues(knobExpandRate, 0.0f)
knobExpandAnimator!!.start()
if (this@ShSwitchView.onSwitchStateChangeListener != null && isOn != preIsOn) {
onSwitchStateChangeListener!!.onSwitchStateChange(isOn)
}
dirtyAnimation = false
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
isAttachedToWindow = false
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var widthMeasureSpec = widthMeasureSpec
var heightMeasureSpec = heightMeasureSpec
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
width = MeasureSpec.getSize(widthMeasureSpec)
height = MeasureSpec.getSize(heightMeasureSpec)
//make sure widget remain in a good appearance
if (height.toFloat() / width.toFloat() < 0.33333f) {
height = (width.toFloat() * 0.33333f).toInt()
widthMeasureSpec =
MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec))
heightMeasureSpec =
MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec))
super.setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
}
centerX = width / 2
centerY = height / 2
cornerRadius = (centerY - shadowSpace).toFloat()
innerContentBound.left = (outerStrokeWidth + shadowSpace).toFloat()
innerContentBound.top = (outerStrokeWidth + shadowSpace).toFloat()
innerContentBound.right = (width - outerStrokeWidth - shadowSpace).toFloat()
innerContentBound.bottom = (height - outerStrokeWidth - shadowSpace).toFloat()
intrinsicInnerWidth = innerContentBound.width()
intrinsicInnerHeight = innerContentBound.height()
knobBound.left = (outerStrokeWidth + shadowSpace).toFloat()
knobBound.top = (outerStrokeWidth + shadowSpace).toFloat()
knobBound.right = (height - outerStrokeWidth - shadowSpace).toFloat()
knobBound.bottom = (height - outerStrokeWidth - shadowSpace).toFloat()
intrinsicKnobWidth = knobBound.height()
knobMaxExpandWidth = width.toFloat() * 0.7f
if (knobMaxExpandWidth > knobBound.width() * 1.25f) {
knobMaxExpandWidth = knobBound.width() * 1.25f
}
}
fun isOn(): Boolean {
return this.isOn
}
fun setOn(on: Boolean) {
setOn(on, false)
}
fun setOn(on: Boolean, animated: Boolean) {
if (this.isOn == on) {
return
}
if (!isAttachedToWindow && animated) {
dirtyAnimation = true
this.isOn = on
return
}
this.isOn = on
knobState = this.isOn
if (!animated) {
if (on) {
setKnobMoveRate(1.0f)
setInnerContentRate(0.0f)
} else {
setKnobMoveRate(0.0f)
setInnerContentRate(1.0f)
}
setKnobExpandRate(0.0f)
} else {
if (knobState) {
knobMoveAnimator!!.setFloatValues(knobMoveRate, 1.0f)
knobMoveAnimator!!.start()
innerContentAnimator!!.setFloatValues(innerContentRate, 0.0f)
innerContentAnimator!!.start()
} else {
knobMoveAnimator!!.setFloatValues(knobMoveRate, 0.0f)
knobMoveAnimator!!.start()
innerContentAnimator!!.setFloatValues(innerContentRate, 1.0f)
innerContentAnimator!!.start()
}
knobExpandAnimator!!.setFloatValues(knobExpandRate, 0.0f)
knobExpandAnimator!!.start()
}
if (this@ShSwitchView.onSwitchStateChangeListener != null && isOn != preIsOn) {
onSwitchStateChangeListener!!.onSwitchStateChange(isOn)
}
}
fun setTintColor(tintColor: Int) {
this.tintColor = tintColor
tempTintColor = this.tintColor
}
fun getTintColor(): Int {
return this.tintColor
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!isEnabled) {
return false
}
when (event.action) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (!knobState) {
innerContentAnimator!!.setFloatValues(innerContentRate, 1.0f)
innerContentAnimator!!.start()
}
knobExpandAnimator!!.setFloatValues(knobExpandRate, 0.0f)
knobExpandAnimator!!.start()
isOn = knobState
if (this@ShSwitchView.onSwitchStateChangeListener != null && isOn != preIsOn) {
onSwitchStateChangeListener!!.onSwitchStateChange(isOn)
}
}
}
return gestureDetector.onTouchEvent(event)
}
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
if (enabled) {
this.tintColor = tempTintColor
} else {
this.tintColor = this.RGBColorTransform(0.5f, tempTintColor, -0x1)
}
}
public override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//innerContentCalculation begin
var w = intrinsicInnerWidth / 2.0f * innerContentRate
val h = intrinsicInnerHeight / 2.0f * innerContentRate
innerContentBound.left = centerX - w
innerContentBound.top = centerY - h
innerContentBound.right = centerX + w
innerContentBound.bottom = centerY + h
//innerContentCalculation end
//knobExpandCalculation begin
w = intrinsicKnobWidth + (knobMaxExpandWidth - intrinsicKnobWidth) * knobExpandRate
val left = knobBound.left + knobBound.width() / 2 > centerX
if (left) {
knobBound.left = knobBound.right - w
} else {
knobBound.right = knobBound.left + w
}
//knobExpandCalculation end
//knobMoveCalculation begin
val kw = knobBound.width()
w = (width - kw - ((shadowSpace + outerStrokeWidth) * 2)) * knobMoveRate
this.colorStep = RGBColorTransform(knobMoveRate, backgroundColor, tintColor)
knobBound.left = shadowSpace + outerStrokeWidth + w
knobBound.right = knobBound.left + kw
//knobMoveCalculation end
//background
paint.color = colorStep
paint.style = Paint.Style.FILL
drawRoundRect(
shadowSpace.toFloat(),
shadowSpace.toFloat(),
(width - shadowSpace).toFloat(),
(height - shadowSpace).toFloat(),
cornerRadius,
canvas,
paint
)
//innerContent
paint.color = foregroundColor
canvas.drawRoundRect(
innerContentBound,
innerContentBound.height() / 2,
innerContentBound.height() / 2,
paint
)
//knob
// shadowDrawable.setBounds((int) (knobBound.left - shadowSpace), (int) (knobBound.top - shadowSpace), (int) (knobBound.right + shadowSpace), (int) (knobBound.bottom + shadowSpace));
// shadowDrawable.draw(canvas);
paint.setShadowLayer(
2f,
0f,
(shadowSpace / 2).toFloat(),
if (isEnabled) 0x20000000 else 0x10000000
)
// paint.setColor(isEnabled() ? 0x20000000 : 0x10000000);
// drawRoundRect(knobBound.left, knobBound.top + shadowSpace / 2, knobBound.right, knobBound.bottom + shadowSpace / 2, cornerRadius - outerStrokeWidth, canvas, paint);
//
// paint.setColor(foregroundColor);
canvas.drawRoundRect(
knobBound,
cornerRadius - outerStrokeWidth,
cornerRadius - outerStrokeWidth,
paint
)
paint.setShadowLayer(0f, 0f, 0f, 0)
paint.color = backgroundColor
paint.style = Paint.Style.STROKE
paint.strokeWidth = 1f
canvas.drawRoundRect(
knobBound,
cornerRadius - outerStrokeWidth,
cornerRadius - outerStrokeWidth,
paint
)
}
private fun drawRoundRect(
left: Float,
top: Float,
right: Float,
bottom: Float,
radius: Float,
canvas: Canvas,
paint: Paint
) {
tempForRoundRect.left = left
tempForRoundRect.top = top
tempForRoundRect.right = right
tempForRoundRect.bottom = bottom
canvas.drawRoundRect(tempForRoundRect, radius, radius, paint)
}
//seperate RGB channels and calculate new value for each channel
//ignore alpha channel
private fun RGBColorTransform(progress: Float, fromColor: Int, toColor: Int): Int {
val or = (fromColor shr 16) and 0xFF
val og = (fromColor shr 8) and 0xFF
val ob = fromColor and 0xFF
val nr = (toColor shr 16) and 0xFF
val ng = (toColor shr 8) and 0xFF
val nb = toColor and 0xFF
val rGap = ((nr - or).toFloat() * progress).toInt()
val gGap = ((ng - og).toFloat() * progress).toInt()
val bGap = ((nb - ob).toFloat() * progress).toInt()
return -0x1000000 or ((or + rGap) shl 16) or ((og + gGap) shl 8) or (ob + bGap)
}
companion object {
private const val commonDuration = 300L
private const val intrinsicWidth = 0
private const val intrinsicHeight = 0
private const val backgroundColor = -0x333334
private const val foregroundColor = -0xa0a0b
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -24,6 +24,11 @@
<declare-styleable name="CustomTitleBar">
<attr name="titleName" format="string" />
<attr name="titleIcon" format="reference" />
</declare-styleable>
<declare-styleable name="ShSwitchView">
<attr name="tintColor" format="reference|color" />
<attr name="outerStrokeWidth" format="reference|dimension" />
<attr name="shadowSpace" format="reference|dimension" />
</declare-styleable>
</resources>