Android之自定义View学习(二)


Android学习系列
Android之Room学习
Android之自定义View学习(一)
Android之自定义View学习(二)

目录Android学习系列Android之Room学习Android之自定义View学习(一)Android之自定义View学习(二)Android之自定义View学习(二)前言2. 自定义View初体验2.1 View类简介2.2 自定义View构造函数2.3 绘制自定义View2.3.1 测量View2.3.1.1 MeasureSpec2.3.1.2 DecorView2.3.1.3 onMeasure2.3.2 布局View2.3.3 绘制View2.3.4 绘制自定义View总结其他学习分享系列数据结构与算法系列数据结构与算法之哈希表数据结构与算法之跳跃表数据结构与算法之字典树数据结构与算法之2-3树数据结构与算法之平衡二叉树数据结构与算法之十大经典排序数据结构与算法之二分查找三模板

Android之自定义View学习(二)
前言

在上一节当中,博主介绍了布局加载的流程以及布局加载的源码,今天主要介绍一下View的工作原理和源码。

2. 自定义View初体验
2.1 View类简介

直观上:视图上的各种控件包括布局都是一种View
代码上:View类是Android所有组件控件间接或者直接的父类

借用红黑联盟网站上的一张图,下图为View的继承关系图,红色控件为常用控件

Android之自定义View学习(二)

自定义View,顾名思义,就是写自己所需要的控件。下面开始View第一步。

2.2 自定义View构造函数

观看控件的源码,控件都是或直接或间接的继承了View类进行操作。因此第一步继承View类,并继承四种构造函数如下。

public class MyTestView extends View {
public static String TAG = "View";

//第一类,MyTestView在代码中创建
public MyTestView(Context context) {
super(context);
}

//第二类,MyTestView在.xml的布局文件中创建
//自定义属性从AttributeSet传入
public MyTestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

//第三类,MyTestView有自定义style属性时调用
public MyTestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

//第四类,MyTestView设置自定义style resource文件时调用
public MyTestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}

2.3 绘制自定义View

绘制自定义View主要用到以下函数

public class MyTestView extends View {
......
@Override
//用于被内部调用测量视图大小
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

@Override
//用于被内部调用安排视图位置
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}

@Override
//用于被内部调用绘制视图样式
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

}
}

2.3.1 测量View

View系统的绘制流程会从ViewRoot(源码位置是ViewRootImpl.java)的performTraversals()方法中开始调用performMeasure(),进而调用View的measure()方法。

然后onMeasure方法在源码中被measure方法调用,用来测量自定义View的大小,并且在measure方法注解中提及,想要自定义View必须要重写onMeasure方法。

同时,onMeasure方法的参数widthMeasureSpecheightMeasureSpec也有着重要意义,这两个参数是从MeasureSpec得到。

2.3.1.1 MeasureSpec

MeasureSpec封装了父类给子类的布局要求,widthMeasureSpecheightMeasureSpec即对宽度、高度的要求。

MeasureSpec实质上是一个32位int值,由测量模式SpecMode和测量模式下的大小值SpecSize组成,高两位是测量模式,有三种模式,低30位是测量模式下的大小值。

UNSPECIFIED

父类不指定尺寸也不限制尺寸,随便继承的子类如何定义尺寸。

EXACTLY

父类指定子类具体尺寸

AT_MOST

父类指定子类的最大尺寸

定义如下:

 public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;

/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}

public static final int UNSPECIFIED = 0 << MODE_SHIFT;

public static final int EXACTLY = 1 << MODE_SHIFT;

public static final int AT_MOST = 2 << MODE_SHIFT;
......
}

2.3.1.2 DecorView

正常情况下,我们直接按照MeasureSpec来进行指定即可。对于一般View,确实好像没有问题,但是仔细思考一下,那么XML中指定属性match_parentwrap_content从哪儿得到的?LinearLayout是个ViewGroupViewGroup继承于View,当Activity启动时,最开始的根视图是谁呢,这个根视图又是如何获得宽高的呢?

这时候就要引入LayoutParamsDecorView的概念。

首先LayoutParams比较简单,就是布局所需要的宽高设置。

ViewGroup.java

public static class LayoutParams {
......
@SuppressWarnings({"UnusedDeclaration"})
@Deprecated
public static final int FILL_PARENT = -1;

public static final int MATCH_PARENT = -1;

public static final int WRAP_CONTENT = -2;
......
}

因此对于一般View,它的宽高是由父容器的MeasureSpec 和自身 LayoutParams一起决定的。关于XML中标签指定宽高的问题已经解决了。

那么进入第二个问题,根视图是谁的呢?它是如何获取宽高呢?

最顶层也就是最外层的根视图我们称之为DecorView.

如下图,Activity是一个PhoneWindow实例,其布局形式即为DecorViewDecorView是一个FrameLayout布局,有标题栏(ActionBar)和内容视图(ContentView, 也就是每个活动我们所调用的函数SetContentView)
Android之自定义View学习(二)

那么现在来看,DecorView又是如何获得宽高的呢?

ViewRootImplmeasureHierarchy中有着如下一段代码,就是DecorViewMeasureSpec的赋值。

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

再来看下getRootMeasureSpec的实现:

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {

case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}

可以看出,三种模式,且都和windowSize有关,即可以理解为根视图/最外层视图的宽高是由窗口尺寸和自身LayoutParams决定的。

我们可以得出结论:

对于一般视图View,它的宽高是由父容器的MeasureSpec 和自身 LayoutParams一起决定的。
对于根视图DecorView,它的宽高是由窗口尺寸和自身 LayoutParams一起决定的。

2.3.1.3 onMeasure

根据getDefaultSize函数来给定高度或者宽度的大小,然后使用setMeasuredDimension函数来指定自定义View的(尺寸)高度、宽度

getSuggestedMinimumWidth用来获取内容或者背景尺寸二者中的较大值。

   	   public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
//父类子类不同种View 则调整宽高
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}

// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.

//宽高是否发生了变化
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;

//是否是EXACT模式
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;

//匹配
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

resolveRtlPropertiesIfNeeded();

int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}

mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}

mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;

mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

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

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
//如果当前View与父类View不是同种View
if (optical != isLayoutModeOptical(mParent)) {
//不同就要调整测量值大小
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
//直接赋值
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
//打上标识,已经测量该View的大小
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

//获取默认大小
//根据三种模式调整再次调整大小
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

//三种模式
switch (specMode) {
case MeasureSpec.UNSPECIFIED: //子类自身决定
result = size;
break;
case MeasureSpec.AT_MOST: //父类决定的两种
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
//获取内容或者背景尺寸二者中的较大值
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

上述过程就是一个单一View的测量过程,当然对于ViewGroup来说其包含多个子View,因此在ViewGroup的源码中有measureChildren来测量子View的尺寸。

   protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
//子View的测量过程
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
//getChildMeasureSpec方法,大致过程也是设置size和mode
//其中与LayoutParams.MATCH_PARENT以及 LayoutParams.WRAP_CONTENT进行匹配判断
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

2.3.2 布局View

测量好View的大小之后,performTraversals继续会调用performLayout方法,进而调用Viewlayout方法,然后onLayout方法在源码中被layout方法调用,用来在视图中给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) {
//如果发生了变化,确定View在布局中的位置
onLayout(changed, l, t, r, b);
......
}
......
}

setOptionalFramesetFrame函数的主要目的就是给View确定位置并判断位置是否变化,setOptionalFrame内核也是调用setFrame函数,因此直接分析setFrame源码。

   protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;

if (DBG) {
Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
+ right + "," + bottom + ")");
}
//判断位置是否变化,变化则需要重新绘制
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;

// Remember our drawn bit
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 our old position
invalidate(sizeChanged);

//存储新的位置并设置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
......
}
return changed;
}

对于我们来说,我们仅需在代码中重写onLayout()函数,如下。

    @Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}

接着,我们追溯到View的onLayout源码,发现…

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

???小朋友你是否有很多问号,突然想了想,好像确实应该为空,因为自定义View的位置本该就由其父布局决定,即父布局(一般继承ViewGroup)决定其子View布局(一般继承View类)。那就到ViewGroup里一探究竟吧。

    @Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}

@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);

然而貌似问号越来越多,ViewGroup的layout最关键的部分还是调用其父类Viewlayout函数(是的,ViewGroup的父类也是View)来确定自身位置。而ViewGrouponLayout函数却是一个抽象方法??

仔细考虑一下,ViewGrouponLayout()函数是抽象方法是正确的,因为每个ViewGroup都有着自己的独特布局,如LinearLayoutRelativeLayout等等对于子View的布局规则是不同的,所以写成抽象方法后方便后来继承者自定义自己的布局规则。

接下来看看LinearLayoutonLayout实现

LinearLayout有两种布局形式,以其中一种作为例子分析。

为了好理解布局中一些属性(如margin、padding等),从浏览器中抠出一张图仅供参考。

Android之自定义View学习(二)

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);//纵向线性布局
} else {
layoutHorizontal(l, t, r, b);//横向线性布局
}
}

/**
* Position the children during a layout pass if the orientation of this
* LinearLayout is set to {@link #VERTICAL}.
*
* @see #getOrientation()
* @see #setOrientation(int)
* @see #onLayout(boolean, int, int, int, int)
* @param left
* @param top
* @param right
* @param bottom
*/
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;//左内填充

int childTop;//上位置
int childLeft;//左位置

// Where right end of child should go
final int width = right - left;//宽度
int childRight = width - mPaddingRight;//去掉内填充的右位置

// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;//内容部分要用宽度去除内填充的长度
//获得子View数量 getVirtualChildCount()调用的就是getChildCount()
final int count = getVirtualChildCount();

//对齐方式,通过改变子ViewTop值
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;

// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;

case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
//循环遍历子View
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);//子View的Top是基于上一个的,nullChild的值为0
} else if (child.getVisibility() != GONE) {//这就解释了为什么GONE状态不会占用布局内容
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();

final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();

int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;

case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;

case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}

if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}

childTop += lp.topMargin;
//确定子View位置,并判断是否变化
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);//下一个子View的Top位置

i += getChildrenSkipCount(child, i);
}
}
}

最后做一个小小的实践。
布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<com.example.myviewlearning.TestLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"/>
</com.example.myviewlearning.TestLayout>

TestLayout.java如下:

public class TestLayout extends ViewGroup {

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

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

public TestLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

public TestLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
//上述四类和继承View的自定义View相同

//注意一个顺序,在Measure测量以前,getMeasureWidth和getMeasureHeight两个函数返回值为0
//这里只给了一个子View
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
//循环遍历子View
for(int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);//测量子View
}
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, childView.getMeasuredWidth() + 100, childView.getMeasuredHeight() + 100);//给子View安排位置
Log.d("View Size", "Width: "+ Integer.toString(childView.getWidth()) + " height:" + Integer.toString(childView.getHeight()));
Log.d("View MeasuredSize", "MeasureWidth: "+ Integer.toString(childView.getMeasuredWidth()) + " height:" + Integer.toString(childView.getMeasuredHeight()));
}
}
}

onLayout运行结束后,我们可以通过getWidthgetHeight方法来获取子View的高和宽。

注意:

childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());//给子View安排位置

一般来说childView.getWidth()childView.getMeasuredWidth()会相同,原因是上述给子View布局的这句代码。

getWidth() = childView.getMeasuredWidth() - 0(就是 right - left);

但是实际上两者意义是不同的。

childView.getWidth()主要是用来表示当前childView在该布局中的宽度,只有在layout过程过后才有值。

childView.getMeasuredWidth()主要是用来测量视图本身的大小,在measure之后即可获取到值。

2.3.3 绘制View

测量好View(measure),给View布局好位置(layout),ViewRoot中会继续执行调用performDraw

private void performDraw() {
......
try {
boolean canUseAsync = draw(fullRedrawNeeded);
......
}
......
}

performDraw调用自身的draw方法,在drawSoftware中创建出一个Canvas对象进行一些基本绘制(如背景颜色)并且真正的调用View类中的draw方法,传入创建的Canvas对象。

ViewRootImpl.java

private boolean draw(boolean fullRedrawNeeded) {
.......
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
}
......
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {

// Draw with software renderer.
final Canvas canvas;
......
try {
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(mTag, "Surface " + surface + " drawing to bitmap w="
+ canvas.getWidth() + ", h=" + canvas.getHeight());
//canvas.drawARGB(255, 255, 0, 0);
}

if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}

......
try {
canvas.translate(-xoff, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
attachInfo.mSetIgnoreDirtyState = false;

mView.draw(canvas);

drawAccessibilityFocusedDrawableIfNeeded(canvas);
} finally {
if (!attachInfo.mSetIgnoreDirtyState) {
// Only clear the flag if it was not set during the mView.draw() call
attachInfo.mIgnoreDirtyState = false;
}
}
} finally {
......
}
return true;
}

然后调用View的draw()方法来执行具体的开始绘制(draw)View,最后onDraw方法被View类中的draw方法调用进行内容绘制,内容绘制也是最关键的一步。

绘制过程主要分六步骤,其中第二、第五步骤相对使用较少。

    View.java
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
* 1. 绘制背景
* 2. 保存当前canvas,非必须
* 3. 绘制View的内容
* 4. 绘制子View
* 5. 绘制边缘、阴影等效果,非必须
* 6. 绘制装饰,如滚动条等
*/

// Step 1, draw the background, if needed
int saveCount;

if (!dirtyOpaque) {
drawBackground(canvas);
}

// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children
dispatchDraw(canvas);

// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);

.......
// we're done...
return;
}
......
}

private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}

setBackgroundBounds();
......
}

void setBackgroundBounds() {
if (mBackgroundSizeChanged && mBackground != null) {
mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
}

从代码中看出,第一步,背景的绘制实际上调用了一个Drawable对象mBackground进行背景绘制,然后根据layout过程确定的视图位置(mLeft mRight mTop mBottom)来设置背景的绘制区域,之后再调用onDraw方法来完成背景的绘制工作。

而这个mBackground对象其实就是在XML中通过android:background属性设置的图片或颜色。当然也可以在代码中通过setBackgroundColor()setBackgroundResource()等方法进行赋值。

跳过第二步,来到第三步骤,调用onDraw方法对View内容进行绘制,但是有了onLayout经验,这里onDraw方法同样需要被重写。

第四步,进行子View绘制处理,当然对于View来说,dispatchDraw是空方法,因为没有子View,但是对于ViewGroup来说,dispatchDraw还是比较复杂的。

跳过第五步,来到最后一步,对视图的滚动条进行装饰,从这里其实就可以看出,其实所有的控件都是有着自己的滚动条的,只不过被隐藏了起来。

最后同样来个小实践。
布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/main_layout"
tools:context=".MainActivity">

<com.example.myviewlearning.MyTestView
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="#000000"/>

</LinearLayout>

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;
int radius = Math.min(width, height)/2;

//画圆
canvas.drawCircle(paddingLeft + width/2, paddingTop + height/2, radius, mPaint);

//设置text和textColor
mPaint.setTextSize(88);
String text = "Hi, my view";
mPaint.setColor(Color.WHITE);
canvas.drawText(text,width/3, height/2, mPaint);

}

2.3.4 绘制自定义View总结

到此为止,我们就明白了,要想能够自定义一个View,少不了这三个步骤。

测量------>布局------>绘制

其他学习分享系列
数据结构与算法系列
数据结构与算法之哈希表
数据结构与算法之跳跃表
数据结构与算法之字典树
数据结构与算法之2-3树
数据结构与算法之平衡二叉树
数据结构与算法之十大经典排序
数据结构与算法之二分查找三模板

如有兴趣可以关注我的微信公众号,每周带你学一点算法与数据结构。
Android之自定义View学习(二)

原创:https://www.panoramacn.com
源码网提供WordPress源码,帝国CMS源码discuz源码,微信小程序,小说源码,杰奇源码,thinkphp源码,ecshop模板源码,微擎模板源码,dede源码,织梦源码等。

专业搭建小说网站,小说程序,杰奇系列,微信小说系列,app系列小说

Android之自定义View学习(二)

免责声明,若由于商用引起版权纠纷,一切责任均由使用者承担。

您必须遵守我们的协议,如果您下载了该资源行为将被视为对《免责声明》全部内容的认可-> 联系客服 投诉资源
www.panoramacn.com资源全部来自互联网收集,仅供用于学习和交流,请勿用于商业用途。如有侵权、不妥之处,请联系站长并出示版权证明以便删除。 敬请谅解! 侵权删帖/违法举报/投稿等事物联系邮箱:2640602276@qq.com
未经允许不得转载:书荒源码源码网每日更新网站源码模板! » Android之自定义View学习(二)
关注我们小说电影免费看
关注我们,获取更多的全网素材资源,有趣有料!
120000+人已关注
分享到:
赞(0) 打赏

评论抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

您的打赏就是我分享的动力!

支付宝扫一扫打赏

微信扫一扫打赏