自定义view相关Android自学笔记自定义控件

Android最易懂的自定义View讲解

2018-09-06  本文已影响231人  Android_Jieyao

前言: 最近开发的时候, 频繁的需要使用到自定义控件。自定义控件是成为高级工程师必不可少的条件之一,所以今天决定认真总结一下。其实自定义控件也没有想象中的那么复杂,无非只要掌握其中的几个关键方法就能满足绝大部分需求。但是若要真的要深入进去,都能写一本书了,这里就不做那么深入了。能满足日常的需求即可, 想深入了解的可自行查阅其他资料进行学习。

在学习本篇自定义View之前,读者有必要先学习一下View的绘制流程,这样才能更好的理解文字的内容。必知必会 | 面试官装逼失败之View的绘制流程

首先我们要明白,为什么要自定义View?主要是Android系统内置的View无法实现我们的需求,我们需要针对我们的业务需求定制我们想要的View。简单来说自定义控件无非就两种,自定义View和自定义ViewGroup:

1. 自定义View

自定义View的话我们大部分时候只需重写两个函数:onMeasure()onDraw()。onMeasure负责对当前View的尺寸进行测量,onDraw负责把当前这个View绘制出来。当然了,你还得写至少写2个构造函数:

    // 一个参数的构造方法,在代码中创建该控件时,调用该构造方法
    public MyView(Context context) {
        super(context);
    }
  
    // 在xml 中引用该控件时,调用该方法。attrs是定义在xml布局中的属性集合
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs); 
    }

1.1 重写onMeasure

我们自定义View,首先得要测量宽高尺寸。为什么要测量宽高尺寸?有的人要问了,我不是在xml文件中已经指定好了宽高尺寸了吗, 我自定义的View有必要再一次获取宽高去设置宽高吗?既然我自定义的View是继承自View类,google团队直接在View类中直接把xml设置的宽高获取,并且设置进去不就好了吗?为什么要让我们自己来做,真可恨!别着急,既然google让我们做这样的“重复工作”,自然有他的道理。

在学习Android的时候,我们就知道,在xml布局文件中,我们的layout_widthlayout_height参数可以不用写具体的尺寸,而是wrap_content或者是match_parent。其意思我们都知道,就是将尺寸设置为“包住内容”和“填充父布局给我们的所有空间”。这两个设置并没有指定真正的大小,可是我们绘制到屏幕上的View必须是要有具体的宽高的,这回知道了吧?并不是所有情况下我们都会给某个View特定的尺寸的。正是因为这个原因,我们必须自己去处理和设置尺寸。当然了,View类给了默认的处理,但是如果View类的默认处理不满足我们的要求,我们就得重写onMeasure函数啦。这里举个例子,比如我们希望我们的View是个正方形,如果在xml中指定宽高为wrap_content,如果使用View类提供的measure处理方式,显然无法满足我们的需求。

关于onMeasure函数的源码解析,我已经在上一篇文章中做了详细的解释了,不了解的请移步必知必会 | 面试官装逼失败之View的绘制流程。了解了onMeaSure方法的实现原理,在自定义View时我们需要对其进行重写。

讲了太多理论,我们来实际操作一下吧,感受一下onMeasure的使用,现在假设我们要实现这样一个效果:将当前的View以正方形的形式显示,即要宽高相等,并且默认的宽高值为100像素。代码如下:

// defaultSize 默认尺寸,这里为100像素
// measureSpec 测量规格
private int getSize(int defaultSize, int measureSpec) {
        int mySize = defaultSize;
        
        // 测量模式
        int mode = MeasureSpec.getMode(measureSpec);
        // 测量尺寸
        int size = MeasureSpec.getSize(measureSpec);

        switch (mode) {
            case MeasureSpec.UNSPECIFIED: {//如果没有指定大小,就设置为默认大小
                mySize = defaultSize;
                break;
            }
            case MeasureSpec.AT_MOST: {//如果测量模式是最大取值为size
                //我们将大小取最大值,你也可以取其他值
                mySize = size;
                break;
            }
            case MeasureSpec.EXACTLY: {//如果是固定的大小,那就不要去改变它
                mySize = size;
                break;
            }
        }
        return mySize;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getSize(100, widthMeasureSpec);
        int height = getSize(100, heightMeasureSpec);

        if (width < height) {
            height = width;
        } else {
            width = height;
        }
      
        // 设置测量之后的参数
        setMeasuredDimension(width, height);
}

布局中使用它:

<com.jieyao.test.MyView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#ff0000" />

使用了我们自己定义的onMeasure函数后的效果:

正方形显示View

而如果我们不重写onMeasure,效果则是如下:

未重写onMeasure的效果

显然重写之后按照了我们意愿去显示的,实现了我们的需求。

1.2 重写onDraw

上面我们学会了自定义尺寸大小,尺寸我们会设定了,接下来就是把我们想要的效果画出来吧~绘制我们想要的效果很简单,直接在画板Canvas对象上绘制就好啦,逻辑过于简单,我们以一个简单的例子去学习:假设我们需要实现的是,我们的View显示一个圆形,我们在上面已经实现了宽高尺寸相等的基础上,继续往下做:

@Override
    protected void onDraw(Canvas canvas) {
        //调用父View的onDraw函数,因为View这个类帮我们实现了一些
        // 基本的而绘制功能,比如绘制背景颜色、背景图片等
        super.onDraw(canvas);
       //也可以是getMeasuredHeight()/2。
       //本例中我们已经将宽高设置相等了。
        int r = getMeasuredWidth() / 2;
        //圆心的横坐标为当前的View的左边起始位置+半径
        int centerX = getLeft() + r;
        //圆心的纵坐标为当前的View的顶部起始位置+半径
        int centerY = getTop() + r;

        Paint paint = new Paint();
        paint.setColor(Color.GREEN);
        //绘制圆形
        canvas.drawCircle(centerX, centerY, r, paint);
    }

效果图如下:

圆形显示

1.3 自定义属性

有时候有些属性我们希望由用户指定,只有当用户不指定的时候才用我们硬编码的值,比如上面的默认尺寸,我们想要由用户自己在布局文件里面指定该怎么做呢?那当然是通我们自定属性,让用户用我们定义的属性啦~

<resources>
    <!--name为声明的"属性集合"名,可以随便取,但是最好是设置为跟我们的View一样的名称-->
    <declare-styleable name="MyView">
        <!--声明我们的属性,名称为default_size,取值类型为尺寸类型(dp,px等)-->
        <attr name="default_size" format="dimension" />
    </declare-styleable>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:jieyao="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.jieyao.test.MyView
        android:layout_width="match_parent"
        android:layout_height="100dp"
       jieyao:default_size="100dp" />

</LinearLayout>

注意:需要在根标签(LinearLayout)里面设定命名空间,命名空间名称可以随便取,比如 jieyao,命名空间后面取的值是固定的:"http://schemas.android.com/apk/res-auto"

  private int defalutSize;

  public MyView(Context context, AttributeSet attrs) {
      super(context, attrs);
        //第二个参数就是我们在attrs.xml文件中的<declare-styleable>标签
        //即属性集合的标签,在R文件中名称为R.styleable.name
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);
        
        //第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
        //第二个参数为,如果没有设置这个属性,则设置的默认的值
        defalutSize = a.getDimensionPixelSize(R.styleable.MyView_default_size, 100);
        
        //最后记得将TypedArray对象回收
        a.recycle();
   }

最后,把MyView的完整代码附上:

package com.jieyao.test;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class MyView extends View {

    private int defalutSize;

    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
        //即属性集合的标签,在R文件中名称为R.styleable.name
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);
        //第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
        //第二个参数为,如果没有设置这个属性,则设置的默认的值
        defalutSize = a.getDimensionPixelSize(R.styleable.MyView_default_size, 100);
        //最后记得将TypedArray对象回收
        a.recycle();
    }

    private int getSize(int defaultSize, int measureSpec) {
        int mySize = defaultSize;

        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);

        switch (mode) {
            case MeasureSpec.UNSPECIFIED: {//如果没有指定大小,就设置为默认大小
                mySize = defaultSize;
                break;
            }
            case MeasureSpec.AT_MOST: {//如果测量模式是最大取值为size
                //我们将大小取最大值,你也可以取其他值
                mySize = size;
                break;
            }
            case MeasureSpec.EXACTLY: {//如果是固定的大小,那就不要去改变它
                mySize = size;
                break;
            }
        }
        return mySize;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getSize(defalutSize, widthMeasureSpec);
        int height = getSize(defalutSize, heightMeasureSpec);

        if (width < height) {
            height = width;
        } else {
            width = height;
        }

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //调用父View的onDraw函数,因为View这个类帮我们实现了一些
        // 基本的而绘制功能,比如绘制背景颜色、背景图片等
        super.onDraw(canvas);
        int r = getMeasuredWidth() / 2;//也可以是getMeasuredHeight()/2,本例中我们已经将宽高设置相等了
        //圆心的横坐标为当前的View的左边起始位置+半径
        int centerX = getLeft() + r;
        //圆心的纵坐标为当前的View的顶部起始位置+半径
        int centerY = getTop() + r;

        Paint paint = new Paint();
        paint.setColor(Color.GREEN);
        //绘制圆形
        canvas.drawCircle(centerX, centerY, r, paint);
    }
}

2. 自定义ViewGroup

自定义View的过程很简单,就那几步,可自定义ViewGroup可就没那么简单啦~,因为它不仅要管好自己的,还要兼顾它的子View。我们都知道ViewGroup是个View容器,它装纳child View并且负责把child View放入指定的位置。我们结合一个具体案例来一步步实现自定义ViewGroup的过程:将子View按从上到下以垂直顺序一个挨着一个摆放,即模仿实现LinearLayout的垂直布局。

2.1 重写onMeasure

重写onMeasure,实现测量子View大小以及设定ViewGroup的大小,代码如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //将所有的子View进行测量,这会触发每个子View的onMeasure函数
        //注意要与measureChild区分,measureChild是对单个view进行测量
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childCount = getChildCount();// 子View个数

        if (childCount == 0) {//如果没有子View,当前ViewGroup没有存在的意义,不用占用空间
            setMeasuredDimension(0, 0);
        } else { // 有子View,对MeasureSpec为AT_MOST时进行特殊处理
            //如果宽高都是包裹内容
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                //我们将高度设置为所有子View的高度相加,宽度设为子View中最大的宽度
                int height = getTotleHeight();
                int width = getMaxChildWidth();
                setMeasuredDimension(width, height);
            } else if (heightMode == MeasureSpec.AT_MOST) {//如果只有高度是包裹内容
                //宽度设置为ViewGroup自己的测量宽度,高度设置为所有子View的高度总和
                setMeasuredDimension(widthSize, getTotleHeight());
            } else if (widthMode == MeasureSpec.AT_MOST) {//如果只有宽度是包裹内容
                //宽度设置为子View中宽度最大的值,高度设置为ViewGroup自己的测量值
                setMeasuredDimension(getMaxChildWidth(), heightSize);
            }
        }
    }
    /***
     * 获取子View中宽度最大的值
     */
    private int getMaxChildWidth() {
        int childCount = getChildCount();
        int maxWidth = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView.getMeasuredWidth() > maxWidth)
                maxWidth = childView.getMeasuredWidth();
        }
        return maxWidth;
    }

    /***
     * 将所有子View的高度相加
     **/
    private int getTotleHeight() {
        int childCount = getChildCount();
        int height = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            height += childView.getMeasuredHeight();
        }
        return height;
    }

代码中的注释我已经写得很详细,不再对每一行代码进行讲解,相信很容易理解吧。

2.2 重写onLayout

上面的onMeasure将子View测量好了,以及把自己的尺寸也设置好了,接下来我们去摆放子View吧~只需要重写onLayout方法即可,代码如下:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        //记录当前的高度位置
        int curHeight = t;
        //将子View逐个摆放
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            int height = child.getMeasuredHeight();
            int width = child.getMeasuredWidth();
            //摆放子View,参数分别是子View矩形区域的左、上、右、下边
            child.layout(l, curHeight, l + width, curHeight + height);
            curHeight += height;
        }
    }

自定义ViewGroup已经完成, 我们来测试一下效果,将我们自定义的ViewGroup里面放3个Button ,将这3个Button的宽度设置不一样,把我们的ViewGroup的宽高都设置为包裹内容wrap_content,为了看的效果明显,我们给ViewGroup加个背景颜色:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.jieyao.test.MyViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ff9900">

        <Button
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="btn" />

        <Button
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:text="btn" />

        <Button
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:text="btn" />
    </com.hc.studyview.MyViewGroup>
</LinearLayout>

看看最后的效果吧~是不是很激动我们自己也可以实现LinearLayout的效果啦

自定义ViewGroup

最后附上MyViewGroup的完整源码:

package com.jieyao.test;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

public class MyViewGroup extends ViewGroup {

    public MyViewGroup(Context context) {
        super(context);
    }

    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /***
     * 获取子View中宽度最大的值
     */
    private int getMaxChildWidth() {
        int childCount = getChildCount();
        int maxWidth = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView.getMeasuredWidth() > maxWidth)
                maxWidth = childView.getMeasuredWidth();
        }
        return maxWidth;
    }

    /***
     * 将所有子View的高度相加
     **/
    private int getTotleHeight() {
        int childCount = getChildCount();
        int height = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            height += childView.getMeasuredHeight();
        }
        return height;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //将所有的子View进行测量,这会触发每个子View的onMeasure函数
        //注意要与measureChild区分,measureChild是对单个view进行测量
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childCount = getChildCount();//子View个数

        if (childCount == 0) {//如果没有子View,当前ViewGroup没有存在的意义,不用占用空间
            setMeasuredDimension(0, 0);
        } else { // 有子View,对MeasureSpec为AT_MOST时进行特殊处理
            //如果宽高都是包裹内容
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                //我们将高度设置为所有子View的高度相加,宽度设为子View中最大的宽度
                int height = getTotleHeight();
                int width = getMaxChildWidth();
                setMeasuredDimension(width, height);
            } else if (heightMode == MeasureSpec.AT_MOST) {//如果只有高度是包裹内容
                //宽度设置为ViewGroup自己的测量宽度,高度设置为所有子View的高度总和
                setMeasuredDimension(widthSize, getTotleHeight());
            } else if (widthMode == MeasureSpec.AT_MOST) {//如果只有宽度是包裹内容
                //宽度设置为子View中宽度最大的值,高度设置为ViewGroup自己的测量值
                setMeasuredDimension(getMaxChildWidth(), heightSize);
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        //记录当前的高度位置
        int curHeight = t;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            int height = child.getMeasuredHeight();
            int width = child.getMeasuredWidth();
            child.layout(l, curHeight, l + width, curHeight + height);
            curHeight += height;
        }
    }   
}

3. 实战项目

本人虽然是一个Android开发者,却对苹果手机有独特的爱好。经常使用苹果手机的朋友可能知道, 苹果的设置界面有很多滑动的开关按钮, 可以左滑右滑实现某个功能的开启和关闭, 看上去也是很酷炫有没有~今天, 就来实现一下这个功能。

首先,来看一下我实现的滑动开关效果图:

滑动开关效果图

这个滑动开关是一个纯粹的自定义控件,上面的按钮会随着我们的左右滑动而滑动,并且在状态改变时通知用户,这也是应用中设置某些状态信息时最常见的控件。

在实际开发中,完整的实现一个自定义控件,并让该控件具备某个功能,一般来说要有以下几个步骤:

  1. 创建一个view继承自View或者ViewGroup
  2. 定义自定义view的属性
  3. 在代码中获取属性,并给自定义属性相应的设置事件
  4. 根据实际重写自定义view的onMeasure,onLayout,onDraw方法
  5. 与用户进行交互的逻辑实现
  6. 自定义view的代码优化
public class ToggleButton extends View { // 滑动开关类
}
  <?xml version="1.0" encoding="utf-8"?>
  <resources>
     <declare-styleable name="ToggleButton">
          <!-- 滑动开关背景图片属性-->
          <attr name="SwitchBtnBackgroud" format="reference" />
           <!-- 滑动块背景图片属性-->
          <attr name="SlidBtnBackgroud" format="reference" />
          <!-- 滑动开关的状态-->
         <attr name="CurrentState" format="boolean" />
     </declare-styleable>
  </resources>
        private Bitmap switchBitmap;//滑动开关的背景图片
        private Bitmap slidBitmap;//滑动块的背景图片
        private boolean currentState;// 滑动开关的状态

        //在xml 中引用该控件时,调用该方法
        public ToggleButton(Context context, AttributeSet attrs) {
                super(context, attrs);
                String namespace = "http://schemas.android.com/apk/res/com.itheima.togglebuttondemo";
                currentState = attrs.getAttributeBooleanValue(namespace, "CurrentState",
                int switchBtnBackgroudId = attrs.getAttributeResourceValue(namespace, "SwitchBtnBackgroud", -1);
                int slidBtnBackgroudId =attrs.getAttributeResourceValue(namespace, "SlidBtnBackgroud", -1);
                setSwitchBtnBackgroudResource(switchBtnBackgroudId);
                setSlidBtnBackgroudResource(slidBtnBackgroudId);
        }
  
        //在代码中创建该控件时,调用该构造方法
        public ToggleButton(Context context) {
                super(context);
        }

        // 为了可以高度自定义和增强可扩展性,我们给滑动按钮背景和滑动块背景都提供了设置方法
        //设置滑动开关的背景图片
        public void setSwitchBtnBackgroudResource(int switchBackground) {
                switchBitmap = BitmapFactory.decodeResource(getResources(), switchBackground);
        }

        // 设置滑动块的背景图片
       public void setSlidBtnBackgroudResource(int slideButtonBackground) {
                slidBitmap = BitmapFactory.decodeResource(getResources(), slideButtonBackground);
       }

      //设置滑动开关的默认状态
      public void setCurrentState(boolean b) {
                currentState = b;
      }
       // 1、测量滑动开关的宽高
       @Override
       protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // TODO Auto-generated method stub
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            setMeasuredDimension(switchBitmap.getWidth(), switchBitmap.getHeight());
       }
  
       // 2、绘制,画出我们的滑动开关
       //canvas:画布,将图形绘制在canvas,才能显示到屏幕上
       @Override
       protected void onDraw(Canvas canvas) {
           //绘制滑动开关的背景图片
           canvas.drawBitmap(switchBitmap, 0, 0, null);
           //绘制滑动块的背景图片,要根据手势实时绘制
           if(isTouching){//手指触摸的时候,根据currentx 的值来绘制滑动块
               //根据手指的X 值,来绘制滑动块图片
               int left = currentX - slidBitmap.getWidth()/2;
               if(left < 0){//设置左边界
                      left = 0;//左边零点
               }else if(left > (switchBitmap.getWidth() - slidBitmap.getWidth())){//设置右边界
                      left = switchBitmap.getWidth() - slidBitmap.getWidth();//中心点
               }
               canvas.drawBitmap(slidBitmap, left, 0, null);//根据左边界位置绘制滑动块背景
           }else{ // 手指已经离开控件的时候,根据状态来绘制滑动块
               // 根据状态值,来绘制滑动块
               if(currentState){ //当前为true,开关打开,滑动块显示在最右边
                      canvas.drawBitmap(slidBitmap,switchBitmap.getWidth() - slidBitmap.getWidth(),0, null);
               }else{//当前为false,开关关闭,滑动块显示在最左边
                       canvas.drawBitmap(slidBitmap, 0, 0, null);
               }
           }
       }

5.与用户进行交互的逻辑实现

    // 当控件被触摸后,会调用该方法(通过改动isTouching 和currentState的值动态绘制滑动块)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:// 手指按下
            isTouching = true;
            currentX = (int) event.getX();
            break;
        case MotionEvent.ACTION_MOVE:// 手指滑动
            isTouching = true;
            currentX = (int) event.getX();
            break;
        case MotionEvent.ACTION_UP:// 手指抬起
            isTouching = false;
            currentX = (int) event.getX();
            int center = switchBitmap.getWidth() / 2;
            // 当滑动块中心点大于滑动开关背景图片的中心线时,显示到右边,状态改为true
            boolean state = currentState;
            // 获取滑动块的状态
            currentState = currentX > center;
            // 设置滑动块的状态
            // state != currentState说明开关状态发生了改变
            if (mToggleBtnStateChangeListener != null && state != currentState) {
                 mToggleBtnStateChangeListener.onToggleBtnStateChange(currentState);
            }
            break;
        default:
            break;
        }
        // 强制让控件重新绘制,
        invalidate(); //此方法可以强制重新调用onDraw方法
        // 自己处理触摸事件
        return true;
    }

    // 给滑动块设置状态改变监听(方便在activity代码中做相应逻辑处理)
    // 参数为ToggleBtnStateChangeListener 接口,传入之后会回调onToggleBtnStateChange方法。
    // 根据回调方法中的currentState做对应逻辑判断和逻辑处理
    public void setToggleBtnStateChangeListener(
            ToggleBtnStateChangeListener listener) {
        this.mToggleBtnStateChangeListener = listener;
    }

    // 滑动开关状态改变的回调接口
    public interface ToggleBtnStateChangeListener {
        void onToggleBtnStateChange(boolean currentState);
    }
  1. 自定义view的代码优化:

在上面的步骤结束之后,其实一个完善的自定义控件已经出来了。接下来你要做的只是确保自定义控件运行得流畅,官方的说法是:为了避免你的控件看得来迟缓,确保动画始终保持每秒60帧.

下面是官网给出的优化建议:

1、避免不必要的代码
2、在onDraw()方法中不应该有会导致垃圾回收的代码。
3、尽可能少让onDraw()方法调用,大多数onDraw()方法调用都是手动调用了invalidate()的结果,所以如果不是必须,不要调用invalidate()方法。

下面贴出自定义滑动开关的完整源码:

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

    private Bitmap switchBitmap;// 滑动开关的背景图片

    private Bitmap slidBitmap;// 滑动块的背景图片

    private boolean currentState; // 当前滑动开关的状态

    private int currentX;// 手指触摸点的X值

    private boolean isTouching = false; // 是否触摸到屏幕

    private ToggleBtnStateChangeListener mToggleBtnStateChangeListener;// 状态改变监听器

    // 在xml中引用该控件时,调用该方法
    public ToggleButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 声明的命名空间
        String namespace = "http://schemas.android.com/apk/res/com.itheima.togglebuttondemo";
        // 获取布局中滑动开关状态的属性
        currentState = attrs.getAttributeBooleanValue(namespace,
                "CurrentState", false);
        // 获取布局中滑动开关背景的属性
        int switchBtnBackgroudId = attrs.getAttributeResourceValue(namespace,
                "SwitchBtnBackgroud", -1);
        // 获取布局中滑动开关滑动块的背景的属性
        int slidBtnBackgroudId = attrs.getAttributeResourceValue(namespace,
                "SlidBtnBackgroud", -1);
        // 根据布局中的属性设置滑动开关背景
        setSwitchBtnBackgroudResource(switchBtnBackgroudId);
        // 根据布局中的属性设置滑动开关滑动块的背景
        setSlidBtnBackgroudResource(slidBtnBackgroudId);
    }

    // 在代码中创建该控件时,调用该构造方法
    public ToggleButton(Context context) {
        super(context);
    }

    // 设置滑动开关的背景图片
    public void setSwitchBtnBackgroudResource(int switchBackground) {
        switchBitmap = BitmapFactory.decodeResource(getResources(),
                switchBackground);
    }

    // 设置滑动块的背景图片
    public void setSlidBtnBackgroudResource(int slideButtonBackground) {
        slidBitmap = BitmapFactory.decodeResource(getResources(),
                slideButtonBackground);
    }

    // 设置滑动开关的默认状态
    public void setCurrentState(boolean b) {
        currentState = b;
    }

    // 1、测量滑动开关的宽高
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(switchBitmap.getWidth(), switchBitmap.getHeight());
    }

    // 2、绘制,画出我们的滑动开关
    // canvas:画布,将图形绘制在canvas,才能显示到屏幕上
    @Override
    protected void onDraw(Canvas canvas) {
        // 绘制滑动开关的背景图片
        canvas.drawBitmap(switchBitmap, 0, 0, null);
        // 绘制滑动块的背景图片
        if (isTouching) {// 手指触摸的时候,根据currentX的值来绘制滑动块
            // 根据手指的X值,来绘制滑动块图片
            int left = currentX - slidBitmap.getWidth() / 2;
            if (left < 0) { // 设置左边界
                left = 0;
            } else if (left > (switchBitmap.getWidth() - slidBitmap.getWidth())) {// 设置右边界
                left = switchBitmap.getWidth() - slidBitmap.getWidth();
            }
            canvas.drawBitmap(slidBitmap, left, 0, null);
        } else {// 手指离开控件的时候,根据状态来绘制滑动块
                // 根据状态值,来绘制滑动块
            if (currentState) {// 当前为true,开关打开,滑动块显示在最右边
                canvas.drawBitmap(slidBitmap, switchBitmap.getWidth()
                        - slidBitmap.getWidth(), 0, null);
            } else {// 当前为false,开关关闭,滑动块显示在最左边
                canvas.drawBitmap(slidBitmap, 0, 0, null);
            }
        }
    }

    // 当控件被触摸后,会调用该方法
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:// 手指按下
            isTouching = true;
            currentX = (int) event.getX();
            break;
        case MotionEvent.ACTION_MOVE:// 手指滑动
            isTouching = true;
            currentX = (int) event.getX();
            break;
        case MotionEvent.ACTION_UP:// 手指抬起
            isTouching = false;
            currentX = (int) event.getX();
            int center = switchBitmap.getWidth() / 2;
            // 当滑动块中心点大于滑动开关背景图片的中心线时,显示到右边,当前状态为true
            boolean state = currentState;
            // 获取滑动块的状态
            currentState = currentX > center;
            // 设置滑动块的状态
            if (mToggleBtnStateChangeListener != null && state != currentState) {
                mToggleBtnStateChangeListener
                        .onToggleBtnStateChange(currentState);
            }
            break;
        default:
            break;
        }
        // 强制让控件重新绘制,重新调用onDraw方法
        invalidate();
        // 自己处理触摸事件
        return true;
    }

    // 给滑动块设置状态改变监听
    public void setToggleBtnStateChangeListener(
            ToggleBtnStateChangeListener listener) {
        this.mToggleBtnStateChangeListener = listener;
    }

    // 滑动开关状态改变的回调接口
    public interface ToggleBtnStateChangeListener {
        void onToggleBtnStateChange(boolean currentState);
    }
}

大功告成O(∩_∩)O哈哈~ 下面就是使用啦~

xml布局文件如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:jieyao="http://schemas.android.com/apk/res/com.jieyao.togglebuttondemo"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.jieyao.togglebuttondemo.view.ToggleButton 
        android:id="@+id/togglebutton"
        android:layout_width="wrap_content"
        android:layout_centerInParent="true"
        jieyao:SwitchBtnBackgroud="@drawable/switch_background"
        jieyao:SlidBtnBackgroud="@drawable/slide_button_background"
        jieyao:CurrentState="false"
        android:layout_height="wrap_content"/>

</RelativeLayout>

activity中使用~

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 初始化滑动开关
        ToggleButton togglebutton = (ToggleButton) findViewById(R.id.togglebutton);
        // 设置滑动开关的背景图片
        togglebutton.setSwitchBtnBackgroudResource(R.drawable.switch_background);
        // 设置滑动块的背景图片
        togglebutton.setSlidBtnBackgroudResource(R.drawable.slide_button_background);
        // 设置滑动开关的默认状态
        togglebutton.setCurrentState(true);
        // 设置滑动开关状态监听
        togglebutton.setToggleBtnStateChangeListener(new ToggleBtnStateChangeListener() {

                    @Override
                    public void onToggleBtnStateChange(boolean currentState) {
                        //下面就是根据currentState状态做相应的逻辑咯,根据需求来做
                        if (currentState) {
                            Toast.makeText(getApplicationContext(), "开关打开",Toast.LENGTH_SHORT).show();
                        } else {
                            Toast.makeText(getApplicationContext(), "开关关闭",Toast.LENGTH_SHORT).show();
                        }
                    }
                });
    }
}

效果图如下:

滑动开关效果

以上就是自定义View的全过程啦~ 希望能对你们有帮助~! 本人技术有限,如有错误,还请指出,谢谢!

上一篇下一篇

猜你喜欢

热点阅读