Android自定义View实现开关效果

前言:Android自定义View对于刚入门乃至工作几年的程序员来说都是非常恐惧的,但也是Android进阶学习的必经之路,平时项目中经常会有一些苛刻的需求,我们可以在GitHub上找到各种各样的效果,能用则用,不能用自己花功夫改改也能草草了事。不过随着工作经验和工作性质,越来越觉得自定义View是时候有必要自己花点功夫研究一下。

一、经过这两天的努力,自己也尝试着写了一个Demo,效果很简单,就是开关按钮的实现。
可能有的人会说这效果so easy,找UI切三张图就完事了,何必大费周折自定义。你说的没错,不过这里只是用来学习自定义View来展示这么一个常见案例。

自定义View实现开关按钮

自定义控件

1.为什么自定义View?

  • Android自身带的控件不能满足需求, 需要根据自己的需求定义控件.

2.Android 的界面绘制流程?
这里写图片描述
onMeasure()——onLayout()——onDraw()方法都在Activity生命周期的onResume()方法之后执行。

3.Android自定义View的方式?

  • 集成View:View流程
    onMeasure() (在这个方法里指定自己的宽高) -> onDraw() (绘制自己的内容)
  • 集成ViewGroup:ViewGroup流程
    onMeasure() (指定自己的宽高, 所有子View的宽高)-> onLayout() (摆放所有子View) -> onDraw() (绘制内容)

自定义View实现开关按钮步骤:

  1. 写个类继承View,
  2. 拷贝包含包名的全路径到xml中,
  3. 界面中找到该控件, 设置初始信息,
  4. 根据需求绘制界面内容,
  5. 响应用户的触摸事件,
  6. 创建一个状态更新监听.

1.自定义ToggleView集成View,并且重新三个构造方法。

注意:构造方法为什么要重写三个?

  • ToggleView(Context context)一个参数的构造方法是用于代码创建控件时调用的
  • ToggleView(Context context, AttributeSet attrs)用于在xml里使用, 可指定自定义属性
  • ToggleView(Context context, AttributeSet attrs, int defStyle)用于在xml里使用, 可指定自定义属性, 如果指定了样式, 则走此构造函数

我们在XML中定义了背景图片、开关按钮图片和开关默认状态,要获取在XML文件定义的属性就在包含三个参数的构造方法里用TypedArray类来获取。

在attrs.xml声明节点declare-styleable

<declare-styleable name="ToggleView">
    <attr name="switch_background" format="reference" />
    <attr name="slide_button" format="reference" />
    <attr name="switch_state" format="boolean" />
</declare-styleable>
/**
     * 用于在xml里使用, 可指定自定义属性, 如果指定了样式, 则走此构造函数
     * @param context
     * @param attrs
     * @param defStyle
     */
    public ToggleView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        // 获取配置的自定义属性
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ToggleView, defStyle, 0);
        int switchBackgroundResource = a.getResourceId(R.styleable.ToggleView_switch_background, -1);
        int slideButtonResource = a.getResourceId(R.styleable.ToggleView_slide_button, -1);
        mSwitchState = a.getBoolean(R.styleable.ToggleView_switch_state, false);
        //获取背景图片和开关图片后设置图片,便于在onMeasure()方法中设置View宽和高,防止Null
        setSwitchBackgroundResource(switchBackgroundResource);
        setSlideButtonResource(slideButtonResource);
        init();
    }

2.自定义ToggleView集成View后,在XML文件里不要忘记添加命名空间
“xmlns:cb=”http://schemas.android.com/apk/res-auto””
然后将自定义View的完整路径粘贴到XML中,这点类似于Android v4包下的ViewPager控件
以下便是demo中XML文件代码:

设置开关背景图片

- cb:switch_background=”@drawable/switch_background”

设置开关按钮图片

- cb:slide_button=”@drawable/slide_button”

设置开关默认状态

- cb:switch_state=”false”

这里写图片描述

3.界面中找到该控件, 设置初始信息
在Activity中通过findViewById方法找到自定义的View控件,和系统的组件操作没区别。

private ToggleView toggleView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        toggleView = (ToggleView) findViewById(R.id.toggleView);
//        toggleView.setSwitchBackgroundResource(R.drawable.switch_background);
//        toggleView.setSlideButtonResource(R.drawable.slide_button);
//        toggleView.setSwitchState(true);
//        
        // 设置开关更新监听
        toggleView.setOnSwitchStateUpdateListener(new ToggleView.OnSwitchStateUpdateListener(){

            @Override
            public void onStateUpdate(boolean state) {
                Toast.makeText(getApplicationContext(), "state: " + state, Toast.LENGTH_SHORT).show();
            }

        });
    }

4.根据需求绘制界面内容
已经通过onMeasure()方法设置了View的宽度和高度,下面开始绘制的操作就全部在onDraw()方法中进行,onDraw(Canvas canvas) 方法中canvas参数:画布, 画板. 在上边绘制的内容都会显示到界面上.

// 根据开关状态boolean, 直接设置图片位置
    if(mSwitchState){// 开
        int newLeft = switchBackgroupBitmap.getWidth() - slideButtonBitmap.getWidth();
        canvas.drawBitmap(slideButtonBitmap, newLeft, 0, paint);
    }else {// 关
        canvas.drawBitmap(slideButtonBitmap, 0, 0, paint);
    }
  • 开关打开时,开关按钮的位置在开关背景中的位置计算:

    • int newLeft = switchBackgroupBitmap.getWidth() -
      slideButtonBitmap.getWidth(); 背景的宽度-按钮的宽度就是当前开关按钮所在的X轴上的位置点
  • 开关关闭时,当前开关按钮所在的X轴上的位置点=0

5.响应用户的触摸事件
在完成以上3步操作后,你会发现,只有在第一次进入后XML初始化默认开关状态的boolean值才会有变化,此后点击是没有任何效果的,这个时候我们就要想办法监听手势事件,重写onTouchEvent(MotionEvent event)方法,相信大多数朋友对这个方法并不陌生。
MotionEvent有三种状态:

  • MotionEvent.ACTION_DOWN: //按下屏幕
  • MotionEvent.ACTION_MOVE: //手指在屏幕上移动
  • MotionEvent.ACTION_UP //离开屏幕

当前需要考虑的问题是:

  1. 当手指按下屏幕后MotionEvent.ACTION_DOWN(在当前开关背景View中)开关的X轴位置应该移动到手指按下的位置;
  2. 当手指在屏幕上移动MotionEvent.ACTION_MOVE(在当前开关背景View中)开关按钮X轴应该随着手指移动的位置改变;
  3. 当手指离开屏幕后MotionEvent.ACTION_UP(在当前开关背景View中)开关按钮应该判断手指离开的位置是否是当前背景的一半位置,如果X轴位置大于View背景宽度的1/2、那么应该处于打开状态,如果X轴位置小于View背景宽度的1/2,那么应该处于关闭状态。如图所示:
    这里写图片描述
private OnSwitchStateUpdateListener onSwitchStateUpdateListener;
    // 重写触摸事件, 响应用户的触摸.
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            isTouchMode = true;
            System.out.println("event: ACTION_DOWN: " + event.getX());
            currentX = event.getX();
            break;
        case MotionEvent.ACTION_MOVE:
            System.out.println("event: ACTION_MOVE: " + event.getX());
            currentX = event.getX();
            break;
        case MotionEvent.ACTION_UP:
            isTouchMode = false;
            System.out.println("event: ACTION_UP: " + event.getX());
            currentX = event.getX();

            float center = switchBackgroupBitmap.getWidth() / 2.0f;

            // 根据当前按下的位置, 和控件中心的位置进行比较. 
            boolean state = currentX > center;

            // 如果开关状态变化了, 通知界面. 里边开关状态更新了.
            if(state != mSwitchState && onSwitchStateUpdateListener != null){
                // 把最新的boolean, 状态传出去了
                onSwitchStateUpdateListener.onStateUpdate(state);
            }

            mSwitchState = state;
            break;

        default:
            break;
        }

        // 重绘界面
        invalidate(); // 会引发onDraw()被调用, 里边的变量会重新生效.界面会更新

        return true; // 消费了用户的触摸事件, 才可以收到其他的事件.
    }

注意:
以上监听onTouchEvent(MotionEvent
event)方法后还存在一个问题,不知道大家有没有发现,我们没有设置开关按钮的边界值,什么意思呢?就是手指滑动的时候左边和右边可以画出当前背景之外。
所以这里需要对左右两边的X轴位置进行处理:

// Canvas 画布, 画板. 在上边绘制的内容都会显示到界面上.
    @Override
    protected void onDraw(Canvas canvas) {
        // 1. 绘制背景
        canvas.drawBitmap(switchBackgroupBitmap, 0, 0, paint);
        // 2. 绘制滑块
        if(isTouchMode){
            // 根据当前用户触摸到的位置画滑块

            // 让滑块向左移动自身一半大小的位置
            float newLeft = currentX - slideButtonBitmap.getWidth() / 2.0f;

            int maxLeft = switchBackgroupBitmap.getWidth() - slideButtonBitmap.getWidth();

            // 限定滑块范围
            if(newLeft < 0){
                newLeft = 0; // 左边范围
            }else if (newLeft > maxLeft) {
                newLeft = maxLeft; // 右边范围
            }

            canvas.drawBitmap(slideButtonBitmap, newLeft, 0, paint);
        }else {
            // 根据开关状态boolean, 直接设置图片位置
            if(mSwitchState){// 开
                int newLeft = switchBackgroupBitmap.getWidth() - slideButtonBitmap.getWidth();
                canvas.drawBitmap(slideButtonBitmap, newLeft, 0, paint);
            }else {// 关
                canvas.drawBitmap(slideButtonBitmap, 0, 0, paint);
            }
        }

    }

6.创建一个状态更新监听.
基本上所以工作已经完成,这样我们一个自定义View已经大功告成了,当你完成这个效果后,你可能会发现有点类似于CheckBox。既然类似于CheckBox,我们知道当CheckBox点击选中和取消选中的时候都会有ischecked()方法来获取选中状态,所以我们这个自定义的开关按钮自然不能少这个功能,否则我们在界面上只有效果展示,却没有逻辑处理的地方。

public interface OnSwitchStateUpdateListener{
        // 状态回调, 把当前状态传出去
        void onStateUpdate(boolean state);
    }

public void setOnSwitchStateUpdateListener(
        OnSwitchStateUpdateListener onSwitchStateUpdateListener) {
            this.onSwitchStateUpdateListener = onSwitchStateUpdateListener;
    }

代码很简单,写一个接口,然后定义一个回调方法返回开关状态,需要注意的是,在手指离开屏幕的时候,我们需要判断此次操作是否改变了开关的状态,如果没有变化我们不做操作,如果跟上次状态不同,则通知Activity状态更改!

case MotionEvent.ACTION_UP:
    isTouchMode = false;
    System.out.println("event: ACTION_UP: " + event.getX());
    currentX = event.getX();    
    float center = switchBackgroupBitmap.getWidth() / 2.0f;
    // 根据当前按下的位置, 和控件中心的位置进行比较. 
    boolean state = currentX > center;

    // 如果开关状态变化了, 通知界面. 里边开关状态更新了.
    if(state != mSwitchState && onSwitchStateUpdateListener != null){
        // 把最新的boolean, 状态传出去了
        onSwitchStateUpdateListener.onStateUpdate(state);
    }

    mSwitchState = state;
    break;

Activity中回调也是非常简单的,类似于Android系统为我们提供的setOnClickListener回调接口,之前一直用系统定义的监听接口,这次通过简单的一个自定义View,我们也可以给自己的View写回调接口了。是不是觉得是件很开心的事情呢?

// 设置开关更新监听
toggleView.setOnSwitchStateUpdateListener(new ToggleView.OnSwitchStateUpdateListener(){
    @Override
    public void onStateUpdate(boolean state) {
        Toast.makeText(getApplicationContext(), "state: " + state, Toast.LENGTH_SHORT).show();
    }
});

源码下载地址

http://download.csdn.net/detail/jaynm/9635382

发布了37 篇原创文章 · 获赞 35 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/jaynm/article/details/52601935