概述

本文主要解说怎样在 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 的内容

在动手写之前,必须先了解以下几个概念:

  1. View 的默认不支持 WRAP_CONTENT,必须重写 onMeasure 方法,通过 setMeasuredDimension() 设置尺寸

  2. 主要的事件分发机制:onClickListener 一定是在 onTouchEvent 之后运行

自己定义View的流程

自己定义 View 一般遵循例如以下流程:

開始动手

以下就開始动手来实现了

初始化成员

  1. /* 画笔 */
  2. Paint mPaint;
  3. /* 背景图片 */
  4. Bitmap mSwitchBackground;
  5. /* 滑动图片 */
  6. Bitmap mSlideButton;
  7. /* 最大滑动距离 */
  8. int mMaxLeft;
  9. /* 当前滑动距离 */
  10. int mCurrLeft;
  11. /* 当前状态 */
  12. boolean isOpen = false;
  1. /* 初始化各种组件 */
  2. private void init(AttributeSet attrs) {
  3. // 初始化画笔
  4. mPaint = new Paint();
  5. mPaint.setColor(Color.BLUE);
  6. // 初始化背景图片
  7. mSwitchBackground = BitmapFactory.decodeResource(getResources(),R.drawable.switch_background);
  8. mSlideButton = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button);
  9. // 计算最大可滑动距离
  10. mMaxLeft = mSwitchBackground.getWidth() - mSlideButton.getWidth();
  11. // 设置开关事件
  12. // 已将点击事件的逻辑。移至 onTouchEvent 的 ACTION_UP 中
  13. }

封装设置状态的方法:

  1. /* 设置状态。此方法仅仅改变 mCurrLeft 。不引起重绘*/
  2. private void setStatus(boolean status){
  3. if (status){
  4. mCurrLeft = mMaxLeft;
  5. isOpen = true;
  6. } else {
  7. mCurrLeft = 0;
  8. isOpen = false;
  9. }
  10. }

測量并绘制

设置 View 的宽高为背景图的宽高

  1. @Override
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  3. // 将 View 的宽高设置为背景图的宽高
  4. setMeasuredDimension(mSwitchBackground.getWidth(), mSwitchBackground.getHeight());
  5. }

測量完毕后。须要实现 onDraw 。在 View 提供的 Canvas 画板绘制两个图片。 当中 mCurrLeft 是关键,滑动的原理就是不断改变 mCurrLeft 的值,调用 invalidate() 引起重绘,不断又一次运行 onDraw,从而发生位移的改变。

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. canvas.drawBitmap(mSwitchBackground, 0, 0, mPaint);
  4. canvas.drawBitmap(mSlideButton, mCurrLeft, 0, mPaint);
  5. }

处理 onTouchEvent

  1. // 開始位置
  2. int startX;
  3. /**
  4. * event.getX() 基于控件本身
  5. * event.getRawX() 基于整个屏幕
  6. */
  7. @Override
  8. public boolean onTouchEvent(MotionEvent event) {
  9. switch (event.getAction()) {
  10. case MotionEvent.ACTION_DOWN:
  11. startX = (int) event.getX();
  12. break;
  13. case MotionEvent.ACTION_MOVE:
  14. int distance = (int) (event.getX() - startX);
  15. mCurrLeft += distance;
  16. startX = (int) event.getX();
  17. break;
  18. }
  19. // 边界推断,不让滑块滑出边界
  20. if (mCurrLeft < 0){
  21. mCurrLeft = 0;
  22. }
  23. if (mCurrLeft > mMaxLeft){
  24. mCurrLeft = mMaxLeft;
  25. }
  26. // 引起重绘(又一次调用 onDraw 方法)
  27. invalidate();
  28. return true;
  29. }

至此,滑块已经能够做最主要的滑动,基本像样了。

关于 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 不须要

处理状态改变回调

  1. /** 定义接口 */
  2. public interface OnOpenedListener {
  3. void onChecked(View v, boolean isOpened);
  4. }
  5. /** 定义成员变量 */
  6. private onOpenedListener mOpenedkListener;
  7. /** 提供设置回调的方法 */
  8. public void setOnCheckChangedListener(onOpenedListener checkedkListener) {
  9. this.mOpenedkListener = checkedkListener;
  10. }

接着在状态改变的时刻,加入例如以下代码就可以:

onTouchEvent 的 ACTION_UP

  1. //处理回调
  2. if (mOpenedkListener != null){
  3. mOpenedkListener.onChecked(this, isOpen);
  4. }

事件共存

本案例中,我们须要实现的效果是,用户既能点击切换,也能滑动切换。可是我们知道。假设设置了点击事件,而且 onTouchEvent 返回 return super.onTouchEvent(envent)。点击事件必定在 onTouchEventACTION_UP 后运行,因为 onTouchEventonClickListener 共用同一个状态。将导致冲突。

具体表现是:无法将滑块滑到打开位置(一移动。自己主动弹回来)。

这时,我们就须要添加一个变量 moveX,记录用户从手指按下到抬起滑过的距离,假设 moveX <5,则觉得点击,假设 moveX >= 5,则觉得滑动。

代码例如以下:

在initView() 中加入点击事件

  1. setOnClickListener(new OnClickListener() {
  2. @Override
  3. public void onClick(View v) {
  4. if (isClick ){
  5. setStatus(!isOpen);
  6. // 引起重绘(又一次调用 onDraw 方法)
  7. invalidate();
  8. }
  9. }
  10. });
  1. booleal isClick;
  2. @Override
  3. public boolean onTouchEvent(MotionEvent event) {
  4. switch (event.getAction()) {
  5. case MotionEvent.ACTION_DOWN:
  6. startX = (int) event.getX();
  7. break;
  8. case MotionEvent.ACTION_MOVE:
  9. int distance = (int) (event.getX() - startX);
  10. mCurrLeft += distance;
  11. startX = (int) event.getX();
  12. // 移动的距离必须用绝对值。避免往回滑。moveX反向减小
  13. moveX += Math.abs(distance);
  14. break;
  15. case MotionEvent.ACTION_UP:
  16. if (moveX >= 5){
  17. isClick = false;
  18. // 用户的本意是滑动
  19. setStatus(mCurrLeft >= mMaxLeft / 2);
  20. //处理回调
  21. if (mOpenedkListener != null){
  22. mOpenedkListener.onChecked(this, isOpen);
  23. }
  24. } else {
  25. isClick = true;
  26. // 用户的本意是点击,交给 onClickListener
  27. }
  28. // 完毕一次滑动。则 moveX 必须清零
  29. moveX = 0;
  30. break;
  31. }
  32. // 边界推断
  33. ...
  34. // 引起重绘
  35. invalidate();
  36. return true;
  37. }

自己定义属性

为了方便用户在 XML 中设置属性。须要加入自己定义属性

attr.xml

  1. <declare-styleable name="SwitchButton">
  2. <attr name="isOpened" format="boolean" />
  3. <attr name="slide_button" format="reference" />
  4. <attr name="switch_background" format="reference" />
  5. </declare-styleable>

命名空间

  1. xmlns:ifavor="http://schemas.android.com/apk/res/res-auto"

定义控件

  1. <com.example.customeview.switchbutton.SwitchButton
  2. android:layout_centerInParent="true"
  3. android:id="@+id/sb_button"
  4. ifavor:isOpened="true"
  5. ifavor:slide_button="@drawable/slide_button"
  6. ifavor:switch_background="@drawable/switch_background"
  7. android:layout_width="wrap_content"
  8. android:layout_height="wrap_content" />

附录

本文演示样例代码:https://github.com/heshiweij/EasySwitchButton

图片资源来源:

SwitchButton 开关按钮 的多种实现方式 (附源代码DEMO)

自己定义控件:onDraw 方法实现仿 iOS 的开关效果的更多相关文章

  1. android--------自定义控件 之 方法篇

    前面简单的讲述了Android中自定义控件的理论和流程图,今天通过代码来详细的讲解一下其中的方法 首先先创建一个类 CircularView 继承于 View,之后实现构造方法(初始化步骤) publ ...

  2. git-osc自己定义控件之:CircleImageView

    git-osc自己定义控件之:CircleImageView 一.CircleImageView的使用 在项目中能够发现,用户的头像都是圆形的.感觉非常好奇,昨天最终发现了,原来是自定了一个Image ...

  3. 【Asp.net之旅】--因自己定义控件注冊而引发的思考

    前言 近期在开发远洋的SOA系统平台,开发使用的是.NET平台.对于Asp.net并不困难,但该系统的开发并非全然依靠Asp.net.而是自身封装好的框架.这套框架是远洋地产购买的微软的开发平台,项目 ...

  4. 【Android】自己定义控件——仿天猫Indicator

    今天来说说类似天猫的Banner中的小圆点是怎么做的(图中绿圈部分) 在学习自己定义控件之前,我用的是很二的方法,直接在布局中放入多个ImageView,然后代码中依据Pager切换来改变图片.这样的 ...

  5. 自己定义控件的onMeasure方法具体解释

    在我们自己定义控件的时候可能你会用到onMeasure方法,以下就具体的给大家介绍一下这种方法: @Override protected void onMeasure(int widthMeasure ...

  6. Android自己定义控件(状态提示图表)

    [工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处.尊重分享成果] 1 背景 前面分析那么多系统源代码了.也该暂停下来歇息一下,趁昨晚闲着看见一个有意思的需求就操 ...

  7. Android自己定义控件系列五:自己定义绚丽水波纹效果

    尊重原创!转载请注明出处:http://blog.csdn.net/cyp331203/article/details/41114551 今天我们来利用Android自己定义控件实现一个比較有趣的效果 ...

  8. Android自己定义控件系列二:自己定义开关button(一)

    这一次我们将会实现一个完整纯粹的自己定义控件,而不是像之前的组合控件一样.拿系统的控件来实现.计划分为三部分:自己定义控件的基本部分,自己定义控件的触摸事件的处理和自己定义控件的自己定义属性: 以下就 ...

  9. 【Android】自己定义控件实现可滑动的开关(switch)

    ~转载请注明来源:http://blog.csdn.net/u013015161/article/details/46704745 介绍 昨天晚上写了一个Android的滑动开关, 即SlideSwi ...

随机推荐

  1. Git学习笔记(2)-创建仓库

    一.Git简介 1.Git是什么 Git是分布式版本控制系统 2.Git有什么特点 (1)Git是分布式的SCM,SVN是集中式的 (2)Git每个历史版本存储完整的文件,SVN存储文件差异 (3)G ...

  2. redis之有序集合类型(Zset)——排行榜的实现

    当数据库对排序支持的不是很好,可以利用redis有序集合排序 原文链接:http://blog.csdn.net/loophome/article/details/50373202

  3. linux mint 18.3设置分辨率死机问题的解决方法

    linux mint 18.3由高分辨率设置为低分辨率的时候,会出现死机现象. 解决方法是:使用命令行: xrandr 查询所有支持的分辨率 然后通过 xrandr -s 1920x1080_59.9 ...

  4. React 篇 Comment Model

    Model 原型 Comment Box <div className="commentBox"> <h1>Comments</h1> < ...

  5. Xamarin绑定ios静态库

    以下是官方的步骤介绍,我就不再一步步解释了 https://docs.microsoft.com/zh-cn/xamarin/ios/platform/binding-objective-c/walk ...

  6. Java怎么实现文件数据拷贝

    实现一个文件的内容拷贝到另一个文件里 public void copyDemo () throws IOException { //创建文件输入流 FileInputStream fis = new ...

  7. Python基础之简介

    参考原文 廖雪峰Python教程 什么是Python? Python是一种计算机程序设计语言,又被称为胶水语言,它是高级的编程语言. Python能干什么? 网站后端程序员.自动化运维.数据分析师.游 ...

  8. CodeForces 【20C】Dijkstra?

    解题思路 heap+Dijkstra就能过.注意边是双向边,要用long long. 附上代码 #include <iostream> #include <queue> #in ...

  9. MFC 多行文本显示心得

    最近在利用MFC做端口扫描器实验,其中涉及CString.char.int等之间的转换.文本框的多行显示问题.总是显示底层最新结果等问题,下面写一些我总结的相关方法. 一.CString 转  cha ...

  10. linux less-分屏上下翻页浏览文件内容

    博主推荐:获取更多 linux文件内容查看命令 收藏:linux命令大全 less命令的作用与more十分相似,都可以用来浏览文字档案的内容,不同的是less命令允许用户向前或向后浏览文件,而more ...