Android浅谈自定义View

前言

自定义View在实际开发中经常会用到,因此掌握自定义View是必须的。它能帮助我们更好的了解View的绘制流程,提高工作效率和代码质量。

什么是自定义View

自定义View通俗来说就是根据自己的需要实现特定化需求的View。

为什么要用自定义View

在实际项目开发中,UI小姐姐给我们设计的视图组件是各种各样的 ,android提供的基础组件是无法满足实际需要的,这种情况下我们就需要自定义View了。

1.自定义View基础

1.1自定义View的分类

自定义View分为以下几类

类型定义
自定义组合控件多个控件组合成为一个新的控件,方便多处复用(eg:app通用标题栏)
继承系统View控件继承自TextView等系统控件,在系统控件的基础功能上进行扩展
直接继承View不复用系统控件逻辑,继承View进行功能定义
继承系统ViewGroup控件继承自LinearLayout等系统控件,在系统控件的基础功能上进行扩展
直接继承ViewGroup不复用系统控件逻辑,继承ViewGroup进行功能定义

1.2.View的绘制流程

View的绘制主要由measure()、layout()、draw()这个三个函数完成的。

函数名作用相关函数
measure()测量View的宽高measure(),setMeasuredDimension(),onMeasure()
layout()计算当前View以及子View的位置ayout(),onLayout(),setFrame()
draw()视图的绘制工作draw(),onDraw()

1.3.坐标系

坐标系上节有详细讲过

1.4.构造函数

public class MyView extends View {
    /**
     * 在java代码里new的时候会用到
     * @param context
     */
    public MyView(Context context) {
        super(context);
    }
 
    /**
     * 在xml布局文件中使用时自动调用
     * @param context
     */
    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
 
    /**
     * 不会自动调用,如果有默认style时,在第二个构造函数中调用
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
 
 
    /**
     * 只有在API版本>21时才会用到
     * 不会自动调用,如果有默认style时,在第二个构造函数中调用
     * @param context
     * @param attrs
     * @param defStyleAttr
     * @param defStyleRes
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}

1.5.自定义属性

Android系统的控件以android开头的都是系统自带的属性。为了方便配置自定义View的属性,我们也可以自定义属性值。
Android自定义属性可分为以下几步:
1.自定义一个View
2.编写values/attrs.xml,在其中编写styleable和item等标签元素
3.在布局文件中View使用自定义的属性(注意namespace)
5.在View的构造方法中通过TypedArray获取

实例说明

1.5.1.自定义属性的声明文件

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="myView">
            <attr name="test" format="string" />
            <attr name="testAttr" format="integer" />
        </declare-styleable>
    </resources>

1.5.2.自定义View类

public class MyTextView extends View {
    private static final String TAG = MyTextView.class.getSimpleName();
 
    //在View的构造方法中通过TypedArray获取
    public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.myView);
        String test= ta.getString(R.styleable.myView_test);
        int textAttr = ta.getInteger(R.styleable.myView_testAttr, -1);
        Log.e(TAG, "test= " + test+ " , textAttr = " + textAttr);
        ta.recycle();
    }
}

1.5.3.在布局文件中使用

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res/com.example.test"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
 
    <com.example.MyView.MyTextView
        android:layout_width="100dp"
        android:layout_height="200dp"
        app:testAttr="10"
        app:text="hello android" />
 
</RelativeLayout>

1.5.4.属性值的类型format

(1). reference:参考某一资源ID

属性定义:

<declare-styleable name = "名称">
     <attr name = "background" format = "reference" />
</declare-styleable>

属性使用:

<ImageView android:background = "@drawable/图片ID"/>

(2).color:颜色值

属性定义:

<attr name = "textColor" format = "color" />

属性使用:

<TextView android:textColor = "#00FF00" />

(3).boolean:布尔值

属性定义:

<attr name = "focusable" format = "boolean" />

属性使用:

<Button android:focusable = "true"/>

(4). dimension:尺寸值

属性定义:

<attr name = "layout_width" format = "dimension" />

属性使用:

<Button android:layout_width = "18dp"/>

(5). float:浮点值

属性定义:

<attr name = "fromAlpha" format = "float" />

属性使用:

<alpha android:fromAlpha = "1.0"/>

(6). integer:整型值

属性定义:

<attr name = "framesCount" format="integer" />

属性使用:

<animated-rotate android:framesCount = "12"/>

(7). string:字符串

属性定义:

<attr name = "text" format = "string" />

属性使用:

<TextView android:text = "我是文本"/>

(8). fraction:百分数

属性定义:

<attr name = "pivotX" format = "fraction" />

属性使用:

<rotate android:pivotX = "100%"/>

(9). enum:枚举值

属性定义:

<declare-styleable name="名称">
    <attr name="orientation">
        <enum name="horizontal" value="0" />
        <enum name="vertical" value="1" />
    </attr>
</declare-styleable>

属性使用:

<LinearLayout  
    android:orientation = "vertical">
</LinearLayout>

注意:枚举类型的属性在使用的过程中只能使用其中一个,不能 android:orientation = “horizontal|vertical"

(10). flag:位或运算

属性定义:

<declare-styleable name="名称">
    <attr name="gravity">
            <flag name="top" value="0x01" />
            <flag name="bottom" value="0x02" />
            <flag name="left" value="0x04" />
            <flag name="right" value="0x08" />
            <flag name="center_vertical" value="0x16" />
            ...
    </attr>
</declare-styleable>

属性使用:

<TextView android:gravity="bottom|left"/>

注意:位运算类型的属性在使用的过程中可以使用多个值

(11). 混合类型:属性定义时可以指定多种类型值

属性定义:

<declare-styleable name = "名称">
     <attr name = "background" format = "reference|color" />
</declare-styleable>

属性使用:

<ImageView
android:background = "@drawable/图片ID" />
或者:
<ImageView
android:background = "#00FF00" />

2.View绘制流程

View 的绘制流程分为三步:在自定义View的时候一般需要重写父类的onMeasure()、onLayout()、onDraw()三个方法来完成视图的绘制工作。一个完整的绘制流程包括measure、layout、draw三个步骤,其中:

measure:测量。系统会先根据xml布局文件和代码中对控件属性的设置,来获取或者计算出每个ViewViewGrop的尺寸,并将这些尺寸保存下来。
layout:布局。根据测量出的结果以及对应的参数,来确定每一个控件应该显示的位置。
draw:绘制。确定好位置后,就将这些控件绘制到屏幕上。

2.1 Measure()

重要的内部类MeasureSpec
MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。

MeasureSpec是一个int型变量,值保存在一个int值当中。一个int值有32位,MeasureSpec让32位中的最高2位代表mode,后30位代表size。其中mode有UNSPECIFIED(00)= 0、EXACTLY(01)=1、AT_MOST(10)=2
在MeasureSpec当中一共存在三种mode:UNSPECIFIED、EXACTLY 和AT_MOST。

对于View来说,MeasureSpec的mode和Size有如下意义

模式含义对应
EXACTLY精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Sizematch_parent
AT_MOST最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值wrap_content
UNSPECIFIED无限制,View对尺寸没有任何限制,View设置为多大就应当为多大(把该View放在NestScrollView、ScrollView中)一般系统内部使用

使用方法

    // 获取测量模式(Mode)
    int specMode = MeasureSpec.getMode(measureSpec)
    // 获取测量大小(Size)
    int specSize = MeasureSpec.getSize(measureSpec)
    // 通过Mode 和 Size 生成新的SpecMode
    int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);

在View当中,MeasureSpace的测量代码如下:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
 
        int size = Math.max(0, specSize - padding);
 
        int resultSize = 0;
        int resultMode = 0;
 
        switch (specMode) {
        //当父View要求一个精确值时,为子View赋值
        case MeasureSpec.EXACTLY:
            //如果子view有自己的尺寸,则使用自己的尺寸
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
                //当子View是match_parent,将父View的大小赋值给子View
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
                //如果子View是wrap_content,设置子View的最大尺寸为父View
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
 
        // 父布局给子View了一个最大界限
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                //如果子view有自己的尺寸,则使用自己的尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 父View的尺寸为子View的最大尺寸
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //父View的尺寸为子View的最大尺寸
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
 
        // 父布局对子View没有做任何限制
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
            //如果子view有自己的尺寸,则使用自己的尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //因父布局没有对子View做出限制,当子View为MATCH_PARENT时则大小为0
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //因父布局没有对子View做出限制,当子View为WRAP_CONTENT时则大小为0
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
    
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
这里需要注意,这段代码只是在为子View设置MeasureSpec参数而不是实际的设置子View的大小。子View的最终大小需要在View中具体设置。

从源码可以看出来,子View的测量模式是由自身LayoutParam和父View的MeasureSpec来决定的。
在测量子View大小时:

父View mode子View
UNSPECIFIED父布局没有做出限制,子View有自己的尺寸,则使用,如果没有则为0
EXACTLY父布局采用精准模式,有确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围
AT_MOST父布局采用最大模式,存在确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围

onMeasure()
整个测量过程的入口位于View的measure方法当中,该方法做了一些参数的初始化之后调用了onMeasure方法,这里我们主要分析onMeasure。

onMeasure方法的源码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

很简单这里只有一行代码,涉及到了三个方法我们挨个分析。
1.setMeasuredDimension(int measuredWidth, int measuredHeight) :该方法用来设置View的宽高,在我们自定义View时也会经常用到。
2.getDefaultSize(int size, int measureSpec):该方法用来获取View默认的宽高,结合源码来看。

/**
*   有两个参数size和measureSpec
*   1、size表示View的默认大小,它的值是通过`getSuggestedMinimumWidth()方法来获取的,之后我们再分析。
*   2、measureSpec则是我们之前分析的MeasureSpec,里面存储了View的测量值以及测量模式
*/
public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
 
        //从这里我们看出,对于AT_MOST和EXACTLY在View当中的处理是完全相同的。所以在我们自定义View时要对这两种模式做出处理。
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

getSuggestedMinimumWidth():getHeight和该方法原理是一样的,这里只分析这一个。

//当View没有设置背景时,默认大小就是mMinWidth,这个值对应Android:minWidth属性,如果没有设置时默认为0.
//如果有设置背景,则默认大小为mMinWidth和mBackground.getMinimumWidth()当中的较大值。
protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

ViewGroup的测量过程与View有一点点区别,其本身是继承自View,它没有对View的measure方法以及onMeasure方法进行重写。

为什么没有重写onMeasure呢?ViewGroup除了要测量自身宽高外还需要测量各个子View的大小,而不同的布局测量方式也都不同(可参考LinearLayout以及FrameLayout),所以没有办法统一设置。因此它提供了测量子View的方法measureChildren()以及measureChild()帮助我们对子View进行测量。

measureChildren()以及measureChild()的源码这里不再分析,大致流程就是遍历所有的子View,然后调用View的measure()方法,让子View测量自身大小。具体测量流程上面也以及介绍过了

measure过程会因为布局的不同或者需求的不同而呈现不同的形式,使用时还是要根据业务场景来具体分析,如果想再深入研究可以看一下LinearLayout的onMeasure方法。

2.2 Layout()

layout()过程,对于View来说用来计算View的位置参数,对于ViewGroup来说,除了要测量自身位置,还需要测量子View的位置。

layout()方法是整个Layout()流程的入口,看一下这部分源码

/**
*  这里的四个参数l、t、r、b分别代表View的左、上、右、下四个边界相对于其父View的距离。
*
*/
public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
   //   ....省略其它部分
    }
  private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }
  protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
    // ....省略其它部分
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;
            int drawn = mPrivateFlags & PFLAG_DRAWN;
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
            invalidate(sizeChanged);
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
            mPrivateFlags |= PFLAG_HAS_BOUNDS;
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }
            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(sizeChanged);
                invalidateParentCaches();
            }
            mPrivateFlags |= drawn;
            mBackgroundSizeChanged = true;
            mDefaultFocusHighlightSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
            }
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
        return changed;
    }

从源码我们知道,在layout()方法中已经通过setOpticalFrame(l, t, r, b)或 setFrame(l, t, r, b)方法对View自身的位置进行了设置,所以onLayout(changed, l, t, r, b)方法主要是ViewGroup对子View的位置进行计算。

2.3 Draw()

draw流程也就是的View绘制到屏幕上的过程,整个流程的入口在View的draw()方法之中,而源码注释也写的很明白,整个过程可以分为6个步骤。

如果需要,绘制背景。
1.有过有必要,保存当前canvas。
2.绘制View的内容。
3.绘制子View。
4.如果有必要,绘制边缘、阴影等效果。
5.绘制装饰,如滚动条等等。
6.通过各个步骤的源码再做分析:

    public void draw(Canvas canvas) {
        int saveCount;
        // 1. 如果需要,绘制背景
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
        
        // 2. 有过有必要,保存当前canvas。
        final int viewFlags = mViewFlags;
      
        if (!verticalEdges && !horizontalEdges) {
            // 3. 绘制View的内容。
            if (!dirtyOpaque) onDraw(canvas);
 
            // 4. 绘制子View。
            dispatchDraw(canvas);
 
            drawAutofilledHighlight(canvas);
 
            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
 
            // 6. 绘制装饰,如滚动条等等。
            onDrawForeground(canvas);
 
            // we're done...
            return;
        }
    }
    
    /**
    *  1.绘制View背景
    */
    private void drawBackground(Canvas canvas) {
        //获取背景
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        setBackgroundBounds();
        //获取便宜值scrollX和scrollY,如果scrollX和scrollY都不等于0,则会在平移后的canvas上面绘制背景。
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }
    
    /**
    * 3.绘制View的内容,该方法是一个空的实现,在各个业务当中自行处理。
    */
    protected void onDraw(Canvas canvas) {
    }
    
    /**
    * 4. 绘制子View。该方法在View当中是一个空的实现,在各个业务当中自行处理。
    *  在ViewGroup当中对dispatchDraw方法做了实现,主要是遍历子View,并调用子类的draw方法,一般我们不需要自己重写该方法。
    */
    protected void dispatchDraw(Canvas canvas) {
    }
       

至此View的绘制流程结束。
requestLayout重新绘制视图
子View调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制。

invalidate在UI线程中重新绘制视图
当子View调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,进行开始View树重绘流程(只绘制需要重绘的视图)。

postInvalidate在非UI线程中重新绘制视图
这个方法与invalidate方法的作用是一样的,都是使View树重绘,但两者的使用条件不同,postInvalidate是在非UI线程中调用,invalidate则是在UI线程中调用。

总结

本文参考了 Android自定义View全解
本文主要对常用的自定义View的方式进行了总结,并简单分析了View的绘制流程。后面会对各种自定义View做出简单的例子。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值