From 8bf454bb2b29e929b982af42e63fcc7691cd55b5 Mon Sep 17 00:00:00 2001 From: TongTongStudio Date: Mon, 27 Jul 2026 07:25:17 +0800 Subject: [PATCH] =?UTF-8?q?refactor(view):=20=E5=B0=86SwitchButton?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E8=87=B3=E9=A1=B9=E7=9B=AE=E5=86=85=E9=83=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 27 +- .../settings/utils/SettingsUtilsActivity.java | 2 +- .../ttstd/dialer/view/BindingAdapters.java | 2 +- .../com/ttstd/dialer/view/SettingItem.java | 2 +- .../com/ttstd/dialer/view/SwitchButton.kt | 1163 +++++++++++++++++ .../main/res/layout/activity_contact_add.xml | 6 +- .../main/res/layout/activity_contact_edit.xml | 4 +- .../layout/layout_setting_content_item.xml | 2 +- .../main/res/values/switch_button_attrs.xml | 29 + 9 files changed, 1202 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/com/ttstd/dialer/view/SwitchButton.kt create mode 100644 app/src/main/res/values/switch_button_attrs.xml diff --git a/app/build.gradle b/app/build.gradle index 095a214..a9e7f67 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -373,7 +373,7 @@ dependencies { implementation 'com.github.getActivity:Toaster:12.6' // 权限请求框架:https://github.com/getActivity/XXPermissions implementation 'com.github.getActivity:XXPermissions:20.0' - implementation 'com.github.zcweng:switch-button:0.0.3@aar' +// implementation 'com.github.zcweng:switch-button:0.0.3@aar' implementation "com.github.kongzue.DialogX:DialogX:0.0.50" implementation 'cn.6tail:lunar:1.7.7' implementation "io.github.cymchad:BaseRecyclerViewAdapterHelper4:4.4.0" @@ -422,28 +422,3 @@ project.afterEvaluate { ) } } - -//preBuild { -// doLast { -// def imlFile = file(project.name + ".iml") -//// def imlFile = file("..\\.idea\\modules\\" + project.name + "\\" + rootProject.name + "." + project.name + ".iml") -// println 'Change ' + project.name + '.iml order' -// try { -// def parsedXml = (new XmlParser()).parse(imlFile) -// def jdkNode = parsedXml.component[1].orderEntry.find { it.'@type' == 'jdk' } -// parsedXml.component[1].remove(jdkNode) -// def sdkString = "Android API " + android.compileSdkVersion.substring("android-".length()) + " Platform" -// println 'what' + sdkString -// new Node(parsedXml.component[1], 'orderEntry', ['type': 'jdk', 'jdkName': sdkString, 'jdkType': 'Android SDK']) -// groovy.xml.XmlUtil.serialize(parsedXml, new FileOutputStream(imlFile)) -// } catch (FileNotFoundException e) { -// // nop, iml not found -// println "no iml found" -// } -// } -// //https://www.pianshen.com/article/93481144911/ -// //https://blog.csdn.net/dhl_1986/article/details/102856026 -// //https://blog.csdn.net/zhonghe1114/article/details/80923730 -// //https://blog.csdn.net/u014175785/article/details/116235760 -// //使用系统编译后的framework.jar -//} diff --git a/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java b/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java index 8e6cce4..bf59937 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java @@ -5,7 +5,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.view.View; -import com.suke.widget.SwitchButton; +import com.ttstd.dialer.view.SwitchButton; import com.tencent.mmkv.MMKV; import com.ttstd.dialer.R; import com.ttstd.dialer.activity.main.MainActivity; diff --git a/app/src/main/java/com/ttstd/dialer/view/BindingAdapters.java b/app/src/main/java/com/ttstd/dialer/view/BindingAdapters.java index a30ee06..ae2037f 100644 --- a/app/src/main/java/com/ttstd/dialer/view/BindingAdapters.java +++ b/app/src/main/java/com/ttstd/dialer/view/BindingAdapters.java @@ -2,7 +2,7 @@ package com.ttstd.dialer.view; import androidx.databinding.BindingAdapter; -import com.suke.widget.SwitchButton; +import com.ttstd.dialer.view.SwitchButton; public class BindingAdapters { diff --git a/app/src/main/java/com/ttstd/dialer/view/SettingItem.java b/app/src/main/java/com/ttstd/dialer/view/SettingItem.java index 9e9b857..d04dd96 100644 --- a/app/src/main/java/com/ttstd/dialer/view/SettingItem.java +++ b/app/src/main/java/com/ttstd/dialer/view/SettingItem.java @@ -14,7 +14,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; -import com.suke.widget.SwitchButton; +import com.ttstd.dialer.view.SwitchButton; import com.ttstd.dialer.R; import org.jetbrains.annotations.NotNull; diff --git a/app/src/main/java/com/ttstd/dialer/view/SwitchButton.kt b/app/src/main/java/com/ttstd/dialer/view/SwitchButton.kt new file mode 100644 index 0000000..fc3ed67 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/view/SwitchButton.kt @@ -0,0 +1,1163 @@ +package com.ttstd.dialer.view + +import android.animation.Animator +import android.widget.Checkable +import android.annotation.TargetApi +import android.os.Build +import android.content.res.TypedArray +import android.animation.ValueAnimator +import android.view.View.MeasureSpec +import android.view.MotionEvent +import android.view.View.OnLongClickListener +import android.graphics.RectF +import android.animation.ValueAnimator.AnimatorUpdateListener +import android.animation.Animator.AnimatorListener +import android.animation.ArgbEvaluator +import android.content.Context +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import android.util.TypedValue +import android.view.View +import com.ttstd.dialer.R +import java.lang.RuntimeException +import kotlin.math.max +import kotlin.math.min + +/** + * SwitchButton. + */ +class SwitchButton : View, Checkable { + /** + * 动画状态: + * 1.静止 + * 2.进入拖动 + * 3.处于拖动 + * 4.拖动-复位 + * 5.拖动-切换 + * 6.点击切换 + */ + private val ANIMATE_STATE_NONE = 0 + private val ANIMATE_STATE_PENDING_DRAG = 1 + private val ANIMATE_STATE_DRAGING = 2 + private val ANIMATE_STATE_PENDING_RESET = 3 + private val ANIMATE_STATE_PENDING_SETTLE = 4 + private val ANIMATE_STATE_SWITCH = 5 + + constructor(context: Context) : super(context) { + init(context, null) + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + init(context, attrs) + } + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + init(context, attrs) + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) { + init(context, attrs) + } + + override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + super.setPadding(0, 0, 0, 0) + } + + /** + * 初始化参数 + */ + private fun init(context: Context, attrs: AttributeSet?) { + var typedArray: TypedArray? = null + if (attrs != null) { + typedArray = context.obtainStyledAttributes(attrs, R.styleable.SwitchButton) + } + shadowEffect = optBoolean( + typedArray, + R.styleable.SwitchButton_sb_shadow_effect, + true + ) + uncheckCircleColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_uncheckcircle_color, + -0x555556 + ) //0XffAAAAAA; + uncheckCircleWidth = optPixelSize( + typedArray, + R.styleable.SwitchButton_sb_uncheckcircle_width, + dp2pxInt(1.5f) + ) //dp2pxInt(1.5f); + uncheckCircleOffsetX = dp2px(10f) + uncheckCircleRadius = optPixelSize( + typedArray, + R.styleable.SwitchButton_sb_uncheckcircle_radius, + dp2px(4f) + ) //dp2px(4); + checkedLineOffsetX = dp2px(4f) + checkedLineOffsetY = dp2px(4f) + shadowRadius = optPixelSize( + typedArray, + R.styleable.SwitchButton_sb_shadow_radius, + dp2pxInt(2.5f) + ) //dp2pxInt(2.5f); + shadowOffset = optPixelSize( + typedArray, + R.styleable.SwitchButton_sb_shadow_offset, + dp2pxInt(1.5f) + ) //dp2pxInt(1.5f); + shadowColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_shadow_color, + 0X33000000 + ) //0X33000000; + uncheckColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_uncheck_color, + -0x222223 + ) //0XffDDDDDD; + checkedColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_checked_color, + -0xae2c99 + ) //0Xff51d367; + borderWidth = optPixelSize( + typedArray, + R.styleable.SwitchButton_sb_border_width, + dp2pxInt(1f) + ) //dp2pxInt(1); + checkLineColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_checkline_color, + Color.WHITE + ) //Color.WHITE; + checkLineWidth = optPixelSize( + typedArray, + R.styleable.SwitchButton_sb_checkline_width, + dp2pxInt(1f) + ) //dp2pxInt(1.0f); + checkLineLength = dp2px(6f) + val buttonColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_button_color, + Color.WHITE + ) //Color.WHITE; + uncheckButtonColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_uncheckbutton_color, + buttonColor + ) + checkedButtonColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_checkedbutton_color, + buttonColor + ) + val effectDuration = optInt( + typedArray, + R.styleable.SwitchButton_sb_effect_duration, + 300 + ) //300; + isChecked = optBoolean( + typedArray, + R.styleable.SwitchButton_sb_checked, + false + ) + showIndicator = optBoolean( + typedArray, + R.styleable.SwitchButton_sb_show_indicator, + true + ) + background = optColor( + typedArray, + R.styleable.SwitchButton_sb_background, + Color.WHITE + ) //Color.WHITE; + enableEffect = optBoolean( + typedArray, + R.styleable.SwitchButton_sb_enable_effect, + true + ) + typedArray?.recycle() + paint = Paint(Paint.ANTI_ALIAS_FLAG) + buttonPaint = Paint(Paint.ANTI_ALIAS_FLAG) + buttonPaint!!.color = buttonColor + if (shadowEffect) { + buttonPaint!!.setShadowLayer( + shadowRadius.toFloat(), 0f, shadowOffset.toFloat(), + shadowColor + ) + } + viewState = ViewState() + beforeState = ViewState() + afterState = ViewState() + valueAnimator = ValueAnimator.ofFloat(0f, 1f) + valueAnimator?.duration = effectDuration.toLong() + valueAnimator?.repeatCount = 0 + valueAnimator?.addUpdateListener(animatorUpdateListener) + valueAnimator?.addListener(animatorListener) + super.setClickable(true) + setPadding(0, 0, 0, 0) + setLayerType(LAYER_TYPE_SOFTWARE, null) + + } + + override fun onMeasure(widthMeasureSpecParam: Int, heightMeasureSpecParam: Int) { + var widthMeasureSpec = widthMeasureSpecParam + var heightMeasureSpec = heightMeasureSpecParam + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + if (widthMode == MeasureSpec.UNSPECIFIED + || widthMode == MeasureSpec.AT_MOST + ) { + widthMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_WIDTH, MeasureSpec.EXACTLY) + } + if (heightMode == MeasureSpec.UNSPECIFIED + || heightMode == MeasureSpec.AT_MOST + ) { + heightMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_HEIGHT, MeasureSpec.EXACTLY) + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + val viewPadding = max(shadowRadius + shadowOffset, borderWidth).toFloat() + height = h - viewPadding - viewPadding + width = w - viewPadding - viewPadding + viewRadius = height * .5f + buttonRadius = viewRadius - borderWidth + left = viewPadding + top = viewPadding + right = w - viewPadding + bottom = h - viewPadding + centerX = (left + right) * .5f + centerY = (top + bottom) * .5f + buttonMinX = left + viewRadius + buttonMaxX = right - viewRadius + if (isChecked()) { + setCheckedViewState(viewState) + } else { + setUncheckViewState(viewState) + } + isUiInited = true + postInvalidate() + } + + /** + * @param viewState + */ + private fun setUncheckViewState(viewState: ViewState?) { + viewState?.radius = 0f + viewState?.checkStateColor = uncheckColor + viewState?.checkedLineColor = Color.TRANSPARENT + viewState?.buttonX = buttonMinX + buttonPaint?.color = uncheckButtonColor + } + + /** + * @param viewState + */ + private fun setCheckedViewState(viewState: ViewState?) { + viewState?.radius = viewRadius + viewState?.checkStateColor = checkedColor + viewState?.checkedLineColor = checkLineColor + viewState?.buttonX = buttonMaxX + buttonPaint?.color = checkedButtonColor + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + paint?.strokeWidth = borderWidth.toFloat() + paint?.style = Paint.Style.FILL + //绘制白色背景 + paint?.color = background + drawRoundRect( + canvas, + left, top, right, bottom, + viewRadius, paint + ) + //绘制关闭状态的边框 + paint?.style = Paint.Style.STROKE + paint?.color = uncheckColor + drawRoundRect( + canvas, + left, top, right, bottom, + viewRadius, paint + ) + + //绘制小圆圈 + if (showIndicator) { + drawUncheckIndicator(canvas) + } + + //绘制开启背景色 + val des = viewState!!.radius * .5f //[0-backgroundRadius*0.5f] + paint?.style = Paint.Style.STROKE + paint?.color = viewState?.checkStateColor ?: Color.TRANSPARENT + paint?.strokeWidth = borderWidth + des * 2f + drawRoundRect(canvas, left + des, top + des, right - des, bottom - des, viewRadius, paint) + + //绘制按钮左边绿色长条遮挡 + paint?.style = Paint.Style.FILL + paint?.strokeWidth = 1f + drawArc(canvas, left, top, left + 2 * viewRadius, top + 2 * viewRadius, 90f, 180f, paint) + paint?.let { canvas.drawRect(left + viewRadius, top, viewState?.buttonX ?: 0f, top + 2 * viewRadius, it) } + + //绘制小线条 + if (showIndicator) { + drawCheckedIndicator(canvas) + } + + //绘制按钮 + viewState?.buttonX?.let { drawButton(canvas, it, centerY) } + } + /** + * 绘制选中状态指示器 + * @param canvas + * @param color + * @param lineWidth + * @param sx + * @param sy + * @param ex + * @param ey + * @param paint + */ + /** + * 绘制选中状态指示器 + * @param canvas + */ + protected fun drawCheckedIndicator( + canvas: Canvas, + color: Int = viewState?.checkedLineColor ?: Color.TRANSPARENT, + lineWidth: Float = checkLineWidth.toFloat(), + sx: Float = left + viewRadius - checkedLineOffsetX, + sy: Float = centerY - checkLineLength, + ex: Float = left + viewRadius - checkedLineOffsetY, + ey: Float = centerY + checkLineLength, + paint: Paint? = this.paint + ) { + paint?.style = Paint.Style.STROKE + paint?.color = color + paint?.strokeWidth = lineWidth + paint?.let { canvas.drawLine(sx, sy, ex, ey, it) } + } + + /** + * 绘制关闭状态指示器 + * @param canvas + */ + private fun drawUncheckIndicator(canvas: Canvas) { + drawUncheckIndicator( + canvas, + uncheckCircleColor, + uncheckCircleWidth.toFloat(), + right - uncheckCircleOffsetX, centerY, + uncheckCircleRadius, + paint + ) + } + + /** + * 绘制关闭状态指示器 + * @param canvas + * @param color + * @param lineWidth + * @param centerX + * @param centerY + * @param radius + * @param paint + */ + protected fun drawUncheckIndicator( + canvas: Canvas, + color: Int, + lineWidth: Float, + centerX: Float, centerY: Float, + radius: Float, + paint: Paint? + ) { + paint?.style = Paint.Style.STROKE + paint?.color = color + paint?.strokeWidth = lineWidth + if (paint != null) { + canvas.drawCircle(centerX, centerY, radius, paint) + } + } + + /** + * @param canvas + * @param left + * @param top + * @param right + * @param bottom + * @param startAngle + * @param sweepAngle + * @param paint + */ + private fun drawArc( + canvas: Canvas, + left: Float, top: Float, + right: Float, bottom: Float, + startAngle: Float, + sweepAngle: Float, + paint: Paint? + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + canvas.drawArc( + left, top, right, bottom, + startAngle, sweepAngle, true, paint!! + ) + } else { + rect[left, top, right] = bottom + canvas.drawArc( + rect, + startAngle, sweepAngle, true, paint!! + ) + } + } + + /** + * @param canvas + * @param left + * @param top + * @param right + * @param bottom + * @param backgroundRadius + * @param paint + */ + private fun drawRoundRect( + canvas: Canvas, + left: Float, top: Float, + right: Float, bottom: Float, + backgroundRadius: Float, + paint: Paint? + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + canvas.drawRoundRect( + left, top, right, bottom, + backgroundRadius, backgroundRadius, paint!! + ) + } else { + rect[left, top, right] = bottom + canvas.drawRoundRect( + rect, + backgroundRadius, backgroundRadius, paint!! + ) + } + } + + /** + * @param canvas + * @param x px + * @param y px + */ + private fun drawButton(canvas: Canvas, x: Float, y: Float) { + buttonPaint?.let { canvas.drawCircle(x, y, buttonRadius, it) } + paint?.style = Paint.Style.STROKE + paint?.strokeWidth = 1f + paint?.color = -0x222223 + paint?.let { canvas.drawCircle(x, y, buttonRadius, it) } + } + + override fun setChecked(checked: Boolean) { + if (checked == isChecked()) { + postInvalidate() + return + } + toggle(enableEffect, false) + } + + override fun isChecked(): Boolean { + return isChecked + } + + override fun toggle() { + toggle(true) + } + + /** + * 切换状态 + * @param animate + */ + fun toggle(animate: Boolean) { + toggle(animate, true) + } + + private fun toggle(animate: Boolean, broadcast: Boolean) { + if (!isEnabled) { + return + } + if (isEventBroadcast) { + throw RuntimeException("should NOT switch the state in method: [onCheckedChanged]!") + } + if (!isUiInited) { + isChecked = !isChecked + if (broadcast) { + broadcastEvent() + } + return + } + if (valueAnimator!!.isRunning) { + valueAnimator!!.cancel() + } + if (!enableEffect || !animate) { + isChecked = !isChecked + if (isChecked()) { + setCheckedViewState(viewState) + } else { + setUncheckViewState(viewState) + } + postInvalidate() + if (broadcast) { + broadcastEvent() + } + return + } + animateState = ANIMATE_STATE_SWITCH + viewState?.let { beforeState?.copy(it) } + if (isChecked()) { + //切换到unchecked + setUncheckViewState(afterState) + } else { + setCheckedViewState(afterState) + } + valueAnimator!!.start() + } + + /** + * + */ + private fun broadcastEvent() { + if (onCheckedChangeListener != null) { + isEventBroadcast = true + onCheckedChangeListener?.onCheckedChanged(this, isChecked()) + } + isEventBroadcast = false + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (!isEnabled) { + return false + } + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + isTouchingDown = true + touchDownTime = System.currentTimeMillis() + //取消准备进入拖动状态 + removeCallbacks(postPendingDrag) + //预设100ms进入拖动状态 + postDelayed(postPendingDrag, 100) + } + MotionEvent.ACTION_MOVE -> { + val eventX = event.x + if (isPendingDragState) { + //在准备进入拖动状态过程中,可以拖动按钮位置 + var fraction = eventX / getWidth() + fraction = max(0f, min(1f, fraction)) + viewState?.buttonX = (buttonMinX + + (buttonMaxX - buttonMinX) + * fraction) + } else if (isDragState) { + //拖动按钮位置,同时改变对应的背景颜色 + var fraction = eventX / getWidth() + fraction = max(0f, min(1f, fraction)) + viewState?.buttonX = (buttonMinX + + (buttonMaxX - buttonMinX) + * fraction) + viewState?.checkStateColor = argbEvaluator.evaluate( + fraction, + uncheckColor, + checkedColor + ) as Int + postInvalidate() + } + } + MotionEvent.ACTION_UP -> { + isTouchingDown = false + //取消准备进入拖动状态 + removeCallbacks(postPendingDrag) + if (System.currentTimeMillis() - touchDownTime <= 300) { + //点击时间小于300ms,认为是点击操作 + toggle() + } else if (isDragState) { + //在拖动状态,计算按钮位置,设置是否切换状态 + val eventX = event.x + var fraction = eventX / getWidth() + fraction = Math.max(0f, Math.min(1f, fraction)) + val newCheck = fraction > .5f + if (newCheck == isChecked()) { + pendingCancelDragState() + } else { + isChecked = newCheck + pendingSettleState() + } + } else if (isPendingDragState) { + //在准备进入拖动状态过程中,取消之,复位 + pendingCancelDragState() + } + } + MotionEvent.ACTION_CANCEL -> { + isTouchingDown = false + removeCallbacks(postPendingDrag) + if (isPendingDragState + || isDragState + ) { + //复位 + pendingCancelDragState() + } + } + } + return true + } + + /** + * 是否在动画状态 + * @return + */ + private val isInAnimating: Boolean + get() = animateState != ANIMATE_STATE_NONE + + /** + * 是否在进入拖动或离开拖动状态 + * @return + */ + private val isPendingDragState: Boolean + get() = (animateState == ANIMATE_STATE_PENDING_DRAG + || animateState == ANIMATE_STATE_PENDING_RESET) + + /** + * 是否在手指拖动状态 + * @return + */ + private val isDragState: Boolean + get() = animateState == ANIMATE_STATE_DRAGING + + /** + * 设置是否启用阴影效果 + * @param shadowEffect true.启用 + */ + fun setShadowEffect(shadowEffect: Boolean) { + if (this.shadowEffect == shadowEffect) { + return + } + this.shadowEffect = shadowEffect + if (this.shadowEffect) { + buttonPaint!!.setShadowLayer( + shadowRadius.toFloat(), 0f, shadowOffset.toFloat(), + shadowColor + ) + } else { + buttonPaint!!.setShadowLayer( + 0f, 0f, 0f, + 0 + ) + } + } + + fun setEnableEffect(enable: Boolean) { + enableEffect = enable + } + + /** + * 开始进入拖动状态 + */ + private fun pendingDragState() { + if (isInAnimating) { + return + } + if (!isTouchingDown) { + return + } + if (valueAnimator!!.isRunning) { + valueAnimator!!.cancel() + } + animateState = ANIMATE_STATE_PENDING_DRAG + viewState?.let { beforeState?.copy(it) } + viewState?.let { afterState?.copy(it) } + if (isChecked()) { + afterState?.checkStateColor = checkedColor + afterState?.buttonX = buttonMaxX + afterState?.checkedLineColor = checkedColor + } else { + afterState?.checkStateColor = uncheckColor + afterState?.buttonX = buttonMinX + afterState?.radius = viewRadius + } + valueAnimator?.start() + } + + /** + * 取消拖动状态 + */ + private fun pendingCancelDragState() { + if (isDragState || isPendingDragState) { + if (valueAnimator?.isRunning == true) { + valueAnimator?.cancel() + } + animateState = ANIMATE_STATE_PENDING_RESET + viewState?.let { beforeState?.copy(it) } + if (isChecked()) { + setCheckedViewState(afterState) + } else { + setUncheckViewState(afterState) + } + valueAnimator?.start() + } + } + + /** + * 动画-设置新的状态 + */ + private fun pendingSettleState() { + if (valueAnimator?.isRunning == true) { + valueAnimator?.cancel() + } + animateState = ANIMATE_STATE_PENDING_SETTLE + viewState?.let { beforeState?.copy(it) } + if (isChecked()) { + setCheckedViewState(afterState) + } else { + setUncheckViewState(afterState) + } + valueAnimator?.start() + } + + override fun setOnClickListener(l: OnClickListener?) {} + override fun setOnLongClickListener(l: OnLongClickListener?) {} + fun setOnCheckedChangeListener(l: OnCheckedChangeListener?) { + onCheckedChangeListener = l + } + + interface OnCheckedChangeListener { + fun onCheckedChanged(view: SwitchButton?, isChecked: Boolean) + } + /** */ + /** + * 阴影半径 + */ + private var shadowRadius = 0 + + /** + * 阴影Y偏移px + */ + private var shadowOffset = 0 + + /** + * 阴影颜色 + */ + private var shadowColor = 0 + + /** + * 背景半径 + */ + private var viewRadius = 0f + + /** + * 按钮半径 + */ + private var buttonRadius = 0f + + /** + * 背景高 + */ + private var height = 0f + + /** + * 背景宽 + */ + private var width = 0f + + /** + * 背景位置 + */ + private var left = 0f + private var top = 0f + private var right = 0f + private var bottom = 0f + private var centerX = 0f + private var centerY = 0f + + /** + * 背景底色 + */ + private var background = 0 + + /** + * 背景关闭颜色 + */ + private var uncheckColor = 0 + + /** + * 背景打开颜色 + */ + private var checkedColor = 0 + + /** + * 边框宽度px + */ + private var borderWidth = 0 + + /** + * 打开指示线颜色 + */ + private var checkLineColor = 0 + + /** + * 打开指示线宽 + */ + private var checkLineWidth = 0 + + /** + * 打开指示线长 + */ + private var checkLineLength = 0f + + /** + * 关闭圆圈颜色 + */ + private var uncheckCircleColor = 0 + + /** + * 关闭圆圈线宽 + */ + private var uncheckCircleWidth = 0 + + /** + * 关闭圆圈位移X + */ + private var uncheckCircleOffsetX = 0f + + /** + * 关闭圆圈半径 + */ + private var uncheckCircleRadius = 0f + + /** + * 打开指示线位移X + */ + private var checkedLineOffsetX = 0f + + /** + * 打开指示线位移Y + */ + private var checkedLineOffsetY = 0f + + /** + * Color for button when it's uncheck + */ + private var uncheckButtonColor = 0 + + /** + * Color for button when it's check + */ + private var checkedButtonColor = 0 + + /** + * 按钮最左边 + */ + private var buttonMinX = 0f + + /** + * 按钮最右边 + */ + private var buttonMaxX = 0f + + /** + * 按钮画笔 + */ + private var buttonPaint: Paint? = null + + /** + * 背景画笔 + */ + private var paint: Paint? = null + + /** + * 当前状态 + */ + private var viewState: ViewState? = null + private var beforeState: ViewState? = null + private var afterState: ViewState? = null + private val rect = RectF() + + /** + * 动画状态 + */ + private var animateState = ANIMATE_STATE_NONE + + /** + * + */ + private var valueAnimator: ValueAnimator? = null + private val argbEvaluator = ArgbEvaluator() + + /** + * 是否选中 + */ + private var isChecked = false + + /** + * 是否启用动画 + */ + private var enableEffect = false + + /** + * 是否启用阴影效果 + */ + private var shadowEffect = false + + /** + * 是否显示指示器 + */ + private var showIndicator = false + + /** + * 收拾是否按下 + */ + private var isTouchingDown = false + + /** + * + */ + private var isUiInited = false + + /** + * + */ + private var isEventBroadcast = false + private var onCheckedChangeListener: OnCheckedChangeListener? = null + + /** + * 手势按下的时刻 + */ + private var touchDownTime: Long = 0 + private val postPendingDrag = Runnable { + if (!isInAnimating) { + pendingDragState() + } + } + private val animatorUpdateListener: AnimatorUpdateListener = object : AnimatorUpdateListener { + override fun onAnimationUpdate(animation: ValueAnimator) { + val value = animation.animatedValue as Float + when (animateState) { + ANIMATE_STATE_PENDING_SETTLE -> { + run {} + run {} + run { + viewState!!.checkedLineColor = argbEvaluator.evaluate( + value, + beforeState!!.checkedLineColor, + afterState!!.checkedLineColor + ) as Int + viewState!!.radius = (beforeState!!.radius + + (afterState!!.radius - beforeState!!.radius) * value) + if (animateState != ANIMATE_STATE_PENDING_DRAG) { + viewState!!.buttonX = (beforeState!!.buttonX + + (afterState!!.buttonX - beforeState!!.buttonX) * value) + } + viewState!!.checkStateColor = argbEvaluator.evaluate( + value, + beforeState!!.checkStateColor, + afterState!!.checkStateColor + ) as Int + } + } + ANIMATE_STATE_PENDING_RESET -> { + run {} + run { + viewState!!.checkedLineColor = argbEvaluator.evaluate( + value, + beforeState!!.checkedLineColor, + afterState!!.checkedLineColor + ) as Int + viewState!!.radius = (beforeState!!.radius + + (afterState!!.radius - beforeState!!.radius) * value) + if (animateState != ANIMATE_STATE_PENDING_DRAG) { + viewState!!.buttonX = (beforeState!!.buttonX + + (afterState!!.buttonX - beforeState!!.buttonX) * value) + } + viewState!!.checkStateColor = argbEvaluator.evaluate( + value, + beforeState!!.checkStateColor, + afterState!!.checkStateColor + ) as Int + } + } + ANIMATE_STATE_PENDING_DRAG -> { + viewState!!.checkedLineColor = argbEvaluator.evaluate( + value, + beforeState!!.checkedLineColor, + afterState!!.checkedLineColor + ) as Int + viewState!!.radius = (beforeState!!.radius + + (afterState!!.radius - beforeState!!.radius) * value) + if (animateState != ANIMATE_STATE_PENDING_DRAG) { + viewState!!.buttonX = (beforeState!!.buttonX + + (afterState!!.buttonX - beforeState!!.buttonX) * value) + } + viewState!!.checkStateColor = argbEvaluator.evaluate( + value, + beforeState!!.checkStateColor, + afterState!!.checkStateColor + ) as Int + } + ANIMATE_STATE_SWITCH -> { + viewState?.buttonX = (beforeState!!.buttonX + + (afterState!!.buttonX - beforeState!!.buttonX) * value) + val fraction = (viewState!!.buttonX - buttonMinX) / (buttonMaxX - buttonMinX) + viewState?.checkStateColor = argbEvaluator.evaluate( + fraction, + uncheckColor, + checkedColor + ) as Int + viewState?.radius = fraction * viewRadius + viewState?.checkedLineColor = argbEvaluator.evaluate( + fraction, + Color.TRANSPARENT, + checkLineColor + ) as Int + } + ANIMATE_STATE_DRAGING -> { + run {} + run {} + } + ANIMATE_STATE_NONE -> {} + else -> { + run {} + run {} + } + } + postInvalidate() + } + } + private val animatorListener: AnimatorListener = object : AnimatorListener { + override fun onAnimationStart(animation: Animator) {} + override fun onAnimationEnd(animation: Animator) { + when (animateState) { + ANIMATE_STATE_DRAGING -> {} + ANIMATE_STATE_PENDING_DRAG -> { + animateState = ANIMATE_STATE_DRAGING + viewState?.checkedLineColor = Color.TRANSPARENT + viewState?.radius = viewRadius + postInvalidate() + } + ANIMATE_STATE_PENDING_RESET -> { + animateState = ANIMATE_STATE_NONE + postInvalidate() + } + ANIMATE_STATE_PENDING_SETTLE -> { + animateState = ANIMATE_STATE_NONE + postInvalidate() + broadcastEvent() + } + ANIMATE_STATE_SWITCH -> { + isChecked = !isChecked + animateState = ANIMATE_STATE_NONE + postInvalidate() + broadcastEvent() + } + ANIMATE_STATE_NONE -> {} + else -> {} + } + } + + override fun onAnimationCancel(animation: Animator) {} + override fun onAnimationRepeat(animation: Animator) {} + } + /** */ + /** + * 保存动画状态 + */ + private class ViewState internal constructor() { + /** + * 按钮x位置[buttonMinX-buttonMaxX] + */ + var buttonX = 0f + + /** + * 状态背景颜色 + */ + var checkStateColor = 0 + + /** + * 选中线的颜色 + */ + var checkedLineColor = 0 + + /** + * 状态背景的半径 + */ + var radius = 0f + fun copy(source: ViewState) { + buttonX = source.buttonX + checkStateColor = source.checkStateColor + checkedLineColor = source.checkedLineColor + radius = source.radius + } + } + + companion object { + private val DEFAULT_WIDTH = dp2pxInt(58f) + private val DEFAULT_HEIGHT = dp2pxInt(36f) + + /** */ + private fun dp2px(dp: Float): Float { + val r = Resources.getSystem() + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.displayMetrics) + } + + private fun dp2pxInt(dp: Float): Int { + return dp2px(dp).toInt() + } + + private fun optInt( + typedArray: TypedArray?, + index: Int, + def: Int + ): Int { + return typedArray?.getInt(index, def) ?: def + } + + private fun optPixelSize( + typedArray: TypedArray?, + index: Int, + def: Float + ): Float { + return typedArray?.getDimension(index, def) ?: def + } + + private fun optPixelSize( + typedArray: TypedArray?, + index: Int, + def: Int + ): Int { + return typedArray?.getDimensionPixelOffset(index, def) ?: def + } + + private fun optColor( + typedArray: TypedArray?, + index: Int, + def: Int + ): Int { + return typedArray?.getColor(index, def) ?: def + } + + private fun optBoolean( + typedArray: TypedArray?, + index: Int, + def: Boolean + ): Boolean { + return typedArray?.getBoolean(index, def) ?: def + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_contact_add.xml b/app/src/main/res/layout/activity_contact_add.xml index d73b235..f972ef6 100644 --- a/app/src/main/res/layout/activity_contact_add.xml +++ b/app/src/main/res/layout/activity_contact_add.xml @@ -203,7 +203,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file