Android最易懂的自定义View讲解

作者: Android_Jieyao | 来源:发表于2018-09-06 15:56 被阅读231次

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

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

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

    • 自定义View
      可以理解为自定义View的父类,是一个单独的控件,里面无法存放子View。例如TextView,ImageView等都是继承View的,View里面最关键的方法是onMeasureonDraw

    • 自定义ViewGroup
      ViewGroup是View的子类,相当于一个容器,里面可以放子View。例如LinearLayout,RelativeLayout等都是继承ViewGroup的。ViewGroup里面最关键的方法是onMeasure和onDraw和onLayout。其中onLayout是ViewGroup中特有的方法,用来实现子View的摆放。

    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 自定义属性

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

    • 首先我们需要在res/values/attrs.xml文件(如果没有请自己新建)里面声明一个我们自定义的属性:
    <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"

    • 最后就是在我们的自定义的View的代码里面把我们自定义的属性的值取出来,在构造函数中,还记得有个AttributeSet属性吗?就是靠它帮我们把布局里面的属性取出来:
      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的代码优化
    • 1.创建view
    public class ToggleButton extends View { // 滑动开关类
    }
    
      1. 自定义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>
    
      1. 在代码中获取属性并给自定义属性相应的设置事件
            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. 重写onMeasure方法和onDraw方法
           // 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的全过程啦~ 希望能对你们有帮助~! 本人技术有限,如有错误,还请指出,谢谢!

    相关文章

      网友评论

      本文标题:Android最易懂的自定义View讲解

      本文链接:https://www.haomeiwen.com/subject/impzwftx.html