自己定义控件:onDraw 方法实现仿 iOS 的开关效果
概述
本文主要解说怎样在 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
图片资源来源:
自己定义控件:onDraw 方法实现仿 iOS 的开关效果的更多相关文章
- android--------自定义控件 之 方法篇
前面简单的讲述了Android中自定义控件的理论和流程图,今天通过代码来详细的讲解一下其中的方法 首先先创建一个类 CircularView 继承于 View,之后实现构造方法(初始化步骤) publ ...
- git-osc自己定义控件之:CircleImageView
git-osc自己定义控件之:CircleImageView 一.CircleImageView的使用 在项目中能够发现,用户的头像都是圆形的.感觉非常好奇,昨天最终发现了,原来是自定了一个Image ...
- 【Asp.net之旅】--因自己定义控件注冊而引发的思考
前言 近期在开发远洋的SOA系统平台,开发使用的是.NET平台.对于Asp.net并不困难,但该系统的开发并非全然依靠Asp.net.而是自身封装好的框架.这套框架是远洋地产购买的微软的开发平台,项目 ...
- 【Android】自己定义控件——仿天猫Indicator
今天来说说类似天猫的Banner中的小圆点是怎么做的(图中绿圈部分) 在学习自己定义控件之前,我用的是很二的方法,直接在布局中放入多个ImageView,然后代码中依据Pager切换来改变图片.这样的 ...
- 自己定义控件的onMeasure方法具体解释
在我们自己定义控件的时候可能你会用到onMeasure方法,以下就具体的给大家介绍一下这种方法: @Override protected void onMeasure(int widthMeasure ...
- Android自己定义控件(状态提示图表)
[工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处.尊重分享成果] 1 背景 前面分析那么多系统源代码了.也该暂停下来歇息一下,趁昨晚闲着看见一个有意思的需求就操 ...
- Android自己定义控件系列五:自己定义绚丽水波纹效果
尊重原创!转载请注明出处:http://blog.csdn.net/cyp331203/article/details/41114551 今天我们来利用Android自己定义控件实现一个比較有趣的效果 ...
- Android自己定义控件系列二:自己定义开关button(一)
这一次我们将会实现一个完整纯粹的自己定义控件,而不是像之前的组合控件一样.拿系统的控件来实现.计划分为三部分:自己定义控件的基本部分,自己定义控件的触摸事件的处理和自己定义控件的自己定义属性: 以下就 ...
- 【Android】自己定义控件实现可滑动的开关(switch)
~转载请注明来源:http://blog.csdn.net/u013015161/article/details/46704745 介绍 昨天晚上写了一个Android的滑动开关, 即SlideSwi ...
随机推荐
- HTML城市联动
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- poj1787 Charlie's Change
思路: 完全背包,记录路径. 实现: #include <bits/stdc++.h> using namespace std; const int INF = 0x3f3f3f3f; ] ...
- typeloadexception 方法实现中引用的声明不能是final方法
问题描述: 1. 修改了DVSNetClient项目,其依赖类库CameraDSP没有改动.CameraDSP_DVSNetClient.dll的版本编号和文件编号由1.0.0.0变为1.0.1.0. ...
- Sublime——基本操作
基本安装 程序下载地址:https://www.sublimetext.com/ package control安装 View -> Show Console打开控制台或者用快捷键ctrl+~打 ...
- HDU_1203_01背包
I NEED A OFFER! Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)T ...
- 梦想Android版CAD控件2018.10.12更新
下载地址: http://www.mxdraw.com/ndetail_10106.html 1. 增加读写对象扩展字典功能 2. 修改样条线显示错误 3. 修改shx文字显示错误 4. 增加向量运算 ...
- linux cp复制文件 直接覆盖
命令: \cp -rf aaaa/* bbbb 复制aaa下的文件到bbb目录
- BZOJ 2693: jzptab 莫比乌斯反演 + 积性函数 +筛法
Code: #include<bits/stdc++.h> #define ll long long #define M 10001000 #define maxn 10200100 #d ...
- 洛谷——P2054 [AHOI2005]洗牌(扩展欧几里得,逆元)
P2054 [AHOI2005]洗牌 扩展欧拉定理求逆元 $1 2 3 4 5 6$$4 1 5 2 6 3$$2 4 6 1 3 5$$1 2 3 4 5 6$ 手推一下样例,你就会发现是有规律的: ...
- C++ 迭代器运算符 箭头运算符->
所有标准库容器都支持迭代器,只有少数几种才支持下标运算 迭代器运算符 运算符 作用 *iter 返回迭代器iter所指元素的引用 iter -> mem 解引用iter,并获取元素名为mem的成 ...