Android自定义View和Canvas绘图解析

Android自定义View和Canvas绘图解析
Android自定义View和Canvas绘图解析

Android自定义View和Canvas绘图解析

自定义view的流程分为measure、layout、draw三个主要步骤,今天我们通过源码来分下下measure的过程

我们从顶级view开始,顶级view即DecorView,view的事件都是先经过这个DecorView, 接下来我们来看看这个DecorView的MeasureSpec的创建过程:ViewRoot 对应ViewRootImpl类,是连接WindowManager 和DecorView的纽带,进入ViewRootImpl中,查看measureHierarchy方法,有如下代码:

final DisplayMetrics packageMetrics = res.getDisplayMetrics();

res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);

int baseSize = 0;

if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {

baseSize = (int)mTmpValue.getDimension(packageMetrics);

}

if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": baseSize=" + baseSize

+ ", desiredWindowWidth=" + desiredWindowWidth);

if (baseSize != 0 && desiredWindowWidth > baseSize) {

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);

childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

这里只是截选一部分的源码,我们看到这个baseSize,其实就是屏幕的尺寸大小,获取宽的MeasureSpc的方法:

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);

这里传入的参数是屏幕尺寸以及DecorView自身的大小,接着我们来看getRootMeasureSpec方法:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {

int measureSpec;

switch (rootDimension) {

case https://www.360docs.net/doc/9317054719.html,youtParams.MATCH_PARENT:

// Window can't resize. Force root view to be windowSize.

measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);

break;

case https://www.360docs.net/doc/9317054719.html,youtParams.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;

}

就是这个方法确定了DecorView的MeasureSpec,这里分了三种情况,

1.如果传入的view大小为math_parent,那么这个view的mode为EXACTLY,大小为屏幕的尺寸.

2.如果传入的view大小为wrap_content,那么这个view的mode为AT_MOST,大小为屏幕的尺寸.

3.如果传入的view大小为一个具体的值,那么这个view的mode为EXACTLY,大小为view本身大小。

以上就是DecorView的MeaureSpec的整个创建的过程了。

看了顶级view之后我们来看普通的view,普通的view的measure过程是由viewgroup传递过来的,接着我们来看看viewgroup的measureChildWithMargins方法:

protected void measureChildWithMargins(View child,

int parentWidthMeasureSpec, int widthUsed,

int parentHeightMeasureSpec, int heightUsed) {

final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,

mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin

+ widthUsed, lp.width);

final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,

mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin

+ heightUsed, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}

这个方法获得了子view的MeasureSpec,并且将其传入子view的measure方法中,这里重点来看下viewgroup是如何创建子view的MeasuerSpec的。来看getChildMeasureSpec 方法内部的实现:

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;

// Parent has imposed an exact size on us

case MeasureSpec.EXACTLY:

if (childDimension >= 0) {

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it.

resultSize = size;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be

// bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

}

break;

// Parent has imposed a maximum size on us

case MeasureSpec.AT_MOST:

if (childDimension >= 0) {

// Child wants a specific size... so be it

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed.

// Constrain child to not be bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

} else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be

// bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

}

break;

// Parent asked to see how big we want to be

case MeasureSpec.UNSPECIFIED:

if (childDimension >= 0) {

// Child wants a specific size... let him have it

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should

resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

resultMode = MeasureSpec.UNSPECIFIED;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

// Child wants to determine its own size.... find out how

// big it should be

resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

resultMode = MeasureSpec.UNSPECIFIED;

}

break;

}

//noinspection ResourceType

return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

}

这个方法很长,但是我们只需要注意到AT_MOST跟EXACTLY这两种情况就行,稍微分析下这个过程:

首先要理解这个方法的三个参数,第一个是父view的MeasureSpec, 第二个是父view已占用的大小,第三个是view的LayoutParams的大小,如果不理解可以看看ViewGroup 的MeasureChildWithMargins方法中的调用:

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,

mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin

+ widthUsed, lp.width);

第二个参数很长,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + withUsed ,这些所有的值都有一个共同特点,就是这些位置是不能摆放任何view的,即父view已经占用的地盘,现在是不是对参数更加理解了呢。

接着我们回到getChildMeasureSpec方法中继续看看viewGroup到底是怎么创建view的MeasureSpec的。

第一步:根据参数一,即传入的父view的MeasureSpec获得父view的Mode和Size。这里的第三行代码:

int size = Math.max(0, specSize - padding);

这个size表示取0与父容器中可占用的位置的最大值,可以直接理解为父view的大小。

第二步:根据父view的Mode分情况处理,到这一步我们应该就清楚为什么说view的大小是由父view的MeasureSpec与本身LayoutParmas大小共同决定的吧。

这里我们依然只看AT_MOST跟EXACTLY两种情况,

switch (specMode) {

// Parent has imposed an exact size on us

case MeasureSpec.EXACTLY:

if (childDimension >= 0) {

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {

// Child wants to be our size. So be it.

resultSize = size;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

// Child wants to determine its own size. It can't be

// bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

}

break;

// Parent has imposed a maximum size on us

case MeasureSpec.AT_MOST:

if (childDimension >= 0) {

// Child wants a specific size... so be it

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {

// Child wants to be our size, but our size is not fixed.

// Constrain child to not be bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

// Child wants to determine its own size. It can't be

// bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

}

break;

这里有个比较难以理解的值就是childDimension > 0 ,这个其实就表示view的大小是一个具体的值比如100dp , 因为view的match_parent和wrap_content在系统内部定义的都是负数,一个是-1. 一个是-2 ,所以判断childDimension > 0即,view的大小为一个具体的值。

接着就比较好理解了,我们来稍微总结下:

无论父view是match_parent还是wrap_content ,只要view是一个具体的值,view的Mode永远都是EXACTLY, 大小均是view本身定义的大小。

父view模式如果是EXACTLY, ---> 子view如果是mathch_parent ,那么子view的大小是父view的大小,模式也跟父view一样为EXACTLY. 子view如果是wrap_content,大小还是父view的大小,模式为AT_MOST

父view模式如果是AT_MOST , --- > 子view如果是math_parent,那么子view大小为父view大小,模式与父view一样都是AT_MOST, 子view如果是wrap_content,子

view大小为父view大小,模式为AT_MOST

上面说的有点绕,但其实我们只需要记住一点,无论上面那种情况,子view在wrap_content下,大小都是父view的大小,到这里我们是不是就能理解为什么在自定义view 的过程中如果不重写onMeasure,wrap_content是和match_parent是一个效果了吧。

以上过程是viewGroup中创建子view的MeasureSpec的过程,有了这个MeasureSpec,测量子view大小就很简单了,我们可以看到在ViewGroup获取到子view的MeasureSpec 之后,传入到子view的measure方法中:

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

进入到view的measure方法,

不知不觉我们已经从viewgroup进入到了view的测量过程,

这里是不是突然意识到,ViewGroup根本没有测绘自己本身啊,只是获取到子view的MeasureSpec然后传入子view的measure方法里去,这是因为ViewGroup是个抽象类,本身并没有定义测量的过程,ViewGroup的onMeasure需要各个子类去实现,比如LinearLayout 、RelativeLayout等等,并且每个子类的测量过程都不一样,这个我们后面会讲,现在我们还是接着看view的Measure过程。

上面说到viewgroup将创建的子view的MeasureSpec传入到了view的Measure方法中,那么我们就来看看View的Measure方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

boolean optical = isLayoutModeOptical(this);

if (optical != isLayoutModeOptical(mParent)) {

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;

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

}

这个方法真是又臭又长。。。讲道理的话其实我也看不懂,但是我们只需要注意到一点,就是这个方法调用了OnMeasure方法!

也就是说measure --> OnMeasure

OnMeasure就简单了:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}

很简洁对不,但是简洁并不代表简单,这里套了好几层。。。不要被迷惑,我们看最外层其实就是setMeasureDimension().

设置宽和高,这个宽和高是在getDefaultSize方法里返回的,所以我们来看看getDefaultSize的具体代码:

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;

}

如果我们忽略掉UNSPECIFIED情况的话,我们会发现第一个参数size根本用不到。。。

也就是说view的大小其实就是父view给他创建的MeasureSpec中的size大小。

这也进一步说明,view在wrap_content情况下,大小还是会跟父view大小一样,所以我们需要在自定义view的时候重写OnMeasure。//为了支持wrap_content,一般的实现方法如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

super.onMeasure(widthMeasureSpec , heightMeasureSpec);

int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);

int widthSpceSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);

int heightSpceSize=MeasureSpec.getSize(heightMeasureSpec);

if(widthSpecMode==MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(mWidth, mHeight);

}else if(widthSpecMode == MeasureSpec.AT_MOST){

setMeasuredDimension(mWidth, heightSpceSize);

}else if(heightSpecMode == MeasureSpec.AT_MOST){

setMeasuredDimension(widthSpceSize, mHeight);

}

}

Layout:

直接看view的layout源码:

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);

mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

ListenerInfo li = mListenerInfo;

if (li != null && li.mOnLayoutChangeListeners != null) {

ArrayList listenersCopy =

(ArrayList)li.mOnLayoutChangeListeners.clone();

int numListeners = listenersCopy.size();

for (int i = 0; i < numListeners; ++i) {

listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);

}

}

}

mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;

mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

}

这里挑重点来看

boolean changed = isLayoutModeOptical(mParent) ?

setOpticalFrame(l, t, r, b) : setFrame(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);

}

点进setOpticalFrame我们发现最终也是调用的setFrame方法,所以我们直接来看这个方法:

protected boolean setFrame(int left, int top, int right, int bottom) {

boolean changed = false;

if (DBG) {

Log.d("View", 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);

mPrivateFlags |= PFLAG_HAS_BOUNDS;

if (sizeChanged) {

sizeChange(newWidth, newHeight, oldWidth, oldHeight);

}

if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {

// If we are visible, force the DRAWN bit to on so that

// this invalidate will go through (at least to our parent).

// This is because someone may have invalidated this view

// before this call to setFrame came in, thereby clearing

// the DRAWN bit.

mPrivateFlags |= PFLAG_DRAWN;

invalidate(sizeChanged);

// parent display list may need to be recreated based on a change in the bounds

// of any child

invalidateParentCaches();

}

// Reset drawn bit to original value (invalidate turns it off)

mPrivateFlags |= drawn;

mBackgroundSizeChanged = true;

if (mForegroundInfo != null) {

mForegroundInfo.mBoundsChanged = true;

}

notifySubtreeAccessibilityStateChangedIfNeeded();

}

return changed;

}

看到这句:

if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {

changed = true;

在该方法中把l,t,r,b分别与之前的mLeft,mTop,mRight,mBottom一一作比较,假若其中任意一个值发生了变化,那么就判定该View的位置发生了变化if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

onLayout(changed, l, t, r, b);

若发生变化则会调用onLayout方法。

接着我们来看View的onLayout方法:

/**

* Called from layout when this view should

* assign a size and position to each of its children.

*

* Derived classes with children should override

* this method and call layout on each of

* their children.

* @param changed This is a new size or position for this view

* @param left Left position, relative to parent

* @param top Top position, relative to parent

* @param right Right position, relative to parent

* @param bottom Bottom position, relative to parent

*/

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

}

居然是空的!

查看注释我们发现View的onLayout是确定子view的位置的,所以我们直接来看viewGroup的onLayout方法:

@Override

protected abstract void onLayout(boolean changed,

int l, int t, int r, int b);

居然是个抽象方法!

到这里我们发现view和viewGroup都没有真正实现onLayout方法。

既然ViewGroup中的方法是抽象方法,那么子类就一定会重写这个方法,我们来看LinearLayout:

@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);

}

果然重写了,并且分为水平跟垂直的两种情况

随便挑一个来看看

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;

final int count = getVirtualChildCount();

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;

}

for (int i = 0; i < count; i++) {

final View child = getVirtualChildAt(i);

if (child == null) {

childTop += measureNullChild(i);

} else if (child.getVisibility() != GONE) {

final int childWidth = child.getMeasuredWidth();

final int childHeight = child.getMeasuredHeight();

final https://www.360docs.net/doc/9317054719.html,youtParams lp =

(https://www.360docs.net/doc/9317054719.html,youtParams) 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;

setChildFrame(child, childLeft, childTop + getLocationOffset(child),

childWidth, childHeight);

childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

i += getChildrenSkipCount(child, i);

}

}

}

这里简单的分析下layoutVertical的逻辑,首先遍历所有子元素并调用setChildFrame方法为子元素指定对应的位置,其中childTop在不断的增大,这就意味着越后面的子元素位置就越靠下,刚好符合垂直linearLayout的特性。

平时用的最多的就是canvas里的各种绘图api, 以及一些关于画布的操作

canvas.save和canvas.restore

网上有一种说法叫:save跟restore一般都是成对出现,但是restore不能比save多,否则会抛异常。但是我在测试的时候发现restore即使比save多也没有出现异常。rotate(float degrees)

画布旋转,值为正顺时针,负逆时针。

Canvas的图层概念:

for(int i=0; i < 5; i++) {

canvas.drawCircle(50, 50, 50, mPaint);

canvas.translate(100, 100);}

如图画布的坐标原点每次分别在x轴、y轴上移动100 ,那么假如我们要重新回到(0 ,0)点处绘制新的图形呢,不会要translate( -100 ,-100) 慢慢的平移过去吧,

我们在平移之前可以将当前的canvas状态进行保存,canvas为我们提供了图层的支持,而这些图层是按栈结构来进行管理的,当我们调用save()方法,会保存当前Canvas 的状态后最为一个Layer(图层) ,添加到Canvas栈中,另外这个Layer不是一个具体的类,就是一个概念性的东西而已,而当我们调用restore()方法的时候,会恢复之前canvas的状态,而此时Canvas的图层栈会弹出栈顶那个layer

PorterDuffXfermode

public Bitmap getRoundCornerBitmap(Bitmap bitmap, float pixels) {

//生成Canvas ,给canvas设置的Bitmap的大小是和原图的大小一致

int width=bitmap.getWidth();

int height=bitmap.getHeight();

Bitmap roundCornerBitmap = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);

Canvas canvas = new Canvas(roundCornerBitmap);

//绘制圆角矩形

Paint paint = new Paint();

paint.setColor(Color.BLACK);

paint.setAntiAlias(true);

Rect rect = new Rect(0, 0, width, height);

RectF rectF = new RectF(rect);

canvas.drawRoundRect(rectF, pixels, pixels, paint);

//为paint设置PorterDuffXfermode

PorterDuffXfermode xfermode=new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);

paint.setXfermode(xfermode);

//绘制原图

canvas.drawBitmap(bitmap, rect, rect, paint);

return roundCornerBitmap;

}

Bitmap和Matrix

除了刚才提到的给图片设置圆角之外,在开发中还常有其他涉及到图片的操作,比如图片的旋转,缩放,平移等等,这些操作可以结合Matrix来实现。在此举个例子,看看利用matrix实现图片的平移和缩放。

private void drawBitmapWithMatrix(Canvas canvas){

//画出原图

Paint paint = new Paint();

paint.setAntiAlias(true);

Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.mm); i

nt width=bitmap.getWidth();

int height=bitmap.getHeight();

Matrix matrix = new Matrix();

canvas.drawBitmap(bitmap, matrix, paint);

//平移原图

matrix.setTranslate(width/2, height);

canvas.drawBitmap(bitmap, matrix, paint);

//缩放原图

matrix.postScale(0.5f, 0.5f);

canvas.drawBitmap(bitmap, matrix, paint); }

利用Matrix对图形操作是跟坐标系无关的,操作的是每个像素点,比如平移缩放每个像素点

matrix.postScale(0.5f, 0.5f);

如上代码,表示对每个像素点缩放到原来的一半大小

在使用Matrix时经常用到一系列的set,pre,post方法

pre表示在队头插入一个方法

post表示在队尾插入一个方法

队列中只保留该set方法,其余的方法都会清除。

下面请看几个小示例:

1.

Matrix m = new Matrix();

m.setRotate(45);

m.setTranslate(80, 80);

只有m.setTranslate(80, 80)有效,因为m.setRotate(45)被清除.

2.

Matrix m = new Matrix();

m.setTranslate(80, 80);

m.postRotate(45);

先执行m.setTranslate(80, 80)后执行m.postRotate(45)

3.

Matrix m = new Matrix();

m.setTranslate(80, 80);

m.preRotate(45);

先执行m.preRotate(45)后执行m.setTranslate(80, 80)

4.

Matrix m = new Matrix();

m.preScale(2f,2f);

m.preTranslate(50f, 20f);

m.postScale(0.2f, 0.5f);

m.postTranslate(20f, 20f);

执行顺序:

m.preTranslate(50f, 20f)–>m.preScale(2f,2f)–>m.postScale(0.2f, 0.5f)–>m.postTranslate(20f, 20f) 5.

Matrix m = new Matrix();

m.postTranslate(20, 20);

m.preScale(0.2f, 0.5f);

m.setScale(0.8f, 0.8f);

m.postScale(3f, 3f);

m.preTranslate(0.5f, 0.5f);

m.preTranslate(0.5f, 0.5f)–>m.setScale(0.8f, 0.8f)–>m.postScale(3f, 3f) Demo1:

/**

* Created by Administrator on 2016/12/17.

*

* 仿支付宝芝麻信用圆形仪表盘

*/

public class ERoundIndicatorView extends View {

private Paint paint;

private Paint paint_2;

private Paint paint_3;

private Paint paint_4;

private Context context;

private int maxNum;

private int startAngle;

private int sweepAngle;

private int radius;

private int mWidth;

private int mHeight;

private int sweepInWidth;//内圆的宽度

private int sweepOutWidth;//外圆的宽度

private int currentNum=0;//需设置setter、getter 供属性动画使用private String[] text ={"较差","中等","良好","优秀","极好"};

private int[] indicatorColor = {0xffffffff,0x00ffffff,0x99ffffff,0xffffffff};

public int getCurrentNum() {

return currentNum;

}

public void setCurrentNum(int currentNum) {

this.currentNum = currentNum;

invalidate();

}

public ERoundIndicatorView(Context context) {

this(context,null);

}

public ERoundIndicatorView(Context context, AttributeSet attrs) { this(context, attrs,0);

}

public ERoundIndicatorView(final Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr);

this.context = context;

setBackgroundColor(0xFFFF6347);

initAttr(attrs);

initPaint();

}

public void setCurrentNumAnim(int num) {

float duration = (float)Math.abs(num-currentNum)/maxNum *1500+500; //根据进度差计算动画时间ObjectAnimator anim = ObjectAnimator.ofInt(this,"currentNum",num);

anim.setDuration((long) Math.min(duration,2000));

anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

int value = (int) animation.getAnimatedValue();

int color = calculateColor(value);

setBackgroundColor(color);

}

});

anim.start();

}

private int calculateColor(int value){

ArgbEvaluator evealuator = new ArgbEvaluator();

float fraction = 0;

int color = 0;

if(value <= maxNum/2){

fraction = (float)value/(maxNum/2);

color = (int) evealuator.evaluate(fraction,0xFFFF6347,0xFFFF8C00); //由红到橙}else {

fraction = ( (float)value-maxNum/2 ) / (maxNum/2);

color = (int) evealuator.evaluate(fraction,0xFFFF8C00,0xFF00CED1); //由橙到蓝}

return color;

}

private void initPaint() {

paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setDither(true);

paint.setStyle(Paint.Style.STROKE);

paint.setColor(0xffffffff);

paint_2 = new Paint(Paint.ANTI_ALIAS_FLAG);

paint_3 = new Paint(Paint.ANTI_ALIAS_FLAG);

paint_4 = new Paint(Paint.ANTI_ALIAS_FLAG);

}

private void initAttr(AttributeSet attrs) {

TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.RoundIndicatorView);

maxNum = array.getInt(R.styleable.RoundIndicatorView_maxNum,500);

startAngle = array.getInt(R.styleable.RoundIndicatorView_startAngle,160);

sweepAngle = array.getInt(R.styleable.RoundIndicatorView_sweepAngle,220);

//内外圆的宽度

sweepInWidth = dp2px(8);

sweepOutWidth = dp2px(3);

array.recycle();

}

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec);

int wSize = MeasureSpec.getSize(widthMeasureSpec);

int wMode = MeasureSpec.getMode(widthMeasureSpec);

int hSize = MeasureSpec.getSize(heightMeasureSpec);

int hMode = MeasureSpec.getMode(heightMeasureSpec);

if (wMode == MeasureSpec.EXACTLY ){

mWidth = wSize;

}else {

mWidth =dp2px(300);

}

if (hMode == MeasureSpec.EXACTLY ){

mHeight= hSize;

}else {

mHeight =dp2px(400);

}

setMeasuredDimension(mWidth,mHeight);

}

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

radius = getMeasuredWidth()/4; //不要在构造方法里初始化,那时还没测量宽高

canvas.save();

canvas.translate(mWidth/2,(mWidth)/2);

drawRound(canvas); //画内外圆

drawScale(canvas);//画刻度

drawIndicator(canvas); //画当前进度值

android 自定义圆角头像以及使用declare-styleable进行配置属性解析

android 自定义圆角头像以及使用declare-styleable进行配置属性解析由于最新项目中正在检查UI是否与效果图匹配,结果关于联系人模块给的默认图片是四角稍带弧度的圆角,而我们截取的图片是正方形的,现在要给应用统一替换。应用中既用到大圆角头像(即整个头像是圆的)又用到四角稍带弧度的圆角头像,封装一下以便重用。以下直接见代码 [java] view plain copy 在CODE上查看代码片派生到我的代码片 package com.test.demo; import com.test.demo.R; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Shader.TileMode; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.widget.ImageView; /** * 圆角imageview */ public class RoundImageView extends ImageView { private static final String TAG = "RoundImageView"; /** * 图片的类型,圆形or圆角 */ private int type; public static final int TYPE_CIRCLE = 0; public static final int TYPE_ROUND = 1; /** * 圆角大小的默认值

Android开发规范参考文档

Android开发参考文档 一、Android编码规范 1. java代码中不出现中文,最多注释中可以出现中文.xml代码中注释 2. 成员变量,局部变量、静态成员变量命名、常量(宏)命名 1). 成员变量: activity中的成员变量以m开头,后面的单词首字母大写(如Button mBackButton; String mName);实体类和自定义View的成员变量可以不以m开头(如ImageView imageView,String name), 2). 局部变量命名:只能包含字母,组合变量单词首字母出第一个外,都为大写,其他字母都为小写 3). 常量(宏)命名: 只能包含字母和_,字母全部大写,单词之间用_隔开UMENG_APP_KEY 3. Application命名 项目名称+App,如SlimApp,里面可以存放全局变量,但是杜绝存放过大的实体对象4. activity和其中的view变量命名 activity命名模式为:逻辑名称+Activity view命名模式为:逻辑名称+View 建议:如果layout文件很复杂,建议将layout分成多个模块,每个模块定义一个moduleViewHolder,其成员变量包含所属view 5. layout及其id命名规则 layout命名模式:activity_逻辑名称,或者把对应的activity的名字用“_”把单词分开。

命名模式为:view缩写_模块名称_view的逻辑名称, 用单词首字母进行缩写 view的缩写详情如下 LayoutView:lv RelativeView:rv TextView:tv ImageView:iv ImageButton:ib Button:btn 6. strings.xml中的 1). id命名模式: activity名称_功能模块名称_逻辑名称/activity名称_逻辑名称/common_逻辑名称,strings.xml中,使用activity名称注释,将文件内容区分开来 2). strings.xml中使用%1$s实现字符串的通配,合起来写 7. drawable中的图片命名 命名模式:activity名称_逻辑名称/common_逻辑名称/ic_逻辑名称 (逻辑名称: 这是一个什么样的图片,展示功能是什么) 8. styles.xml 将layout中不断重现的style提炼出通用的style通用组件,放到styles.xml中; 9. 使用layer-list和selector,主要是View onCclick onTouch等事件界面反映

Android平台我的日记设计文档

Android平台我的日记 设计文档 项目名称:mydiray 项目结构示意: 阶段任务名称(一)布局的设计 开始时间: 结束时间: 设计者: 梁凌旭 一、本次任务完成的功能 1、各控件的显示 二、最终功能及效果 三、涉及知识点介绍 四、代码设计 activity_main.xml:

android:layout_centerHorizontal="true" android:layout_marginTop="88dp" android:text="@string/wo" android:textSize="35sp"/>

相关文档
最新文档