parent
205504d223
commit
ad30cb55e3
@ -0,0 +1,338 @@ |
|||||||
|
package io.legado.app.ui.widget.checkbox |
||||||
|
|
||||||
|
import android.animation.ValueAnimator |
||||||
|
import android.content.Context |
||||||
|
import android.graphics.* |
||||||
|
import android.util.AttributeSet |
||||||
|
import android.view.View |
||||||
|
import android.view.animation.LinearInterpolator |
||||||
|
import android.widget.Checkable |
||||||
|
import io.legado.app.R |
||||||
|
import io.legado.app.lib.theme.ThemeStore |
||||||
|
import io.legado.app.utils.dp |
||||||
|
import io.legado.app.utils.getCompatColor |
||||||
|
import kotlin.math.min |
||||||
|
import kotlin.math.pow |
||||||
|
import kotlin.math.roundToInt |
||||||
|
import kotlin.math.sqrt |
||||||
|
|
||||||
|
class SmoothCheckBox @JvmOverloads constructor( |
||||||
|
context: Context, |
||||||
|
attrs: AttributeSet? = null |
||||||
|
) : View(context, attrs), Checkable { |
||||||
|
private var mPaint: Paint |
||||||
|
private var mTickPaint: Paint |
||||||
|
private var mFloorPaint: Paint |
||||||
|
private var mTickPoints: Array<Point> |
||||||
|
private var mCenterPoint: Point |
||||||
|
private var mTickPath: Path |
||||||
|
private var mLeftLineDistance = 0f |
||||||
|
private var mRightLineDistance = 0f |
||||||
|
private var mDrewDistance = 0f |
||||||
|
private var mScaleVal = 1.0f |
||||||
|
private var mFloorScale = 1.0f |
||||||
|
private var mWidth = 0 |
||||||
|
private var mAnimDuration = 0 |
||||||
|
private var mStrokeWidth = 0 |
||||||
|
private var mCheckedColor = 0 |
||||||
|
private var mUnCheckedColor = 0 |
||||||
|
private var mFloorColor = 0 |
||||||
|
private var mFloorUnCheckedColor = 0 |
||||||
|
private var mChecked = false |
||||||
|
private var mTickDrawing = false |
||||||
|
private var mListener: OnCheckedChangeListener? = null |
||||||
|
|
||||||
|
init { |
||||||
|
val ta = context.obtainStyledAttributes(attrs, R.styleable.SmoothCheckBox) |
||||||
|
var tickColor = ThemeStore.accentColor(context) |
||||||
|
mCheckedColor = context.getCompatColor(R.color.background_card) |
||||||
|
mUnCheckedColor = context.getCompatColor(R.color.background_menu) |
||||||
|
mFloorColor = context.getCompatColor(R.color.transparent30) |
||||||
|
tickColor = ta.getColor(R.styleable.SmoothCheckBox_color_tick, tickColor) |
||||||
|
mAnimDuration = ta.getInt(R.styleable.SmoothCheckBox_duration, DEF_ANIM_DURATION) |
||||||
|
mFloorColor = ta.getColor(R.styleable.SmoothCheckBox_color_unchecked_stroke, mFloorColor) |
||||||
|
mCheckedColor = ta.getColor(R.styleable.SmoothCheckBox_color_checked, mCheckedColor) |
||||||
|
mUnCheckedColor = ta.getColor(R.styleable.SmoothCheckBox_color_unchecked, mUnCheckedColor) |
||||||
|
mStrokeWidth = ta.getDimensionPixelSize(R.styleable.SmoothCheckBox_stroke_width, 0) |
||||||
|
ta.recycle() |
||||||
|
mFloorUnCheckedColor = mFloorColor |
||||||
|
mTickPaint = Paint(Paint.ANTI_ALIAS_FLAG) |
||||||
|
mTickPaint.style = Paint.Style.STROKE |
||||||
|
mTickPaint.strokeCap = Paint.Cap.ROUND |
||||||
|
mTickPaint.color = tickColor |
||||||
|
mFloorPaint = Paint(Paint.ANTI_ALIAS_FLAG) |
||||||
|
mFloorPaint.style = Paint.Style.FILL |
||||||
|
mFloorPaint.color = mFloorColor |
||||||
|
mPaint = Paint(Paint.ANTI_ALIAS_FLAG) |
||||||
|
mPaint.style = Paint.Style.FILL |
||||||
|
mPaint.color = mCheckedColor |
||||||
|
mTickPath = Path() |
||||||
|
mCenterPoint = Point() |
||||||
|
mTickPoints = arrayOf(Point(), Point(), Point()) |
||||||
|
setOnClickListener { |
||||||
|
toggle() |
||||||
|
mTickDrawing = false |
||||||
|
mDrewDistance = 0f |
||||||
|
if (isChecked) { |
||||||
|
startCheckedAnimation() |
||||||
|
} else { |
||||||
|
startUnCheckedAnimation() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun isChecked(): Boolean { |
||||||
|
return mChecked |
||||||
|
} |
||||||
|
|
||||||
|
override fun setChecked(checked: Boolean) { |
||||||
|
mChecked = checked |
||||||
|
reset() |
||||||
|
invalidate() |
||||||
|
if (mListener != null) { |
||||||
|
mListener!!.onCheckedChanged(this@SmoothCheckBox, mChecked) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun toggle() { |
||||||
|
this.isChecked = !isChecked |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* checked with animation |
||||||
|
* |
||||||
|
* @param checked checked |
||||||
|
* @param animate change with animation |
||||||
|
*/ |
||||||
|
fun setChecked(checked: Boolean, animate: Boolean) { |
||||||
|
if (animate) { |
||||||
|
mTickDrawing = false |
||||||
|
mChecked = checked |
||||||
|
mDrewDistance = 0f |
||||||
|
if (checked) { |
||||||
|
startCheckedAnimation() |
||||||
|
} else { |
||||||
|
startUnCheckedAnimation() |
||||||
|
} |
||||||
|
if (mListener != null) { |
||||||
|
mListener!!.onCheckedChanged(this@SmoothCheckBox, mChecked) |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.isChecked = checked |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun reset() { |
||||||
|
mTickDrawing = true |
||||||
|
mFloorScale = 1.0f |
||||||
|
mScaleVal = if (isChecked) 0f else 1.0f |
||||||
|
mFloorColor = if (isChecked) mCheckedColor else mFloorUnCheckedColor |
||||||
|
mDrewDistance = if (isChecked) mLeftLineDistance + mRightLineDistance else 0f |
||||||
|
} |
||||||
|
|
||||||
|
private fun measureSize(measureSpec: Int): Int { |
||||||
|
val defSize: Int = DEF_DRAW_SIZE.dp |
||||||
|
val specSize = MeasureSpec.getSize(measureSpec) |
||||||
|
val specMode = MeasureSpec.getMode(measureSpec) |
||||||
|
var result = 0 |
||||||
|
when (specMode) { |
||||||
|
MeasureSpec.UNSPECIFIED, MeasureSpec.AT_MOST -> result = min(defSize, specSize) |
||||||
|
MeasureSpec.EXACTLY -> result = specSize |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { |
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec) |
||||||
|
setMeasuredDimension(measureSize(widthMeasureSpec), measureSize(heightMeasureSpec)) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onLayout( |
||||||
|
changed: Boolean, |
||||||
|
left: Int, |
||||||
|
top: Int, |
||||||
|
right: Int, |
||||||
|
bottom: Int |
||||||
|
) { |
||||||
|
mWidth = measuredWidth |
||||||
|
mStrokeWidth = if (mStrokeWidth == 0) measuredWidth / 10 else mStrokeWidth |
||||||
|
mStrokeWidth = |
||||||
|
if (mStrokeWidth > measuredWidth / 5) measuredWidth / 5 else mStrokeWidth |
||||||
|
mStrokeWidth = if (mStrokeWidth < 3) 3 else mStrokeWidth |
||||||
|
mCenterPoint.x = mWidth / 2 |
||||||
|
mCenterPoint.y = measuredHeight / 2 |
||||||
|
mTickPoints[0].x = (measuredWidth.toFloat() / 30 * 7).roundToInt() |
||||||
|
mTickPoints[0].y = (measuredHeight.toFloat() / 30 * 14).roundToInt() |
||||||
|
mTickPoints[1].x = (measuredWidth.toFloat() / 30 * 13).roundToInt() |
||||||
|
mTickPoints[1].y = (measuredHeight.toFloat() / 30 * 20).roundToInt() |
||||||
|
mTickPoints[2].x = (measuredWidth.toFloat() / 30 * 22).roundToInt() |
||||||
|
mTickPoints[2].y = (measuredHeight.toFloat() / 30 * 10).roundToInt() |
||||||
|
mLeftLineDistance = sqrt( |
||||||
|
(mTickPoints[1].x - mTickPoints[0].x.toDouble()).pow(2.0) + |
||||||
|
(mTickPoints[1].y - mTickPoints[0].y.toDouble()).pow(2.0) |
||||||
|
).toFloat() |
||||||
|
mRightLineDistance = sqrt( |
||||||
|
(mTickPoints[2].x - mTickPoints[1].x.toDouble()).pow(2.0) + |
||||||
|
(mTickPoints[2].y - mTickPoints[1].y.toDouble()).pow(2.0) |
||||||
|
).toFloat() |
||||||
|
mTickPaint.strokeWidth = mStrokeWidth.toFloat() |
||||||
|
} |
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) { |
||||||
|
drawBorder(canvas) |
||||||
|
drawCenter(canvas) |
||||||
|
drawTick(canvas) |
||||||
|
} |
||||||
|
|
||||||
|
private fun drawCenter(canvas: Canvas) { |
||||||
|
mPaint.color = mUnCheckedColor |
||||||
|
val radius = (mCenterPoint.x - mStrokeWidth) * mScaleVal |
||||||
|
canvas.drawCircle(mCenterPoint.x.toFloat(), mCenterPoint.y.toFloat(), radius, mPaint) |
||||||
|
} |
||||||
|
|
||||||
|
private fun drawBorder(canvas: Canvas) { |
||||||
|
mFloorPaint.color = mFloorColor |
||||||
|
val radius = mCenterPoint.x |
||||||
|
canvas.drawCircle( |
||||||
|
mCenterPoint.x.toFloat(), |
||||||
|
mCenterPoint.y.toFloat(), |
||||||
|
radius * mFloorScale, |
||||||
|
mFloorPaint |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
private fun drawTick(canvas: Canvas) { |
||||||
|
if (mTickDrawing && isChecked) { |
||||||
|
drawTickPath(canvas) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun drawTickPath(canvas: Canvas) { |
||||||
|
mTickPath.reset() |
||||||
|
// draw left of the tick |
||||||
|
if (mDrewDistance < mLeftLineDistance) { |
||||||
|
val step: Float = if (mWidth / 20.0f < 3) 3f else mWidth / 20.0f |
||||||
|
mDrewDistance += step |
||||||
|
val stopX = |
||||||
|
mTickPoints[0].x + (mTickPoints[1].x - mTickPoints[0].x) * mDrewDistance / mLeftLineDistance |
||||||
|
val stopY = |
||||||
|
mTickPoints[0].y + (mTickPoints[1].y - mTickPoints[0].y) * mDrewDistance / mLeftLineDistance |
||||||
|
mTickPath.moveTo(mTickPoints[0].x.toFloat(), mTickPoints[0].y.toFloat()) |
||||||
|
mTickPath.lineTo(stopX, stopY) |
||||||
|
canvas.drawPath(mTickPath, mTickPaint) |
||||||
|
if (mDrewDistance > mLeftLineDistance) { |
||||||
|
mDrewDistance = mLeftLineDistance |
||||||
|
} |
||||||
|
} else { |
||||||
|
mTickPath.moveTo(mTickPoints[0].x.toFloat(), mTickPoints[0].y.toFloat()) |
||||||
|
mTickPath.lineTo(mTickPoints[1].x.toFloat(), mTickPoints[1].y.toFloat()) |
||||||
|
canvas.drawPath(mTickPath, mTickPaint) |
||||||
|
// draw right of the tick |
||||||
|
if (mDrewDistance < mLeftLineDistance + mRightLineDistance) { |
||||||
|
val stopX = |
||||||
|
mTickPoints[1].x + (mTickPoints[2].x - mTickPoints[1].x) * (mDrewDistance - mLeftLineDistance) / mRightLineDistance |
||||||
|
val stopY = |
||||||
|
mTickPoints[1].y - (mTickPoints[1].y - mTickPoints[2].y) * (mDrewDistance - mLeftLineDistance) / mRightLineDistance |
||||||
|
mTickPath.reset() |
||||||
|
mTickPath.moveTo(mTickPoints[1].x.toFloat(), mTickPoints[1].y.toFloat()) |
||||||
|
mTickPath.lineTo(stopX, stopY) |
||||||
|
canvas.drawPath(mTickPath, mTickPaint) |
||||||
|
val step: Float = if (mWidth / 20f < 3) 3f else mWidth / 20f |
||||||
|
mDrewDistance += step |
||||||
|
} else { |
||||||
|
mTickPath.reset() |
||||||
|
mTickPath.moveTo(mTickPoints[1].x.toFloat(), mTickPoints[1].y.toFloat()) |
||||||
|
mTickPath.lineTo(mTickPoints[2].x.toFloat(), mTickPoints[2].y.toFloat()) |
||||||
|
canvas.drawPath(mTickPath, mTickPaint) |
||||||
|
} |
||||||
|
} |
||||||
|
// invalidate |
||||||
|
if (mDrewDistance < mLeftLineDistance + mRightLineDistance) { |
||||||
|
postDelayed({ this.postInvalidate() }, 10) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun startCheckedAnimation() { |
||||||
|
val animator = ValueAnimator.ofFloat(1.0f, 0f) |
||||||
|
animator.duration = mAnimDuration / 3 * 2.toLong() |
||||||
|
animator.interpolator = LinearInterpolator() |
||||||
|
animator.addUpdateListener { animation: ValueAnimator -> |
||||||
|
mScaleVal = animation.animatedValue as Float |
||||||
|
mFloorColor = getGradientColor( |
||||||
|
mUnCheckedColor, |
||||||
|
mCheckedColor, |
||||||
|
1 - mScaleVal |
||||||
|
) |
||||||
|
postInvalidate() |
||||||
|
} |
||||||
|
animator.start() |
||||||
|
val floorAnimator = ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f) |
||||||
|
floorAnimator.duration = mAnimDuration.toLong() |
||||||
|
floorAnimator.interpolator = LinearInterpolator() |
||||||
|
floorAnimator.addUpdateListener { animation: ValueAnimator -> |
||||||
|
mFloorScale = animation.animatedValue as Float |
||||||
|
postInvalidate() |
||||||
|
} |
||||||
|
floorAnimator.start() |
||||||
|
drawTickDelayed() |
||||||
|
} |
||||||
|
|
||||||
|
private fun startUnCheckedAnimation() { |
||||||
|
val animator = ValueAnimator.ofFloat(0f, 1.0f) |
||||||
|
animator.duration = mAnimDuration.toLong() |
||||||
|
animator.interpolator = LinearInterpolator() |
||||||
|
animator.addUpdateListener { animation: ValueAnimator -> |
||||||
|
mScaleVal = animation.animatedValue as Float |
||||||
|
mFloorColor = getGradientColor( |
||||||
|
mCheckedColor, |
||||||
|
mFloorUnCheckedColor, |
||||||
|
mScaleVal |
||||||
|
) |
||||||
|
postInvalidate() |
||||||
|
} |
||||||
|
animator.start() |
||||||
|
val floorAnimator = ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f) |
||||||
|
floorAnimator.duration = mAnimDuration.toLong() |
||||||
|
floorAnimator.interpolator = LinearInterpolator() |
||||||
|
floorAnimator.addUpdateListener { animation: ValueAnimator -> |
||||||
|
mFloorScale = animation.animatedValue as Float |
||||||
|
postInvalidate() |
||||||
|
} |
||||||
|
floorAnimator.start() |
||||||
|
} |
||||||
|
|
||||||
|
private fun drawTickDelayed() { |
||||||
|
postDelayed({ |
||||||
|
mTickDrawing = true |
||||||
|
postInvalidate() |
||||||
|
}, mAnimDuration.toLong()) |
||||||
|
} |
||||||
|
|
||||||
|
fun setOnCheckedChangeListener(l: OnCheckedChangeListener?) { |
||||||
|
mListener = l |
||||||
|
} |
||||||
|
|
||||||
|
interface OnCheckedChangeListener { |
||||||
|
fun onCheckedChanged(checkBox: SmoothCheckBox?, isChecked: Boolean) |
||||||
|
} |
||||||
|
|
||||||
|
companion object { |
||||||
|
private const val DEF_DRAW_SIZE = 25 |
||||||
|
private const val DEF_ANIM_DURATION = 300 |
||||||
|
private fun getGradientColor(startColor: Int, endColor: Int, percent: Float): Int { |
||||||
|
val startA = Color.alpha(startColor) |
||||||
|
val startR = Color.red(startColor) |
||||||
|
val startG = Color.green(startColor) |
||||||
|
val startB = Color.blue(startColor) |
||||||
|
val endA = Color.alpha(endColor) |
||||||
|
val endR = Color.red(endColor) |
||||||
|
val endG = Color.green(endColor) |
||||||
|
val endB = Color.blue(endColor) |
||||||
|
val currentA = (startA * (1 - percent) + endA * percent).toInt() |
||||||
|
val currentR = (startR * (1 - percent) + endR * percent).toInt() |
||||||
|
val currentG = (startG * (1 - percent) + endG * percent).toInt() |
||||||
|
val currentB = (startB * (1 - percent) + endB * percent).toInt() |
||||||
|
return Color.argb(currentA, currentR, currentG, currentB) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue