前面总结到自定义控件分为
- 组合控件
- 继承已有控件 比如自定义SmartImageView继承ImageView
- 完全自定义控件
上一篇写了自定义控件的自定义属性深入理解点击链接查看,是自定控件比较难以理解的地方,但是是很重要滴,是基础。这一篇是完全自定义控件就是继承基类View,实现一个开关控件的功能,会用到以下几个知识点:
- 自定义属性
- View的绘制流程
- View的事件处理
- 回调函数
最后实现的开关效果如下:
目录
1 开关需求整体介绍
开关是一个单纯的控件view,不是已有控件的组合即组合控件,他实际上就是根据两张图绘制而成,下层是开关背景,上层是滑块,绘制成view控件后动态的控制滑块的位置就能实现开关打开、关闭的效果,比如本文例子的材料就是2张图,通过图绘制而成的开关控件,图一:
图二:
布局中引用控件MyToggleButton:
<com.yezhu.myapplication.togglebutton.MyToggleButton
android:id="@+id/mtb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
yezhu:slideBackGround="@drawable/slide_background"
yezhu:slideIcon="@drawable/slide_icon" />
自定义属性attrs:
<!--自定义开关按钮-->
<declare-styleable name="MyToggleButton">
<attr name="slideBackGround" format="reference" />
<attr name="slideIcon" format="reference" />
<attr name="state" format="boolean" />
</declare-styleable>
拿到控件的属性:
public MyToggleButton(Context context) {
this(context, null);
}
public MyToggleButton(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyToggleButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context, attrs);
}
private void initView(Context context, AttributeSet attrs) {
//1.拿到控件的引用值
TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.MyToggleButton);
int resSlideBack = attributes.getResourceId(R.styleable.MyToggleButton_slideBackGround, 0);
int resSlideIcon = attributes.getResourceId(R.styleable.MyToggleButton_slideIcon, 0);
//2.根据引用值
bitmapSlideBack = BitmapFactory.decodeResource(getResources(), resSlideBack);
bitmapSlideIcon = BitmapFactory.decodeResource(getResources(), resSlideIcon);
//3.拿到图之后 根据图绘制控件 调用onMeasure 测量 排版 绘制
//4.走完onDraw后界面上已经能显示控件 后设置控件的事件onTouchEvent
}
拿到图之后就要开始绘制控件,view的绘制流程是
- 测量onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法
- 排版
- 绘制 onDraw(Canvas canvas) 方法
似乎每一个过程展开讲都能说繁多,view的绘制渲染过程很复杂还有诸多如view过度渲染的原因等面试题,本例子只是简单的说明其流程。
2 画开关的背景和滑块
想要绘制view首先要测量,我们拿到背景图和滑块图2个bitmap,那开关的大小肯定是背景图的大小,所以有:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//根据值设定控件的大小
setMeasuredDimension(bitmapSlideBack.getWidth(), bitmapSlideBack.getHeight());
}
setMeasuredDimension()就指定了你要绘制的view控件的宽高。
其次是排版,由于我们的基类是View,所以不需要排版。继承ViewGroup时需要根据要求进行排版的,之后会有例子。
再者是绘制,绘制是根据canvas画布来实现的,当代码走完这一步的时候我们在界面上就能看到真实的控件了,当然还没有设置具体的事件监听。
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(bitmapSlideBack, 0, 0, null);
canvas.drawBitmap(bitmapSlideIcon, 30, 0, null);
}
canvas.drawBitmap(bitmapSlideIcon, 30, 0, null);方法中有四个参数,参数1是根据哪个图片绘制、参数2 距离左侧的距离、参数3 距离顶部的距离、参数4 Paint画笔是否需要。
这里我们把left写死30,效果如下:显然上层滑块距初始位置右移30,也为我们动态控制开关提供了思路,只要在控件onTouchEvent(MotionEvent event)
方法中根据手指的按下、移动、抬起动态的改变left值,再重新调用onDraw(Canvas canvas)
就能实现开关的切换效果。
3 实现开关的功能
如上所述只要在控件onTouchEvent(MotionEvent event)
方法中根据手指的按下、移动、抬起时动态的改变left值,再重新调onDraw(Canvas canvas)
就能实现开关的切换效果。
代码:
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
//手指按下 上层icon距离左侧的位置 最左侧代表是关 最右侧代表开
//iconLeft 距离左侧的距离 画图体会
iconLeft = (int) event.getX() - bitmapSlideIcon.getWidth() / 2;
break;
case MotionEvent.ACTION_MOVE:
//手指移动
iconLeft = (int) event.getX() - bitmapSlideIcon.getWidth() / 2;
break;
case MotionEvent.ACTION_UP:
//手指抬起 要是当前的手指抬起时的位置 已经是超过了背景的一半 松开手就是打开开关
// 要是手指抬起时(离开控件)的位置 尚不足背景的一半 松开手默认关闭开关
if ((int) event.getX() > (int) bitmapSlideBack.getWidth() / 2) {
iconLeft = bitmapSlideBack.getWidth() - bitmapSlideIcon.getWidth();
} else {
iconLeft = 0;
}
break;
}
invalidate();//调用这个方法 内部默认走一遍onDraw()方法
// 设置成true 是指当前监听自己消费此事件
return true;
}
需要解释的是:
event.getX()可以获取当前你点击的位置相对按钮横坐标,还有一个getRowX()方法时获取触摸点相对于屏幕的坐标;
在手指按下和移动时滑块距离左侧的位置都是event.getX()-背景宽度的一半,单纯的说特别难以理解,画图佐证一下:
再者当抬起时要根据当前滑块的位置判断一下到底是关闭还是打开,画图佐证:
当抬起时位置超过了背景的一半就默认打开,否则执行关闭。
invalidate();必须要调用,内部调用的是onDraw()方法。
返回值是true意味着此次事件由当前监听自己消费掉。
4 开关回调
在初学button时总是写点击事件setOnClickListener(new OnOnClickListener ..),那么我们这里也可以模仿点击事件写一个回调函数,当控件打开是toast提示或者做一些保存开关标志的操作。
关于回调我自己的解释是假设你写一个函数是为了显示当前控件的状态, 当控件有了事件触发的时候, 你写的那个函数就会显示改变之后的,就是回调。这样一想有没有感觉好多了,再者有没有感觉回调好像只有一个观察者的观察者模式,当被观察者发生变化时通知注册了的观察者做对应操作。
那么在onTouchEvent()方法中手指抬起时就可以这样操作:
case MotionEvent.ACTION_UP:
//手指抬起 要是当前的手指抬起时的位置 已经是超过了背景的一半 松开手就是打开开关
// 要是手指抬起时(离开控件)的位置 尚不足背景的一半 松开手默认关闭开关
if ((int) event.getX() > (int) bitmapSlideBack.getWidth() / 2) {
iconLeft = bitmapSlideBack.getWidth() - bitmapSlideIcon.getWidth();
isOpen = true;
} else {
iconLeft = 0;
isOpen = false;
}
//手指抬起 开关打开 界面toast 开关如何操作 ? 回调 界面引用开关处 多一个监听
if (mListener != null) {
mListener.onToggleState(isOpen);
}
break;
对于控件使用时:
MyToggleButton myToggleButton = (MyToggleButton) findViewById(R.id.mtb);
//做开关状态监听 回调 开关打开 吐司
myToggleButton.setOnToggleStateListener(new MyToggleButton.OnToggleStateListener() {
@SuppressLint("WrongConstant")
@Override
public void onToggleState(boolean state) {
if (state) {
Toast.makeText(MainActivity.this, "打开", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "关闭", Toast.LENGTH_SHORT).show();
}
}
});
以上就是全部的实现,希望能对大家对自定义控件时view绘制流程有一个大概认识。文章中如有叙述错误或不明确的地方请大家私信或留言指出。烦劳点个赞可好?