自定义控件之滑动开关按钮

对于Android的自定义控件是自己一直想研究总结的,所以未来会从基础开始,一点点来学习一些自定义控件的效果,这些知识并非完全自己来研究的,但是是自己学习成长的点滴记录,重在搞懂原理,言归正传~

这次要实现的滑动开关按钮的效果如下:

【说明】:之后所有的自定义效果的学习文章都是先上效果图之后,然后再一步步从无到有的去实现。

从效果图中可以发现,这是一个"很简单"的开关按钮控件,也就是平常使用的CheckBox的效果,但是又要比CheckBox控件要多出一个效果,就是该控件支持滑动,而且这个效果是从无到有一点点自定义出来的,也就是自己动手实现一个类似于CheckBox的效果,所以其实也不是很简单,麻雀虽小五脏俱全,通过这个例子来熟知自定义控件的整个过程,下面则一点点来实现它。

首先控件需要这两张图片素材:

             

自定义过CheckBox的都清楚,图片的高度是一样大小的:

新建一个工程,然后将这两个资源文件放入到工程中:

然后新建一个自定义View,如下:

其中需要重写构造方法:

其中只需要重写两个构造方法既可:

/**
 * 自定义滑动开关view
 */
public class MyToggleButton extends View {

    /**
     * 在代码里面创建对象的时候,使用此构造方法
     */
    public MyToggleButton(Context context) {
        super(context);
    }

    /**
     * 在布局文件中声名的view,创建时由系统自动调用
     */
    public MyToggleButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

}

而从上面的代码注释中可以发现这两个构造方法是有区分的,有必要知道一下,第一个是类似于这种使用场景:

而第二个构造是在布局文件中定义该View,由系统去调用的,而不是我们去调的,也就是这样:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.togglebutton.MainActivity" >

    <com.example.togglebutton.MyToggleButton
        android:id="@+id/toggle_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

此时,如果我们将第二个构造方法注释掉,就会报错:

/**
 * 自定义滑动开关view
 */
public class MyToggleButton extends View {

    /**
     * 在代码里面创建对象的时候,使用此构造方法
     */
    public MyToggleButton(Context context) {
        super(context);
    }

    /**
     * 在布局文件中声名的view,创建时由系统自动调用
     */
//    public MyToggleButton(Context context, AttributeSet attrs) {
//        super(context, attrs);
//    }

}

运行看效果:

所以对于这两个构造的含义就清楚了,将注释的代码还原。

接下来绘制View,在正式绘制之前,需要把View对象要能显示在屏幕上的几个步骤说明一下,相当于中心思想,抓住了中心思想代码写起来就能有的放矢,如下:

①、调用构造方法,创建对象。

②、测量View的大小,在绘制之前是需要先确定View的大小的,对应的方法是onMeasure(int , int)。

③、确定View的位置,View自身有一些建议权,决定权在父View手中,onLayout(),这个方法由于是由ViewGoup决定的,所以对于View一般没用,不会重写它。

④、绘制View的内容。对应的方法是onDraw(Canvas)。

下面则遵照上面的步骤一一去实现,第一步已经定义了,接着来实现第二步,重写onMeasure方法:

public class MyToggleButton extends View {

    /**
     * 在代码里面创建对象的时候,使用此构造方法
     */
    public MyToggleButton(Context context) {
        super(context);
    }

    /**
     * 在布局文件中声名的view,创建时由系统自动调用
     */
    public MyToggleButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

}

而它的大小应该是来背景图片来决定的,所以需要把图片加载进来:

public class MyToggleButton extends View {

    /** 做为背景的图片 **/
    private Bitmap backgroundBitmap;
    /** 可以滑动的图片 **/
    private Bitmap slideButtonBitmap;

    /**
     * 在代码里面创建对象的时候,使用此构造方法
     */
    public MyToggleButton(Context context) {
        super(context);
    }

    /**
     * 在布局文件中声名的view,创建时由系统自动调用
     */
    public MyToggleButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.switch_background);
        slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.slide_button);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

}

接着来实现onMeasure方法,先来查看一下它的父类的实现:

所以我们也依葫芦画瓢:

public class MyToggleButton extends View {

    /** 做为背景的图片 **/
    private Bitmap backgroundBitmap;
    /** 可以滑动的图片 **/
    private Bitmap slideButtonBitmap;

    /**
     * 在代码里面创建对象的时候,使用此构造方法
     */
    public MyToggleButton(Context context) {
        super(context);
    }

    /**
     * 在布局文件中声名的view,创建时由系统自动调用
     */
    public MyToggleButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.switch_background);
        slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.slide_button);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 设置当前View的大小,以背景图为大小,单位都是像素
        setMeasuredDimension(backgroundBitmap.getWidth(),
                backgroundBitmap.getHeight());
    }

}

接下来到第三部:确认View的位置,由于View本身决定不了,而是由它的父控件决定的,所以不用管这个方法,这里重写只用来观察方法:

public class MyToggleButton extends View {

    /** 做为背景的图片 **/
    private Bitmap backgroundBitmap;
    /** 可以滑动的图片 **/
    private Bitmap slideButtonBitmap;

    /**
     * 在代码里面创建对象的时候,使用此构造方法
     */
    public MyToggleButton(Context context) {
        super(context);
    }

    /**
     * 在布局文件中声名的view,创建时由系统自动调用
     */
    public MyToggleButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.switch_background);
        slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.slide_button);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 设置当前View的大小,以背景图为大小,单位都是像素
        setMeasuredDimension(backgroundBitmap.getWidth(),
                backgroundBitmap.getHeight());
    }

    /**
     * 确定位置的时候调用此方法,自定义View的时候作用不大
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

}

要显示在屏幕上还差最后一步了,重写onDraw()方法:

所以super可以直接删掉,下面来开始将图片绘制在画布上:

其中第四个参数中需要一个paint对象,所以先初始化一个:

public class MyToggleButton extends View {

    /** 做为背景的图片 **/
    private Bitmap backgroundBitmap;
    /** 可以滑动的图片 **/
    private Bitmap slideButtonBitmap;
    private Paint paint;

    /**
     * 在代码里面创建对象的时候,使用此构造方法
     */
    public MyToggleButton(Context context) {
        super(context);
    }

    /**
     * 在布局文件中声名的view,创建时由系统自动调用
     */
    public MyToggleButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.switch_background);
        slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.slide_button);

        paint = new Paint();
        paint.setAntiAlias(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 设置当前View的大小,以背景图为大小,单位都是像素
        setMeasuredDimension(backgroundBitmap.getWidth(),
                backgroundBitmap.getHeight());
    }

    /**
     * 确定位置的时候调用此方法,自定义View的时候作用不大
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
    }
}

下面来绘制图片:

【说明】:关于View的绘制这里不多讲,之后会不断去研究它的,这里先直接用。

这时先来运行看下初步的效果:

接下来,实现点击可以进行开关按钮的切换效果,先来思考一下怎么来实现:

所以这时的这个参数需要声明成一个变量动态去改变:

public class MyToggleButton extends View {

    /** 做为背景的图片 **/
    private Bitmap backgroundBitmap;
    /** 可以滑动的图片 **/
    private Bitmap slideButtonBitmap;
    private Paint paint;
    /** 滑动按钮的左边距 **/
    private float slideButtonLeft;

    /**
     * 在代码里面创建对象的时候,使用此构造方法
     */
    public MyToggleButton(Context context) {
        super(context);
    }

    /**
     * 在布局文件中声名的view,创建时由系统自动调用
     */
    public MyToggleButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.switch_background);
        slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.slide_button);

        paint = new Paint();
        paint.setAntiAlias(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 设置当前View的大小,以背景图为大小,单位都是像素
        setMeasuredDimension(backgroundBitmap.getWidth(),
                backgroundBitmap.getHeight());
    }

    /**
     * 确定位置的时候调用此方法,自定义View的时候作用不大
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 先绘制背景图
        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
        // 再绘制滑动按钮
        canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint);
    }
}

接着给当前View增加点击事件,然后处理点击逻辑:

public class MyToggleButton extends View implements OnClickListener {

    /** 做为背景的图片 **/
    private Bitmap backgroundBitmap;
    /** 可以滑动的图片 **/
    private Bitmap slideButtonBitmap;
    private Paint paint;
    /** 滑动按钮的左边距 **/
    private float slideButtonLeft;
    /** 当前开关的状态,true为开,false为关 **/
    private boolean currentToggleSate;

    /**
     * 在代码里面创建对象的时候,使用此构造方法
     */
    public MyToggleButton(Context context) {
        super(context);
    }

    /**
     * 在布局文件中声名的view,创建时由系统自动调用
     */
    public MyToggleButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.switch_background);
        slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.slide_button);

        paint = new Paint();
        paint.setAntiAlias(true);

        setOnClickListener(this);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 设置当前View的大小,以背景图为大小,单位都是像素
        setMeasuredDimension(backgroundBitmap.getWidth(),
                backgroundBitmap.getHeight());
    }

    /**
     * 确定位置的时候调用此方法,自定义View的时候作用不大
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 先绘制背景图
        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
        // 再绘制滑动按钮
        canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint);
    }

    @Override
    public void onClick(View v) {
        currentToggleSate = !currentToggleSate;
        flushState();
    }

    /**
     * 刷新当前状态
     */
    private void flushState() {
        if (currentToggleSate) {
            slideButtonLeft = backgroundBitmap.getWidth()
                    - slideButtonBitmap.getWidth();
        } else {
            slideButtonLeft = 0;
        }
        invalidate();
    }
}

编译运行看效果:

接下来实现最后一个功能,也是相对而言最复杂的,也就是支持滑动切换,怎么做呢?当然是要监听它的touch事件喽:

这个滑动切换的第一步,就是这个SlideButton能够随着手指滑动,所以先来实现它:

public class MyToggleButton extends View implements OnClickListener {

    /** 做为背景的图片 **/
    private Bitmap backgroundBitmap;
    /** 可以滑动的图片 **/
    private Bitmap slideButtonBitmap;
    private Paint paint;
    /** 滑动按钮的左边距 **/
    private float slideButtonLeft;
    /** 当前开关的状态,true为开,false为关 **/
    private boolean currentToggleSate;
    /** down 事件时的x值 **/
    private int firstX;
    /** touch 事件时上一个x值 **/
    private int lastX;

    /**
     * 在代码里面创建对象的时候,使用此构造方法
     */
    public MyToggleButton(Context context) {
        super(context);
    }

    /**
     * 在布局文件中声名的view,创建时由系统自动调用
     */
    public MyToggleButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.switch_background);
        slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.slide_button);

        paint = new Paint();
        paint.setAntiAlias(true);

        setOnClickListener(this);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 设置当前View的大小,以背景图为大小,单位都是像素
        setMeasuredDimension(backgroundBitmap.getWidth(),
                backgroundBitmap.getHeight());
    }

    /**
     * 确定位置的时候调用此方法,自定义View的时候作用不大
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 先绘制背景图
        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
        // 再绘制滑动按钮
        canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint);
    }

    @Override
    public void onClick(View v) {
        currentToggleSate = !currentToggleSate;
        flushState();
    }

    /**
     * 刷新当前状态
     */
    private void flushState() {
        if (currentToggleSate) {
            slideButtonLeft = backgroundBitmap.getWidth()
                    - slideButtonBitmap.getWidth();
        } else {
            slideButtonLeft = 0;
        }
        invalidate();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            firstX = lastX = (int) event.getX();
            break;
        case MotionEvent.ACTION_MOVE:
            int currentX = (int) event.getX();
            // 算出移动的距离
            int moveDistance = currentX - lastX;
            // 并把当前的x值缓存起来,但计算下次移动的距离
            lastX = currentX;
            // 然后根据移动位置来动态改变slideButtonLeft
            slideButtonLeft = slideButtonLeft + moveDistance;
            break;
        case MotionEvent.ACTION_UP:
            break;
        }
        invalidate();
        return true;
    }
}

运行看下效果:

随着手指移动倒没啥问题了,但是发现移动时没有做位置限制,应该不允许滑出背景,所以接下来需要做一下判断,也就是在触摸刷新前需要判断一下:

public class MyToggleButton extends View implements OnClickListener {

    /** 做为背景的图片 **/
    private Bitmap backgroundBitmap;
    /** 可以滑动的图片 **/
    private Bitmap slideButtonBitmap;
    private Paint paint;
    /** 滑动按钮的左边距 **/
    private float slideButtonLeft;
    /** 当前开关的状态,true为开,false为关 **/
    private boolean currentToggleSate;
    /** down 事件时的x值 **/
    private int firstX;
    /** touch 事件时上一个x值 **/
    private int lastX;

    /**
     * 在代码里面创建对象的时候,使用此构造方法
     */
    public MyToggleButton(Context context) {
        super(context);
    }

    /**
     * 在布局文件中声名的view,创建时由系统自动调用
     */
    public MyToggleButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.switch_background);
        slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.slide_button);

        paint = new Paint();
        paint.setAntiAlias(true);

        setOnClickListener(this);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 设置当前View的大小,以背景图为大小,单位都是像素
        setMeasuredDimension(backgroundBitmap.getWidth(),
                backgroundBitmap.getHeight());
    }

    /**
     * 确定位置的时候调用此方法,自定义View的时候作用不大
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 先绘制背景图
        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
        // 再绘制滑动按钮
        canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint);
    }

    @Override
    public void onClick(View v) {
        currentToggleSate = !currentToggleSate;
        flushState();
    }

    /**
     * 刷新当前状态
     */
    private void flushState() {
        if (currentToggleSate) {
            slideButtonLeft = backgroundBitmap.getWidth()
                    - slideButtonBitmap.getWidth();
        } else {
            slideButtonLeft = 0;
        }
        flushView();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            firstX = lastX = (int) event.getX();
            break;
        case MotionEvent.ACTION_MOVE:
            int currentX = (int) event.getX();
            // 算出移动的距离
            int moveDistance = currentX - lastX;
            // 并把当前的x值缓存起来,但计算下次移动的距离
            lastX = currentX;
            // 然后根据移动位置来动态改变slideButtonLeft
            slideButtonLeft = slideButtonLeft + moveDistance;
            break;
        case MotionEvent.ACTION_UP:
            break;
        }
        flushView();
        return true;
    }

    private void flushView() {
        // 对slideButtonLeft的值进行判断,确保滑动时只能在合理的范围内:0<=slideButtonLeft<=maxleft
        int maxLeft = backgroundBitmap.getWidth()
                - slideButtonBitmap.getWidth();
        // 确保slideButtonLeft>=0
        slideButtonLeft = slideButtonLeft > 0 ? slideButtonLeft : 0;
        // 确保slideButtonLeft<=maxleft
        slideButtonLeft = slideButtonLeft < maxLeft ? slideButtonLeft : maxLeft;
        invalidate();
    }
}

其区域判断的核心就是:

再次运行:

从结果来看已经加入了区域限制,这时滑不出背景区域了,接下来还有一个问题需要处理一下,就是关于滑动事件与onClick冲突的问题,下面来看下:

首先给onClick()方法上加上一条log用来观察呆会的实验:

下面运行,对于onClick()事件的触发,就是称按下鼠标,最后松开鼠标这样就构成了一个点击事件,那如果按下鼠标不松手,然后进行滑动,最后再松开鼠标也会触发onClick()事件么,用实验来证明下:

可见系统的这种onClick()触发行为不是我们想要的,我们想要的是如果发生了滑动,则就不触发onClick()了,所以下面需要进行一个逻辑判断来避免这个问题:

public class MyToggleButton extends View implements OnClickListener {

    /** 做为背景的图片 **/
    private Bitmap backgroundBitmap;
    /** 可以滑动的图片 **/
    private Bitmap slideButtonBitmap;
    private Paint paint;
    /** 滑动按钮的左边距 **/
    private float slideButtonLeft;
    /** 当前开关的状态,true为开,false为关 **/
    private boolean currentToggleSate;
    /** down 事件时的x值 **/
    private int firstX;
    /** touch 事件时上一个x值 **/
    private int lastX;
    /** 判断是否发生拖动,如果拖动了,则不响应onClick事件 **/
    private boolean isDrag;

    /**
     * 在代码里面创建对象的时候,使用此构造方法
     */
    public MyToggleButton(Context context) {
        super(context);
    }

    /**
     * 在布局文件中声名的view,创建时由系统自动调用
     */
    public MyToggleButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.switch_background);
        slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.slide_button);

        paint = new Paint();
        paint.setAntiAlias(true);

        setOnClickListener(this);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 设置当前View的大小,以背景图为大小,单位都是像素
        setMeasuredDimension(backgroundBitmap.getWidth(),
                backgroundBitmap.getHeight());
    }

    /**
     * 确定位置的时候调用此方法,自定义View的时候作用不大
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 先绘制背景图
        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
        // 再绘制滑动按钮
        canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint);
    }

    @Override
    public void onClick(View v) {
        if (isDrag) {
            // 如果发生了拖动,则不响应点击事件了
            return;
        }
        Log.d("cexo", "onClick()");
        currentToggleSate = !currentToggleSate;
        flushState();
    }

    /**
     * 刷新当前状态
     */
    private void flushState() {
        if (currentToggleSate) {
            slideButtonLeft = backgroundBitmap.getWidth()
                    - slideButtonBitmap.getWidth();
        } else {
            slideButtonLeft = 0;
        }
        flushView();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            firstX = lastX = (int) event.getX();
            break;
        case MotionEvent.ACTION_MOVE:
            int currentX = (int) event.getX();
            // 算出移动的距离
            int moveDistance = currentX - lastX;
            // 并把当前的x值缓存起来,但计算下次移动的距离
            lastX = currentX;
            // 然后根据移动位置来动态改变slideButtonLeft
            slideButtonLeft = slideButtonLeft + moveDistance;
            break;
        case MotionEvent.ACTION_UP:
            break;
        }
        flushView();
        return true;
    }

    private void flushView() {
        // 对slideButtonLeft的值进行判断,确保滑动时只能在合理的范围内:0<=slideButtonLeft<=maxleft
        int maxLeft = backgroundBitmap.getWidth()
                - slideButtonBitmap.getWidth();
        // 确保slideButtonLeft>=0
        slideButtonLeft = slideButtonLeft > 0 ? slideButtonLeft : 0;
        // 确保slideButtonLeft<=maxleft
        slideButtonLeft = slideButtonLeft < maxLeft ? slideButtonLeft : maxLeft;
        invalidate();
    }
}

那问题的关键来了,判断是否是拖动的界限是?其实可以这样来认为:如果从ACTION_DOWN到ACTION_MOVE这两点的位置超过了5px,则认为是滑动,所以判断代码如下:

下面再来运行看下是否对滑动和点击做了明显的区分:

从结果来看,当拖动时再松手,则就没有走onClick的逻辑了,而是停到了我们滑动的位置不动了,也就达到了我们的目的。

接下来就要处理滑动切换的效果了,那切换的界限在哪呢?

所以,根据上图的描述,开关的判断也很简单了,具体代码如下:

public class MyToggleButton extends View implements OnClickListener {

    /** 做为背景的图片 **/
    private Bitmap backgroundBitmap;
    /** 可以滑动的图片 **/
    private Bitmap slideButtonBitmap;
    private Paint paint;
    /** 滑动按钮的左边距 **/
    private float slideButtonLeft;
    /** 当前开关的状态,true为开,false为关 **/
    private boolean currentToggleSate;
    /** down 事件时的x值 **/
    private int firstX;
    /** touch 事件时上一个x值 **/
    private int lastX;
    /** 判断是否发生拖动,如果拖动了,则不响应onClick事件 **/
    private boolean isDrag;

    /**
     * 在代码里面创建对象的时候,使用此构造方法
     */
    public MyToggleButton(Context context) {
        super(context);
    }

    /**
     * 在布局文件中声名的view,创建时由系统自动调用
     */
    public MyToggleButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.switch_background);
        slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.slide_button);

        paint = new Paint();
        paint.setAntiAlias(true);

        setOnClickListener(this);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 设置当前View的大小,以背景图为大小,单位都是像素
        setMeasuredDimension(backgroundBitmap.getWidth(),
                backgroundBitmap.getHeight());
    }

    /**
     * 确定位置的时候调用此方法,自定义View的时候作用不大
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 先绘制背景图
        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
        // 再绘制滑动按钮
        canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint);
    }

    @Override
    public void onClick(View v) {
        if (isDrag) {
            // 如果发生了拖动,则不响应点击事件了
            return;
        }
        Log.d("cexo", "onClick()");
        currentToggleSate = !currentToggleSate;
        flushState();
    }

    /**
     * 刷新当前状态
     */
    private void flushState() {
        if (currentToggleSate) {
            slideButtonLeft = backgroundBitmap.getWidth()
                    - slideButtonBitmap.getWidth();
        } else {
            slideButtonLeft = 0;
        }
        flushView();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            isDrag = false;
            firstX = lastX = (int) event.getX();
            break;
        case MotionEvent.ACTION_MOVE:
            int currentX = (int) event.getX();
            // 判断是否发生拖动
            if (Math.abs(currentX - firstX) > 5) {
                isDrag = true;
            }
            // 算出移动的距离
            int moveDistance = currentX - lastX;
            // 并把当前的x值缓存起来,但计算下次移动的距离
            lastX = currentX;
            // 然后根据移动位置来动态改变slideButtonLeft
            slideButtonLeft = slideButtonLeft + moveDistance;
            break;
        case MotionEvent.ACTION_UP:
            if (isDrag) {
                int maxLeft = backgroundBitmap.getWidth()
                        - slideButtonBitmap.getWidth();
                // 根据slideButtonLeft来判断当前应该是什么状态(开,关)
                if (slideButtonLeft > maxLeft / 2) {
                    // 开状态
                    currentToggleSate = true;
                } else {
                    currentToggleSate = false;
                }

                flushState();
            }
            break;
        }
        flushView();
        return true;
    }

    private void flushView() {
        // 对slideButtonLeft的值进行判断,确保滑动时只能在合理的范围内:0<=slideButtonLeft<=maxleft
        int maxLeft = backgroundBitmap.getWidth()
                - slideButtonBitmap.getWidth();
        // 确保slideButtonLeft>=0
        slideButtonLeft = slideButtonLeft > 0 ? slideButtonLeft : 0;
        // 确保slideButtonLeft<=maxleft
        slideButtonLeft = slideButtonLeft < maxLeft ? slideButtonLeft : maxLeft;
        invalidate();
    }
}

运行看下效果:

虽说这个例子很简单,但是实际上涉及了自定义一个View的一个大致过程,之后会不断对其进行研究。

posted on 2015-07-06 21:26  cexo  阅读(758)  评论(0编辑  收藏  举报

导航