ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# 1. 图片资源: ![](https://img.kancloud.cn/41/2b/412ba42096c55f3fb9d4633d8963cb48_114x66.png) ![](https://img.kancloud.cn/0f/a0/0fa0375e59f07ddd5536f52ca1ccc91e_180x66.png) # 2. 简单尝试 由于比较简单,所以这里就直接开始。 首先需要将两个图片给相对位置布局放置好,还是放置在xml文件中: ~~~ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <ImageView android:id="@+id/btn_b" android:background="@drawable/switch_background" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" /> <ImageView android:id="@+id/btn_f" android:background="@drawable/slide_button" android:layout_width="wrap_content" android:layout_alignBottom="@id/btn_b" android:layout_height="wrap_content" android:text="Hello World!" /> </RelativeLayout> ~~~ 然后可以看见预览: ![](https://img.kancloud.cn/d2/d3/d2d38445453272704df24a628c73f80f_162x71.png) 接着,我们需要在代码中进行简单切换处理: ~~~ fun calcMargin(): Int{ return btn_b.width - btn_f.width } ~~~ ~~~ // 为背景层设置点击事件 btn_b.setOnClickListener(object : View.OnClickListener { override fun onClick(v: View?) { val layoutParams = RelativeLayout.LayoutParams(btn_f.width, btn_f.height) if (isOpen) { layoutParams.marginStart = 0 } else { layoutParams.marginStart = calcMargin() } isOpen = !isOpen btn_f.layoutParams = layoutParams } }) ~~~ 然后就可以实现点击切换。但是有时候用户也会使用拖拽,所以这里还需要简单处理一下拖拽的效果。但是这种处理还会随着外层布局的改变而改变,也就是每次我们都需要自己去处理控件的边距,或许要做到动态效果还需要进行添加动画。比如下面的简单处理: ~~~ class MainActivity : AppCompatActivity() { val gb by lazy { findViewById<ImageView>(R.id.btn_b)} val qg by lazy { findViewById<ImageView>(R.id.btn_f)} var startX = 0f var marginLeftValue = 0f var margin = 0f var isOpen = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_other) qg.setOnTouchListener(object : View.OnTouchListener { override fun onTouch(v: View?, event: MotionEvent?): Boolean { margin = (gb.width - qg.width).toFloat() when (event?.action) { MotionEvent.ACTION_DOWN -> { // 记录起始位置 startX = event.x } MotionEvent.ACTION_MOVE -> { // 计算逻辑位置 val offset = event.x - startX marginLeftValue += offset // 屏蔽边界 if (marginLeftValue < 0) { marginLeftValue = 0f } else if (marginLeftValue > margin) { marginLeftValue = margin } refresh() } } return true } }) } fun refresh(){ val parms = RelativeLayout.LayoutParams(qg.width, qg.height) parms.marginStart = marginLeftValue.toInt() qg.layoutParams = parms } } ~~~ 但是这不是我们所期望的,这里将其封装一下。以方便调用。 # 3. 回顾 就需要自定View,当然对于自定义的属性这里再次复习一下: * 定义一个继承自View的类,并复写对应的构造方法; * 在res/values/attrs.xml文件中定义需要用的属性; * 在主布局文件中使用; * 在自定义的View类中写自己需要的一些操作功能; 当然,这里简单复写一下,就以测试为主。比如首先定义对应的attrs.xml文件: ~~~ <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="MyButton"> <attr name="textInfo" format="string"/> <attr name="textSize" format="integer"/> <attr name="background" format="reference|color"/> </declare-styleable> </resources> ~~~ 对应的在主布局文件中使用: ~~~ <com.weizu.switchbutton.MyButton android:layout_width="wrap_content" android:layout_height="wrap_content" app:textInfo="Hello!" app:textSize="32" app:textBg="#FF00FF" /> ~~~ 然后我们再看下自定义的View,这里由于我只需要xml方式,所以这里就不需要其余的构造函数,只需要一个即可。这里首先来看看使用命名空间方式获取配置的属性值: ## 3.1 命名空间方式获取属性值 ~~~ class MyButton: View{ // 读取xml中配置的属性 constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs){ // 方法一,直接获取字符串类型数据 val textInfo = attrs?.getAttributeValue("http://schemas.android.com/apk/res-auto", "textInfo") val textSize = attrs?.getAttributeValue("http://schemas.android.com/apk/res-auto", "textSize") val background = attrs?.getAttributeValue("http://schemas.android.com/apk/res-auto", "textBg") Log.e("TAG", "textInfo: ${textInfo}, textSize: ${textSize}, textBg: ${background}") } } ~~~ 运行后可以看见日志信息: > textInfo: Hello!, textSize: 32, textBg: #ffff00ff 值得注意的是,对于背景资源这里是引用,但是这种按照命名空间取出值的方式得到的还是一个字符串,比如如果我们传入的是一个资源ID,再次测试一下,修改下主布局文件: ~~~ <com.weizu.switchbutton.MyButton android:layout_width="wrap_content" android:layout_height="wrap_content" app:textInfo="Hello!" app:textSize="32" app:textBg="@drawable/slide_button" /> ~~~ 然后再次运行: > textInfo: Hello!, textSize: 32, textBg: @2131165328 从上面结果可以看出对于引用类型资源,就不适用了,因为返回的也只是一个资源ID,没多大意义。 ## 3.2 TypedArray ~~~ class MyButton: View{ // 读取xml中配置的属性 constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { // 方法二,TypedArray val typedArray = context?.obtainStyledAttributes(attrs, R.styleable.MyButton) for (i in 0..typedArray!!.indexCount) { val index = typedArray.getIndex(i) when (index) { R.styleable.MyButton_textInfo -> { Log.e("TAG", "MyButton_textInfo: ${typedArray.getText(index)}") } R.styleable.MyButton_textSize -> { Log.e("TAG", "MyButton_textInfo: ${typedArray.getInt(index, 0)}") } R.styleable.MyButton_textBg -> { Log.e("TAG", "MyButton_textInfo: ${typedArray.getDrawable(index)}") } } } } } ~~~ 当然需要回收一下资源: ~~~cpp //释放TypedArray typedArray.recycle(); ~~~ # 4. 封装 回到正题,这里简单分析可以知道我们需要定义的只有三个属性: * 前景图层资源 * 背景图层资源 * 滑块拖动时间 ## 4.1 预备工作 ~~~ res/values/attrs.xml <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="MyButton"> <attr name="bgImg" format="reference"/> <attr name="qgImg" format="reference"/> <attr name="DTime" format="integer"/> </declare-styleable> </resources> ~~~ ~~~ main_activity.xml <com.weizu.switchbutton.MyButton android:layout_width="wrap_content" android:layout_height="wrap_content" app:bgImg="@drawable/switch_background" app:qgImg="@drawable/slide_button" app:DTime="500" /> ~~~ 然后我们就可以简单的让其绘制出来: ~~~ class MyButton(context: Context?, attrs: AttributeSet?) : View(context, attrs) { lateinit var mQg: Bitmap lateinit var mBg: Bitmap lateinit var paint: Paint var mDTime: Int = 0 var margin: Int = 0 init { // 方法二,TypedArray// 读取xml中配置的属性 val typedArray = context?.obtainStyledAttributes(attrs, R.styleable.MyButton) for (i in 0..typedArray!!.indexCount) { val index = typedArray.getIndex(i) when (index) { R.styleable.MyButton_qgImg -> { mQg = (typedArray.getDrawable(index)!! as BitmapDrawable).bitmap } R.styleable.MyButton_bgImg -> { mBg = (typedArray.getDrawable(index)!! as BitmapDrawable).bitmap } R.styleable.MyButton_DTime -> { mDTime = typedArray.getInt(index, mDTime) } } } // 计算距离 margin = mBg.width - mQg.width // 初始化画笔 paint = Paint() paint.isAntiAlias = true } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { // 设置宽度为背景图片的宽度 setMeasuredDimension(mBg.width, mBg.height) } override fun onDraw(canvas: Canvas?) { // 绘制前景和背景 canvas?.drawBitmap(mBg, 0f, 0f, paint) canvas?.drawBitmap(mQg, 0f, 0f, paint) } } ~~~ 效果: ![](https://img.kancloud.cn/36/7a/367a141134998f152eb170d825ca7052_227x70.png) 然后就是为其添加动态的交互效果。 ## 4.2 添加点击 类似的,我们可以很容易的为之添加点击事件,因为这里是整个view,所以我们可以在其中直接设置监听即可。首先定义两个状态: ~~~ // 开关状态 var isOpen = false // 前景层距离左边界的值 var marginLeftValue: Float ~~~ 然后修改onDraw: ~~~ override fun onDraw(canvas: Canvas?) { // 绘制前景和背景 canvas?.drawBitmap(mBg, 0f, 0f, paint) canvas?.drawBitmap(mQg, marginLeftValue, 0f, paint) } ~~~ 然后设置一下监听: ~~~ // 设置点击监听 setOnClickListener(object : OnClickListener{ override fun onClick(v: View?) { if(isOpen){ // 设置边距为最大,然后重新绘制 marginLeftValue = margin.toFloat() } else{ // 设置边距为0,然后重新绘制 marginLeftValue = 0f } isOpen = !isOpen postInvalidate() } }) ~~~ 就可以做到按钮的简单开关切换。 ## 4.3 添加拖拽事件 对于拖拽事件,我们需要使用到触摸事件,所以这里需要重写一下onTouchEvent方法,然后处理的简单逻辑为: * 按下记录位置,然后根据移动的下一个点位置来判断移动方向; * 根据手指移动距离来计相应的移动前景照片; * 设置有效移动范围; * 判断开关状态,然后重新绘制; ~~~ // 触摸事件 override fun onTouchEvent(event: MotionEvent?): Boolean { super.onTouchEvent(event) when(event?.action){ MotionEvent.ACTION_DOWN -> { // 手指按下,记录起始值 currentX = event.x } MotionEvent.ACTION_MOVE -> { // 手指移动,计算偏移量 val endX = event.x val offset = endX - currentX // 对应逻辑上移动的距离 marginLeftValue += offset // 但是要屏蔽非法值 if(marginLeftValue < 0){ marginLeftValue = 0f } else if(marginLeftValue > margin){ marginLeftValue = margin.toFloat() } // 请求重新绘制 postInvalidate() // 更新 currentX = endX } MotionEvent.ACTION_UP -> { // 手指离开屏幕,判断当前状态 isOpen = marginLeftValue > margin / 2 if(isOpen){ // 设置边距为最大,然后重新绘制 marginLeftValue = margin.toFloat() } else{ // 设置边距为0,然后重新绘制 marginLeftValue = 0f } postInvalidate() } } return true; // 表示事件已经处理 } ~~~ 然后就可以实现这个效果。至于拖动时间这里感觉没有这个必要。 # 5. 完整代码: ~~~ class MyButton(context: Context?, attrs: AttributeSet?) : View(context, attrs) { lateinit var mQg: Bitmap lateinit var mBg: Bitmap var paint: Paint var mDTime: Int = 0 var margin: Int = 0 // 开关状态 var isOpen = false // 前景层距离左边界的值 var marginLeftValue: Float init { // 初始化边距值为0 marginLeftValue = 0f // 方法二,TypedArray// 读取xml中配置的属性 val typedArray = context?.obtainStyledAttributes(attrs, R.styleable.MyButton) for (i in 0..typedArray!!.indexCount) { val index = typedArray.getIndex(i) when (index) { R.styleable.MyButton_qgImg -> { mQg = (typedArray.getDrawable(index)!! as BitmapDrawable).bitmap } R.styleable.MyButton_bgImg -> { mBg = (typedArray.getDrawable(index)!! as BitmapDrawable).bitmap } R.styleable.MyButton_DTime -> { mDTime = typedArray.getInt(index, mDTime) } } } // 计算距离 margin = mBg.width - mQg.width // 初始化画笔 paint = Paint() paint.isAntiAlias = true // 设置点击监听 setOnClickListener(object : OnClickListener{ override fun onClick(v: View?) { if(isOpen){ // 设置边距为最大,然后重新绘制 marginLeftValue = margin.toFloat() } else{ // 设置边距为0,然后重新绘制 marginLeftValue = 0f } isOpen = !isOpen postInvalidate() } }) } var currentX = 0f // 触摸事件 override fun onTouchEvent(event: MotionEvent?): Boolean { super.onTouchEvent(event) when(event?.action){ MotionEvent.ACTION_DOWN -> { // 手指按下,记录起始值 currentX = event.x } MotionEvent.ACTION_MOVE -> { // 手指移动,计算偏移量 val endX = event.x val offset = endX - currentX // 对应逻辑上移动的距离 marginLeftValue += offset // 但是要屏蔽非法值 if(marginLeftValue < 0){ marginLeftValue = 0f } else if(marginLeftValue > margin){ marginLeftValue = margin.toFloat() } // 请求重新绘制 postInvalidate() // 更新 currentX = endX } MotionEvent.ACTION_UP -> { // 手指离开屏幕,判断当前状态 isOpen = marginLeftValue > margin / 2 if(isOpen){ // 设置边距为最大,然后重新绘制 marginLeftValue = margin.toFloat() } else{ // 设置边距为0,然后重新绘制 marginLeftValue = 0f } postInvalidate() } } return true; // 表示事件已经处理 } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { // 设置宽度为背景图片的宽度 setMeasuredDimension(mBg.width, mBg.height) } override fun onDraw(canvas: Canvas?) { // 绘制前景和背景 canvas?.drawBitmap(mBg, 0f, 0f, paint) canvas?.drawBitmap(mQg, marginLeftValue, 0f, paint) } } ~~~