美文网首页Android自定义View
自定义View5---完整的自定义View

自定义View5---完整的自定义View

作者: 凯玲之恋 | 来源:发表于2018-09-10 15:11 被阅读14次

    移步自定义View系列

    • 1.自定义view的分类
      • 自定义单一view(不含子view)
        • 继承view
        • 继承特定view如textview
      • 自定义viewGroup(含子view)
        • 继承viewGroup
        • 继承特定的viewGroup如LinearLayout
    • 2.使用注意点
      • 支持特殊属性
        • wrap_content
        • padding
        • margin
      • 多线程直接使用post方式
        • 避免使用handler等其他方式
      • 避免内存泄露
        • 线程/动画要及时停止
      • 处理好滑动冲突
        • view带有滑动嵌套情况
    • 3.具体实例
      • 实现基本自定义view
      • 支持wrap_content属性
      • 支持padding属性
      • 提供自定义属性

    1 自定义View的分类

    自定义View一共分为两大类,具体如下图:


    image

    2 具体介绍 & 使用场景

    image

    3 使用注意点

    image

    3.1 支持特殊属性--wrap_content

    • 支持wrap_content
      如果不在onMeasure()中对wrap_content作特殊处理,那么wrap_content属性将失效
      自定义View2---View Measure过程
    • 在onMeasure()中的getDefaultSize()的默认实现中,当View的测量模式是AT_MOST或EXACTLY时,View的大小都会被设置成子View MeasureSpec的specSize。
    • 因为AT_MOST对应wrap_content;EXACTLY对应match_parent,所以,默认情况下,wrap_content和match_parent是具有相同的效果的。
    • 在计算子View MeasureSpec的getChildMeasureSpec()中,子View MeasureSpec在属性被设置为wrap_content或match_parent情况下,子View MeasureSpec的specSize被设置成parenSize = 父容器当前剩余空间大小
    • 所以:wrap_content起到了和match_parent相同的作用:等于父容器当前剩余空间大小

    3.1.1 默认情况getDefaultSize()

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    //参数说明:View的宽 / 高测量规格
    
    //setMeasuredDimension()  用于获得View宽/高的测量值
    //这两个参数是通过getDefaultSize()获得的
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
               getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
    }
    
    public static int getDefaultSize(int size, int measureSpec) {  
    
    //参数说明:
    // 第一个参数size:提供的默认大小
    // 第二个参数:宽/高的测量规格(含模式 & 测量大小)
    
        //设置默认大小
        int result = size; 
    
        //获取宽/高测量规格的模式 & 测量大小
        int specMode = MeasureSpec.getMode(measureSpec);  
        int specSize = MeasureSpec.getSize(measureSpec);  
    
        switch (specMode) {  
            // 模式为UNSPECIFIED时,使用提供的默认大小
            // 即第一个参数:size 
            case MeasureSpec.UNSPECIFIED:  
                result = size;  
                break;  
            // 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值
            // 即measureSpec中的specSize
            case MeasureSpec.AT_MOST:  
            case MeasureSpec.EXACTLY:  
                result = specSize;  
                break;  
        }  
    
     //返回View的宽/高值
        return result;  
    }
    

    3.1.2 getChildMeasureSpec()

    //作用:
    / 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
    //即子view的确切大小由两方面共同决定:父view的MeasureSpec 和 子view的LayoutParams属性 
    
    
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  
    
     //参数说明
     * @param spec 父view的详细测量值(MeasureSpec) 
     * @param padding view当前尺寸的的内边距和外边距(padding,margin) 
     * @param childDimension 子视图的布局参数(宽/高)
    
        //父view的测量模式
        int specMode = MeasureSpec.getMode(spec);     
    
        //父view的大小
        int specSize = MeasureSpec.getSize(spec);     
    
        //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)   
        int size = Math.max(0, specSize - padding);  
    
        //子view想要的实际大小和模式(需要计算)  
        int resultSize = 0;  
        int resultMode = 0;  
    
        //通过父view的MeasureSpec和子view的LayoutParams确定子view的大小  
    
    
        // 当父view的模式为EXACITY时,父view强加给子view确切的值
       //一般是父view设置为match_parent或者固定值的ViewGroup 
        switch (specMode) {  
        case MeasureSpec.EXACTLY:  
            // 当子view的LayoutParams>0,即有确切的值  
            if (childDimension >= 0) {  
                //子view大小为子自身所赋的值,模式大小为EXACTLY  
                resultSize = childDimension;  
                resultMode = MeasureSpec.EXACTLY;  
    
            // 当子view的LayoutParams为MATCH_PARENT时(-1)  
            } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                //子view大小为父view大小,模式为EXACTLY  
                resultSize = size;  
                resultMode = MeasureSpec.EXACTLY;  
    
            // 当子view的LayoutParams为WRAP_CONTENT时(-2)      
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST  
                resultSize = size;  
                resultMode = MeasureSpec.AT_MOST;  
            }  
            break;  
    
        // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)  
        case MeasureSpec.AT_MOST:  
            // 道理同上  
            if (childDimension >= 0) {  
                resultSize = childDimension;  
                resultMode = MeasureSpec.EXACTLY;  
            } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                resultSize = size;  
                resultMode = MeasureSpec.AT_MOST;  
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                resultSize = size;  
                resultMode = MeasureSpec.AT_MOST;  
            }  
            break;  
    
        // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
        // 多见于ListView、GridView  
        case MeasureSpec.UNSPECIFIED:  
            if (childDimension >= 0) {  
                // 子view大小为子自身所赋的值  
                resultSize = childDimension;  
                resultMode = MeasureSpec.EXACTLY;  
            } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0  
                resultSize = 0;  
                resultMode = MeasureSpec.UNSPECIFIED;  
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0  
                resultSize = 0;  
                resultMode = MeasureSpec.UNSPECIFIED;  
            }  
            break;  
        }  
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    
    image

    3.1.3解决方案:

    • 当自定义View的布局参数设置成wrap_content时时,指定一个默认大小(宽 / 高)。
    • 具体是在复写onMeasure()里进行设置
    @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
    
            // 获取宽-测量规则的模式和大小
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    
            // 获取高-测量规则的模式和大小
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            // 设置wrap_content的默认宽 / 高值
            // 默认宽/高的设定并无固定依据,根据需要灵活设置
            // 类似TextView,ImageView等针对wrap_content均在onMeasure()对设置默认宽 / 高值有特殊处理,具体读者可以自行查看
            int mWidth = 400;
            int mHeight = 400;
    
          // 当布局参数设置为wrap_content时,设置默认值
            if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                setMeasuredDimension(mWidth, mHeight);
            // 宽 / 高任意一个布局参数为= wrap_content时,都设置默认值
            } else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
                setMeasuredDimension(mWidth, heightSize);
            } else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                setMeasuredDimension(widthSize, mHeight);
            }
    }
    

    这样,当你的自定义View的宽 / 高设置成wrap_content属性时就会生效了。

    3.2 支持特殊属性-支持padding & margin

    如果不支持,那么padding和margin(ViewGroup情况)的属性将失效

    1. 对于继承View的控件,padding是在draw()中处理
    2. 对于继承ViewGroup的控件,padding和margin会直接影响measure和layout过程
        // 复写onDraw()
        @Override
        protected void onDraw(Canvas canvas) {
    
            super.onDraw(canvas);
    
            final int paddingLeft = getPaddingLeft();
            final int paddingRight = getPaddingRight();
            final int paddingTop = getPaddingTop();
            final int paddingBottom = getPaddingBottom();
    
    
            // 获取控件的高度和宽度
            int width = getWidth() - paddingLeft - paddingRight;
            int height = getHeight() - paddingTop - paddingBottom;
    
            // 设置圆的半径 = 宽,高最小值的2分之1
            int r = Math.min(width, height) / 2;
    
            // 画出圆(蓝色)
            // 圆心 = 控件的中央,半径 = 宽,高最小值的2分之1
            canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, r, mPaint1);
    
        }
    

    3.3 多线程应直接使用post方式

    View的内部本身提供了post系列的方法,完全可以替代Handler的作用,使用起来更加方便、直接。

    3.4 避免内存泄露

    主要针对View中含有线程或动画的情况:当View退出或不可见时,记得及时停止该View包含的线程和动画,否则会造成内存泄露问题。

    启动或停止线程/ 动画的方式:

    1. 启动线程/ 动画:使用view.onAttachedToWindow(),因为该方法调用的时机是当包含View的Activity启动的时刻
    2. 停止线程/ 动画:使用view.onDetachedFromWindow(),因为该方法调用的时机是当包含View的Activity退出或当前View被remove的时刻

    3.5 处理好滑动冲突

    当View带有滑动嵌套情况时,必须要处理好滑动冲突,否则会严重影响View的显示效果。

    3.6 提供自定义属性

    使用步骤有如下:

    1. 在values目录下创建自定义属性的xml文件
    2. 在自定义View的构造方法中解析自定义属性的值
    3. 在布局文件中使用自定义属性

    3.6.1 步骤1:在values目录下创建自定义属性的xml文件

    attrs_circle_view.xml

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <!--自定义属性集合:CircleView-->
        <!--在该集合下,设置不同的自定义属性-->
        <declare-styleable name="CircleView">
            <!--在attr标签下设置需要的自定义属性-->
            <!--此处定义了一个设置图形的颜色:circle_color属性,格式是color,代表颜色-->
            <!--格式有很多种,如资源id(reference)等等-->
            <attr name="circle_color" format="color"/>
    
        </declare-styleable>
    </resources>
    

    对于自定义属性类型 & 格式如下:

    <-- 1. reference:使用某一资源ID -->
    <declare-styleable name="名称">
        <attr name="background" format="reference" />
    </declare-styleable>
    // 使用格式
      // 1. Java代码
      private int ResID;
      private Drawable ResDraw;
      ResID = typedArray.getResourceId(R.styleable.SuperEditText_background, R.drawable.background); // 获得资源ID
      ResDraw = getResources().getDrawable(ResID); // 获得Drawble对象
    
      // 2. xml代码
    <ImageView
        android:layout_width="42dip"
        android:layout_height="42dip"
        app:background="@drawable/图片ID" />
    
    <--  2. color:颜色值 -->
    <declare-styleable name="名称">
        <attr name="textColor" format="color" />
    </declare-styleable>
    // 格式使用
    <TextView
        android:layout_width="42dip"
        android:layout_height="42dip"
        android:textColor="#00FF00" />
    
    <-- 3. boolean:布尔值 -->
    <declare-styleable name="名称">
        <attr name="focusable" format="boolean" />
    </declare-styleable>
    // 格式使用
    <Button
        android:layout_width="42dip"
        android:layout_height="42dip"
        android:focusable="true" />
    
    <-- 4. dimension:尺寸值 -->
    <declare-styleable name="名称">
        <attr name="layout_width" format="dimension" />
    </declare-styleable>
    // 格式使用:
    <Button
        android:layout_width="42dip"
        android:layout_height="42dip" />
    
    <-- 5. float:浮点值 -->
    <declare-styleable name="AlphaAnimation">
        <attr name="fromAlpha" format="float" />
        <attr name="toAlpha" format="float" />
    </declare-styleable>
    // 格式使用
    <alpha
        android:fromAlpha="1.0"
        android:toAlpha="0.7" />
    
    <-- 6. integer:整型值 -->
    <declare-styleable name="AnimatedRotateDrawable">
        <attr name="frameDuration" format="integer" />
        <attr name="framesCount" format="integer" />
    </declare-styleable>
    // 格式使用
    <animated-rotate
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:frameDuration="100"
        android:framesCount="12"
     />
    
    <-- 7. string:字符串 -->
    <declare-styleable name="MapView">
        <attr name="apiKey" format="string" />
    </declare-styleable>
    // 格式使用
    <com.google.android.maps.MapView
     android:apiKey="0jOkQ80oD1JL9C6HAja99uGXCRiS2CGjKO_bc_g" />
    
    <-- 8. fraction:百分数 -->
    <declare-styleable name="RotateDrawable">
        <attr name="pivotX" format="fraction" />
        <attr name="pivotY" format="fraction" />
    </declare-styleable>
    // 格式使用
    <rotate
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:pivotX="200%"
        android:pivotY="300%"
     />
    
    
    <-- 9. enum:枚举值 -->
    <declare-styleable name="名称">
        <attr name="orientation">
            <enum name="horizontal" value="0" />
            <enum name="vertical" value="1" />
        </attr>
    </declare-styleable>
    // 格式使用
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
    />
    
    <-- 10. flag:位或运算 -->
    <declare-styleable name="名称">
        <attr name="windowSoftInputMode">
            <flag name="stateUnspecified" value="0" />
            <flag name="stateUnchanged" value="1" />
            <flag name="stateHidden" value="2" />
            <flag name="stateAlwaysHidden" value="3" />
            <flag name="stateVisible" value="4" />
            <flag name="stateAlwaysVisible" value="5" />
            <flag name="adjustUnspecified" value="0x00" />
            <flag name="adjustResize" value="0x10" />
            <flag name="adjustPan" value="0x20" />
            <flag name="adjustNothing" value="0x30" />
        </attr>
    </declare-styleable>、
    // 使用
    <activity
        android:name=".StyleAndThemeActivity"
        android:label="@string/app_name"
        android:windowSoftInputMode="stateUnspecified | stateUnchanged | stateHidden" >
    
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    
    
    
    <-- 特别注意:属性定义时可以指定多种类型值 -->
    <declare-styleable name="名称">
        <attr name="background" format="reference|color" />
    </declare-styleable>
    // 使用
    <ImageView
        android:layout_width="42dip"
        android:layout_height="42dip"
        android:background="@drawable/图片ID|#00FF00" />
    

    3.6.2 步骤2:在自定义View的构造方法中解析自定义属性的值

        // 自定义View的三个构造函数
        public CircleView(Context context) {
            super(context);
    
            // 在构造函数里初始化画笔的操作
            init();
        }
    
        public CircleView(Context context, AttributeSet attrs) {
            this(context, attrs,0);
            init();
    
        }
    
        public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
    
            // 加载自定义属性集合CircleView
            TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CircleView);
    
            // 解析集合中的属性circle_color属性
            // 该属性的id为:R.styleable.CircleView_circle_color
            // 第二个参数是默认设置颜色
            mColor = a.getColor(R.styleable.CircleView_circle_color,Color.RED);
    
            // 解析后释放资源
            a.recycle();
    
            init();
        }
    
        // 画笔初始化
        private void init() {
    
            // 创建画笔
            mPaint1 = new Paint();
            // 设置画笔颜色为蓝色
            mPaint1.setColor(mColor);
            // 设置画笔宽度为10px
            mPaint1.setStrokeWidth(5f);
            //设置画笔模式为填充
            mPaint1.setStyle(Paint.Style.FILL);
    
        }
    

    3.6.3 步骤3:在布局文件中使用自定义属性

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
      <!--必须添加schemas声明才能使用自定义属性-->
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.kailing.diy_view.MainActivity"
        >
      
    <!-- 注意添加自定义View组件的标签名:包名 + 自定义View类名-->
        <!--  控件背景设置为黑色-->
        <com.kailing.diy_view.CircleView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
    
            android:background="#000000"
            android:padding="30dp"
    
        <!--设置自定义颜色-->
            app:circle_color="#FF4081"
             />
    </RelativeLayout>
    

    参考

    手把手教你写一个完整的自定义View

    相关文章

      网友评论

        本文标题:自定义View5---完整的自定义View

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