[置顶] 自定义控件:onDraw 方法实现仿 iOS 的开关效果
2016-03-24 10:38
741 查看
概述
本文主要讲解如何在 Android 下实现高仿 iOS 的开关按钮,并非是在 Android 自带的 ToggleButton 上修改,而是使用 API 提供的 onDraw、onMeasure、Canvas 方法,纯手工绘制。基本原理就是在 Canvas 上叠着放两张图片,上面的图片根据手指触摸情况,不断移动,实现开关效果。本文示例代码:https://github.com/heshiweij/EasySwitchButton
效果图:
功能点:
1. 不滑出边界,超过一半自动切换(边界判断)
2. 可滑动,也可点击(事件共存)
3. 提供状态改变监听(设置回调)
3. 通过属性设置初始状态、背景图片、滑动按钮(自定义属性)
自定义View的概述
Android 在绘制 View 时,其实就像蒙上眼睛在画板上画画,它并不知道应该把 View 画多大,画哪儿,怎么画。所以我们必须实现 View 的三个重要方法,以告诉它这些信息。即:onMeasure(画多大),onLayout(画哪儿),onDraw(怎么画)。View的生命周期
未设置点击事件 | 是否监听 ACTION_MOVE |
---|---|
onFinishedInflate() | 当从布局文件创建时调用,做一些初始化的操作,如创建对象等 |
onSizeChanged() | 当尺寸改变时调用,做一些进一步的初始化,如:处理外部通过 set 设置的属性 |
onMeasure() | 当需要测量时调用,指定 View 的大小 |
onLayout() | 当需要布局时调用,指定 View 的位置 |
onDraw() | 当需要绘制时调用,指定 View 的内容 |
View 的默认不支持 WRAP_CONTENT,必须重写 onMeasure 方法,通过 setMeasuredDimension() 设置尺寸
基本的事件分发机制:onClickListener 一定是在 onTouchEvent 之后执行
自定义View的流程
自定义 View 一般遵循如下流程:开始动手
下面就开始动手来实现了初始化成员
/* 画笔 */ Paint mPaint; /* 背景图片 */ Bitmap mSwitchBackground; /* 滑动图片 */ Bitmap mSlideButton; /* 最大滑动距离 */ int mMaxLeft; /* 当前滑动距离 */ int mCurrLeft; /* 当前状态 */ boolean isOpen = false;
/* 初始化各种组件 */ private void init(AttributeSet attrs) { // 初始化画笔 mPaint = new Paint(); mPaint.setColor(Color.BLUE); // 初始化背景图片 mSwitchBackground = BitmapFactory.decodeResource(getResources(),R.drawable.switch_background); mSlideButton = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button); // 计算最大可滑动距离 mMaxLeft = mSwitchBackground.getWidth() - mSlideButton.getWidth(); // 设置开关事件 // 已将点击事件的逻辑,移至 onTouchEvent 的 ACTION_UP 中 }
封装设置状态的方法:
/* 设置状态,此方法只改变 mCurrLeft ,不引起重绘*/ private void setStatus(boolean status){ if (status){ mCurrLeft = mMaxLeft; isOpen = true; } else { mCurrLeft = 0; isOpen = false; } }
测量并绘制
设置 View 的宽高为背景图的宽高@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 将 View 的宽高设置为背景图的宽高 setMeasuredDimension(mSwitchBackground.getWidth(), mSwitchBackground.getHeight()); }
测量完成后,需要实现 onDraw ,在 View 提供的 Canvas 画板绘制两个图片。 其中
mCurrLeft是关键,滑动的原理就是不断改变
mCurrLeft的值,调用
invalidate()引起重绘,不断重新执行 onDraw,从而发生位移的改变。
@Override protected void onDraw(Canvas canvas) { canvas.drawBitmap(mSwitchBackground, 0, 0, mPaint); canvas.drawBitmap(mSlideButton, mCurrLeft, 0, mPaint); }
处理 onTouchEvent
// 开始位置 int startX; /** * event.getX() 基于控件本身 * event.getRawX() 基于整个屏幕 */ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: int distance = (int) (event.getX() - startX); mCurrLeft += distance; startX = (int) event.getX(); break; } // 边界判断,不让滑块滑出边界 if (mCurrLeft < 0){ mCurrLeft = 0; } if (mCurrLeft > mMaxLeft){ mCurrLeft = mMaxLeft; } // 引起重绘(重新调用 onDraw 方法) invalidate(); return true; }
至此,滑块已经可以做最基本的滑动,基本像样了。
关于 onTouchEvent 的返回值
我们发现,当给 onTouchEvent 的返回值设为 false,就不能监听
ACTION_MOVE了。这牵扯到 View 的事件分发机制,关于这个内容,我稍后会写一篇文章,详细阐述我的理解。
目前,暂时只需要记住下面这个规则:
设置点击事件 | 是否监听 ACTION_MOVE | 是否响应点击事件 |
---|---|---|
return true | YES | NO |
return false | NO | NO |
return super.onTouchEvent | YES | YES |
未设置点击事件 | 是否监听 ACTION_MOVE | 是否响应点击事件 |
---|---|---|
return true | YES | 不需要 |
return false | NO | 不需要 |
return super.onTouchEvent | NO | 不需要 |
处理状态改变回调
/** 定义接口 */ public interface OnOpenedListener { void onChecked(View v, boolean isOpened); } /** 定义成员变量 */ private onOpenedListener mOpenedkListener; /** 提供设置回调的方法 */ public void setOnCheckChangedListener(onOpenedListener checkedkListener) { this.mOpenedkListener = checkedkListener; }
接着在状态改变的时刻,添加如下代码即可:
onTouchEvent 的 ACTION_UP
//处理回调 if (mOpenedkListener != null){ mOpenedkListener.onChecked(this, isOpen); }
事件共存
本案例中,我们需要实现的效果是,用户既能点击切换,也能滑动切换。但是我们知道,如果设置了点击事件,并且onTouchEvent返回
return super.onTouchEvent(envent),点击事件必然在
onTouchEvent的
ACTION_UP后执行,由于
onTouchEvent和
onClickListener共用同一个状态,将导致冲突。具体表现是:无法将滑块滑到打开位置(一移动,自动弹回来)。
这时,我们就需要增加一个变量 moveX,记录用户从手指按下到抬起滑过的距离,如果
moveX <5,则认为点击,如果
moveX >= 5,则认为滑动。
代码如下:
在initView() 中添加点击事件
setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (isClick ){ setStatus(!isOpen); // 引起重绘(重新调用 onDraw 方法) invalidate(); } } });
booleal isClick;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE:
int distance = (int) (event.getX() - startX);
mCurrLeft += distance;
startX = (int) event.getX();
// 移动的距离必须用绝对值,避免往回滑,moveX反向减小
moveX += Math.abs(distance);
break;
case MotionEvent.ACTION_UP:
if (moveX >= 5){
isClick = false;
// 用户的本意是滑动
setStatus(mCurrLeft >= mMaxLeft / 2);
//处理回调 if (mOpenedkListener != null){ mOpenedkListener.onChecked(this, isOpen); }
} else {
isClick = true;
// 用户的本意是点击,交给 onClickListener
}
// 完成一次滑动,则 moveX 必须清零
moveX = 0;
break;
}
// 边界判断
...
// 引起重绘
invalidate();
return true;
}
自定义属性
为了方便用户在 XML 中设置属性,需要添加自定义属性attr.xml
<declare-styleable name="SwitchButton"> <attr name="isOpened" format="boolean" /> <attr name="slide_button" format="reference" /> <attr name="switch_background" format="reference" /> </declare-styleable>
命名空间
xmlns:ifavor="http://schemas.android.com/apk/res/res-auto"
定义控件
<com.example.customeview.switchbutton.SwitchButton android:layout_centerInParent="true" android:id="@+id/sb_button" ifavor:isOpened="true" ifavor:slide_button="@drawable/slide_button" ifavor:switch_background="@drawable/switch_background" android:layout_width="wrap_content" android:layout_height="wrap_content" />
附录
本文示例代码:https://github.com/heshiweij/EasySwitchButton图片资源来源:
SwitchButton 开关按钮 的多种实现方式 (附源码DEMO)
相关文章推荐
- iOS 设置NavigationItem的Title的字体大小和颜色
- iOS常用宏定义
- ios 遍历方式
- iOS海哥开发笔记(开发中如何使用数据持久化)海哥原创,让你对存储知识一目了然
- 集成支付宝钱包支付iOS SDK的方法与经验
- iOS 设置视图的圆角效果
- iOS性能优化:Instruments使用实战
- iOS 点击按钮跳转到指定的TabBar
- iOS:OC的定时器任务方法,延时方法
- iOS开发编码建议与编程经验
- IOS中延时执行的几种方式的比较
- NSTimer使用注意事项
- iOS-CGContextAddArc各参数说明
- IOS开发之很简单的下拉刷新,包你满意
- IOS自适应前段库-Masonry的使用(转载)
- iOS 开发-删除storyboard的正确方法
- iOS开发之获取各种文件的目录路径的方法
- iOS 本地推送、远程推送及带快速回复的本地推送
- iOS从入门到颈椎病发作
- 实例讲解设计模式中的命令模式在iOS App开发中的运用