自定义开关
学习笔记
如有错误之处请大家帮忙指出纠正__谢谢
-------------------------------------------------------------------------------------------------------------------------------------------
难点总结 : 一个自定义控件 即实现了触摸事件 又实现了点击事件 , 那么我们就要想到 这两种事件会有冲突 , 每次触摸都会默认的走点击事件
1. 自定义类继承View
2. 拷贝包含包名的全路径到xml中
3. 界面中找到该控件, 设置初始信息
4. 根据需求绘制界面内容
5. 响应用户的触摸事件
6. 创建一个状态更新监听
1 ,自定义类继承View
重写构造 复写
加载的图片写到构造方法中 ,通过BitmapFactory . decodeResource( , )将darwable目录中的图片加载进来得到
BitmapDrawable对象, 调用
getBitmap();获得Bitmap对象
onMeasure 设置控件大小
setMeasuredDimension( , );参数就写传进来的宽高
onDraw 将图片画到界面上 canvas . drawBitmap( 加载进来的图片 , x坐标 , y坐标 ,画笔对象 ) 两个坐标默认写 [ 0 ,0 ]
我们的图片使用代码加载进来的, 代码不运行 , 显示不出来效果, 通过onDraw将图片绘制到界面上
(因为只有activity 的 onCreat方法中 cetContentView 执行后 系统才会自动调用 我们自定义控件 , )
------------------------------------------
2, 给图片设置 点击事件 , 当我们点击按钮的时候, 显示状态的切换 , 显示开 , 关
在构造方法中给自己设置点击侦听,此时,这个组件就可以被点击了
setOnClickListener(
this
);
让我们的自定义控件类实现 OnClickListener 接口 , 显示onclick();方法
这时,组件一旦被点击 , 就会触发给自己设置的点击事件 而触发 onClick( )方法
在onClick中设置 滑块状态位置的改变
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 @Override 2 public void onClick(View v) { 3 if(!isDrop){ 4 if(toggleState){ 5 slideLeft = 0; 6 toggleState = false; 7 } 8 else{ 9 slideLeft = slideLeftMax; 10 toggleState = true; 11 } 12 13 //让界面无效,onDraw会重新调用 14 invalidate(); 15 } 16 }
![](https://i-blog.csdnimg.cn/blog_migrate/6a26342e61070ed9a6850f9ed6d10a71.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/12daedf9610d5c6a0d6cdb88a090fd79.jpeg)
------------------------------------------
3 , 添加滑动功能
重写该组件的
onTouchEvent方法,
触摸事件,当这个组件被触摸,就会产生触摸事件
判断当前的三个状态 , 按下 , 移动 , 抬起
public
boolean
onTouchEvent(MotionEvent event) {
return false ; 触摸的时候该方法只会调用一次,再次触摸就不会传给他,而会传给他的父元素 ; 父元素没有指定操作 ,所以第二次触摸不会有任何反应
return true ; 多少次触摸事件都会传递到我身上 ,我会完全处理,再次点击时,我会继续响应事件
}
int
firstX; 开始坐标
int lastX; 上一次坐标
滑块每一动一点 , 都要重新改变起始位置的值 , 否则逻辑不通
如果界面滑动滑块没有显示效果 , 一定要考虑是否 重绘界面 , invalidate(); 从新调用onDraw方法 否则滑动无效果
每滑动一次都要重绘界面 , 从新调用onDraw方法( ), 否则界面无反应 , 我们这里直接将invalidate方法 封装到flushView ( )方法中了
----------------------------------------------
![](https://i-blog.csdnimg.cn/blog_migrate/83ed18d39effe882801fe064eda8bbdb.jpeg)
问题1 : 滑块滑出去了 ;
在调用重绘界面方法的时候 , 进行判断
![](https://i-blog.csdnimg.cn/blog_migrate/1d1c6db506863146bd26292447f82206.png)
----------------------------------------------
4 滑动距离判断滑动是否跳到指定距离 :
设置 当滑动图片 , 移动小半部分距离的时候 , 松手 --按钮回到起始位置 ; 当移动超过一多半距离的时候 , 松手 直接跳到右边
![](https://i-blog.csdnimg.cn/blog_migrate/497951baaa753dbd6e14c6f218e5ddcb.png)
调用该方法 :
根据传进来的boolean值 而设置 滑块x轴最大值 和 最小值 , 从而实现跳到
指定位置
![](https://i-blog.csdnimg.cn/blog_migrate/cb11fcb50eec09f50e601157ba635a1c.png)
---------------------------------
5 开关的点击 和 滑动的冲突 处理
这时, bug就来了
移动触摸事件生效了 , 点击事件 却失效 了 , 无论如何点击都不会有相应
为什么设置了点击监听就 无效了呢
原因就是我们的
onTouchEvent 我们将该方法中
super
.onTouchEvent(event); 方法删除了
没有调用父类的
onTouchEvent
强调一个概念, 安卓中其实是只有触摸事件 , 没有点击事件的 ,
点击事件是在触摸事件父类中低层自动调用的 , 如果我们不访问父类, 点击事件的onClick ( )方法就不会被调用执行,因此失效
查看低层源码-->
case
MotionEvent.
ACTION_UP
:中有个
performClick(); -->
li.
mOnClickListener
.onClick(
this
);
父类中已经做了点击事件的调用 ,我们将super 写上 , 点击事件就生效了
![](https://i-blog.csdnimg.cn/blog_migrate/622ea98ce6bf3fd2ceecf0b30629a1fe.png)
-----------------------------------
这样 , 当我们每移动一小部分 , 松手 ,开关状体都会切换 , 不合理 , 需要在点击事件中进行处理 :
因为我们移动一丁点的距离 , 松手 , 就会走触摸事件 而 访问父类 导致每次都会调用点击事件 , 这时就需要在点击事件中进行 逻辑判断
![](https://i-blog.csdnimg.cn/blog_migrate/fb2ec3ba3f4ec0ffe9f1df9c432300f3.jpeg)
当滑块滑动到这里 , 不要让点击事件生效, 让触摸事件去处理相应的逻辑 ,
![](https://i-blog.csdnimg.cn/blog_migrate/d02f62f7525832c3cfa5fec6a785c1b5.png)
如果在触摸事件中计算出 用户的操作是滑动, 就使onClick方法中的逻辑不执行就可以了
1 public boolean onTouchEvent(MotionEvent event) { 2 super.onTouchEvent(event); 3 switch (event.getAction()) { 4 case MotionEvent.ACTION_DOWN: 5 //记录手指按下时的坐标 6 firstX = lastX = (int) event.getX(); 7 8 isDrop = false; //用户一按下就需要从新设置标记状态 9 10 break; 11 case MotionEvent.ACTION_MOVE: 12 int newX = (int) event.getX(); 13 14 //判断用户当前时点击还是滑动,如果手指移动的像素超过6,那么判定为滑动 15 if(Math.abs(newX - firstX) > 6){ 这里需要取绝对值 16 isDrop = true; 17 } 18 break; 19 case MotionEvent.ACTION_UP: 20 if(isDrop){ 21 逻辑代码.... 22 } 23 break; 24 } 25 flushView(); 26 return true; 27 }
public void onClick(View v) { if(!isDrop){ 逻辑代码..... } }
这时 , 我们的操作都能实现了
-----------------------------------------------
6 ,自定义属性
-
1 res/values/创建attrs.xml
2 一、在attrs.xml文件中声明属性,如:
3 <declare-styleable name="MyToggleBtn"> // 声名属性集的名称,即这些属性是属于哪个控件的。 4 <attr name="current_state" format="boolean"/> // 声名属性 current_state 格式为 boolean 类型 5 <attr name="slide_button" format="reference"/> // 声名属性 slide_button 格式为 reference 类型 6 </declare-styleable> 7 所有的format类型,详见注1:
8 二、在布局文件中使用:在使用之前必须声名命名空间,xmlns:heima="http://schemas.android.com/apk/res/com.itheima.mytogglebtn" 9 说明:xmlns 是XML name space 的缩写; 10 heima 可为任意写符 11 http://schemas.android.com/apk/res/ 此为android固定格式; 12 com.itheima.mytogglebtn 此应用的包名,如manifest配置文件中一致。
13 布局文件: 14 <com.itheima.mytogglebtn.MyToggleButton 15 xmlns:heima="http://schemas.android.com/apk/res/com.itheima.mytogglebtn" 16 android:layout_width="wrap_content" 17 android:layout_height="wrap_content" 18 heima:slide_button="@drawable/slide_button" />
19 三、在代码中对属性进行解析,主要代码:
20 TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyToggleBtn); // 由attrs 获得 TypeArray
21 注1: 22 format 常用类型 23 reference 引用 24 color 颜色 25 boolean 布尔值 26 dimension 尺寸值 27 float 浮点值 28 integer 整型值 29 string 字符串 30 enum 枚举值
---------------------------------------------------------------------------------------------------------------------------------
自定义Button源代码 :
2 3 import android.annotation.SuppressLint; 4 import android.content.Context; 5 import android.content.res.TypedArray; 6 import android.graphics.Bitmap; 7 import android.graphics.BitmapFactory; 8 import android.graphics.Canvas; 9 import android.graphics.Paint; 10 import android.graphics.drawable.BitmapDrawable; 11 import android.util.AttributeSet; 12 import android.view.MotionEvent; 13 import android.view.View; 14 import android.view.View.OnClickListener; 15 16 17 public class MyToggleButton extends View implements OnClickListener { 18 19 private Bitmap slide_button; 20 private Bitmap switch_background; 21 int slideLeft = 0; 22 boolean toggleState = false; 23 int slideLeftMax; 24 int firstX; 25 int lastX; 26 boolean isDrop = false; 27 28 //AttributeSet:封装了布局文件中定义的属性 29 public MyToggleButton(Context context, AttributeSet attrs) { 30 super(context, attrs); 31 32 //获取属性的名字和值 33 // int count = attrs.getAttributeCount(); 34 // for (int i = 0; i < count; i++) { 35 // System.out.print(attrs.getAttributeName(i) + "; "); 36 // System.out.println(attrs.getAttributeValue(i)); 37 // } 38 39 //解析AttributeSet,返回一个TypedArray,此对象中封装了解析出来的属性 40 //arg1:期望解析出来的属性自定义的属性在R文件中有数组记录 int值 41 TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyToggleBtn); 42 //取出解析好的属性值 43 BitmapDrawable bd1 = (BitmapDrawable) ta.getDrawable(R.styleable.MyToggleBtn_switch_bg); 44 BitmapDrawable bd2 = (BitmapDrawable) ta.getDrawable(R.styleable.MyToggleBtn_slide_bg); 45 //把BitmapDrawable中封装的图片以Bitmap形式返回 46 switch_background = bd1.getBitmap(); 47 slide_button = bd2.getBitmap(); 48 49 // slide_button = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button); 50 // switch_background = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background); 51 52 //算出两个图片的宽度差 53 slideLeftMax = switch_background.getWidth() - slide_button.getWidth(); 54 //给自己设置点击侦听,此时,这个组件就可以被点击了 55 setOnClickListener(this); 56 } 57 58 @Override 59 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 60 // super.onMeasure(widthMeasureSpec, heightMeasureSpec); 61 //指定组件宽高 62 setMeasuredDimension(switch_background.getWidth(), switch_background.getHeight()); 63 } 64 65 @Override 66 protected void onDraw(Canvas canvas) { 67 // TODO Auto-generated method stub 68 super.onDraw(canvas); 69 70 Paint paint = new Paint(); 71 canvas.drawBitmap(switch_background, 0, 0, paint); 72 canvas.drawBitmap(slide_button, slideLeft, 0, paint); 73 } 74 75 @Override 76 public void onClick(View v) { 77 if(!isDrop){ 78 if(toggleState){ 79 slideLeft = 0; 80 toggleState = false; 81 } 82 else{ 83 slideLeft = slideLeftMax; 84 toggleState = true; 85 } 86 87 //让界面无效,onDraw会重新调用 88 invalidate(); 89 } 90 } 91 92 //触摸事件,当这个组件被触摸,就会产生触摸事件 93 @Override 94 public boolean onTouchEvent(MotionEvent event) { 95 super.onTouchEvent(event); 96 switch (event.getAction()) { 97 case MotionEvent.ACTION_DOWN: 98 //记录手指按下时的坐标 99 firstX = lastX = (int) event.getX(); 100 isDrop = false; 101 break; 102 case MotionEvent.ACTION_MOVE: 103 int newX = (int) event.getX(); 104 //计算手指移动的像素 105 int offsetX = newX - lastX; 106 slideLeft += offsetX; 107 lastX = newX; 108 109 //判断用户当前时点击还是滑动,如果手指移动的像素超过6,那么判定为滑动 110 if(Math.abs(newX - firstX) > 6){ 111 isDrop = true; 112 } 113 break; 114 case MotionEvent.ACTION_UP: 115 if(isDrop){ 116 //获取手指抬起时的坐标,判断手指滑动的距离,如果距离大于开关长度的一半,那么就生效 117 int upX = (int) event.getX(); 118 if(upX - firstX >= slideLeftMax / 2){ 119 toggleState = true; 120 } 121 else if(firstX - upX >= slideLeftMax / 2){ 122 toggleState = false; 123 } 124 flushState(); 125 } 126 break; 127 128 } 129 flushView(); 130 return true; 131 } 132 133 private void flushState() { 134 if(toggleState) 135 slideLeft = slideLeftMax; 136 else 137 slideLeft = 0; 138 139 } 140 private void flushView() { 141 if(slideLeft < 0) 142 slideLeft = 0; 143 else if(slideLeft > slideLeftMax) 144 slideLeft = slideLeftMax; 145 invalidate(); 146 } 147 }
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 <RelativeLayout 2 xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:itheima="http://schemas.android.com/apk/res/com.itheima.custombutton" 4 xmlns:tools="http://schemas.android.com/tools" 5 android:layout_width="match_parent" 6 android:layout_height="match_parent" 7 android:paddingBottom="@dimen/activity_vertical_margin" 8 android:paddingLeft="@dimen/activity_horizontal_margin" 9 android:paddingRight="@dimen/activity_horizontal_margin" 10 android:paddingTop="@dimen/activity_vertical_margin" 11 tools:context=".MainActivity" > 12 13 14 <com.itheima.custombutton.MyToggleButton 15 android:layout_width="wrap_content" 16 android:layout_height="wrap_content" 17 android:layout_centerInParent="true" 18 itheima:switch_bg="@drawable/switch_background" 19 itheima:slide_bg="@drawable/slide_button" 20 /> 21 22 </RelativeLayout>
1 <?xml version="1.0" encoding="utf-8"?> 2 <resources> 3 <declare-styleable name="MyToggleBtn"> 4 <attr name="switch_bg" format="reference"/> 5 <attr name="slide_bg" format="reference"/> 6 </declare-styleable> 7 </resources>