Androd - 自定义view了解与应用

文章整理中… …

1. View工作原理了解

onAttachedToWindow
onMeasure
onSizeChanged
onLayout
onDraw

从Android 的 FrameLayout 看 测量,布局,绘制 的自定义view的世界吧.
整个工作流程的核心 ViewRootImpl(ViewRoot) 的 performTraversals 开始的,
分别是 performMeasure->measure,performLayout->layout,performDraw->draw.

// ViewRootImpl.java
private void performTraversals() {
    ... ...
    if (!mStopped || mReportNextDraw) {
        ... ...
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 测量measure的关键函数    
        ... ...
    } 
    ... ...
    if (didLayout) {
        performLayout(lp, desiredWindowWidth, desiredWindowHeight); // 布局layout的关键函数
        ...
    }
    ... ...
    if (!cancelDraw && !newSurface) {
        if (!skipDraw || mReportNextDraw) {
            ... ...
            performDraw(); // 绘制 draw 的关键函数
        }
    } 
}

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
    final View host = mView;
    host.layout(... ...)
}

private void performDraw() {
    ... ...
    draw(fullRedrawNeeded)->drawSoftware->mView.draw
    ... ...
}

mView 是根视图 DecorView(继承关系)->FrameLayout->ViewGroup->View,看看下图的布局关系(转自网上的图片)
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

// 获取上层的视图view 的相关代码,了解下.
getWindow().getDecorView()
findViewById(android.R.id.content)

来看看整体的performTraversals调用过程 的 流程图
在这里插入图片描述

1.1 View测量measure

measure 流程了解下:
ViewRootImpl.performTraversals,performTraversals函数里面执行了 performMeasure

// ViewRootImpl.java
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    // mView 是 DecorView(View树的根节点是DecorView), 调用 measure
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

FrameLayout 的 onMeasure 中 调用measure相关函数的,我们先看看

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // measureMatchParentChildren,false 表示 width 与 height 同时设置了 match_parent 或者指定了大小, true 反之.
    final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    ... ...
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            // 当子控件为 wrap_content
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            ... ...
        }
    }
    ... ...
    // 设置 FrameLayout 的宽高
    setMeasuredDimension(
        resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
        resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
    ... ...
    //  自身是不确定大小的模式,子view又是MATCH_PARENT属性的,就需要为这些子view重新测量。
    for (int i = 0; i < count; i++) {
        if (lp.width == LayoutParams.MATCH_PARENT) {
            ... ...
            childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
        } else {
            childWidthMeasureSpec = getChildMeasureSpec(
                widthMeasureSpec,
                getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                lp.leftMargin + lp.rightMargin,
                lp.width);
        }
        
        final int childHeightMeasureSpec;
        if (lp.height == LayoutParams.MATCH_PARENT) {
            ... ...
            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        } else {
            childHeightMeasureSpec = getChildMeasureSpec(
                heightMeasureSpec,
                getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                lp.topMargin + lp.bottomMargin,
                lp.height);
        }
        // 子控件调用 measure,层层调用下去.
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
}

widthMeasureSpec 与 heightMeasureSpec 是一个int值(java中int型变量由4个字节(32bit)组成),
其中 高2位 用来保存 MeasureMode,低30位 用来保存 size.
MeasureSpec有3种模式:

  • UNSPECIFIED # 父容器不对子view的大小做限制,一般用于系统内部,或者ListView ScrollView等滑动控件。
  • EXACTLY # 父控件已为子控件确定了一个确切的大小,孩子将被给予这些界限,不管子控件自己希望的是多大,对应于 match_parent 和 具体的值(比如 android:layout_width=“200px”),父容器测量出 View所需要的大小,也就是SpecSize的值。
  • AT_MOST # 在此模式下,父容器未能检测出子view的大小,但指定了一个最大大小spec size,子view的大小不能超过此值。父控件会给子控件尽可能大的尺寸,对应于 wrap_comtent, 子view 的最终大小是父View指定的SpecSize值, 并且子view的大小小于这个值.

MeasureSpec 常用的三个函数:

  • makeMeasureSpec # 根据提供的 size 和 mode 创建一个 测量值
  • getMode # 从所提供的测量值 中 提取 模式mode
  • getSize # 从所提供的测量值 中提取 尺寸size
View:
setMeasuredDimension // 设置宽高的,基本上都会使用,很多自定义控件或者Android原生控件
measure

resolveSizeAndState // 
getDefaultSize  //
combineMeasuredStates

View.MeasureSpec:
MeasureSpec.makeMeasureSpec // 根据所提供的size和mode创建一个测量规范

ViewGroup:
measureChildWithMargins  // 子view,多宽,多高, 内部加上了viewGroup的padding值、子view的margin值和传入的宽高wUsed、hUsed  (padding + margin + used) ,把 margin 及  padding 也作为子视图大小的一部分
measureChild  // 某一个子view,多宽,多高, 内部加上了viewGroup的padding值
measureChildren // 循环的调用所有子child的 measureChild
getChildMeasureSpec // 给子view 计算出正确的 MeasureSpec
    
RecyclerView: 为了适应自己的那套东西,重写了很多函数,比如 getChildMeasureSpec等,也新增了很多函数,比如 measureChildWithMargins等

// 在自定义控件的时候,就看自己的需求和定义吧,调用相应的函数或者自己重写,新增都可以.

getChildMeasureSpec 的规则(具体可以查看源码,了解下就好了,调用就行了)
在这里插入图片描述


<FrameLayout
    android:background="#000000"
    // android:padding="50px"
    android:layout_width="500px"
    android:layout_height="500px">
    <Button
        android:text="textTest"
        // android:layout_margin="50px"
        // android:layout_width="wrap_content"
        // android:layout_height="wrap_content" 
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

// FrameLayout.onMeasure->measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
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);
        ... ...
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

// 加上父控件的 mPaddingLeft + mPaddingRight + 还有子控件的 lp.leftMargin + lp.rightMargin
// 传入 getChildMeasureSpec padding参数.
// specSize - padding,得到子控件 初步的 size,还需要 用 specMode 进行相应的操作,
// 最后才会选择 size 还是 childDimension.
// 假设前提: 并且父控件有确切的大小,属于 MeasureSpec.EXACTLY
// 1. 没有 padding, margin,子控件为 match_parent,那么子控件 resultSize = 500px; resultMode = MeasureSpec.EXACTLY;
// 2. 父控件的 padding, button 的 margin 为 50px,那么子控件 resultSize = 300px; resultMode = MeasureSpec.EXACTLY;
// 3. 没有 padding, marigin,子控件为 wrap_content,那么子控件 resultSize = 500px; resultMode = MeasureSpec.AT_MOST;
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) {
    case MeasureSpec.EXACTLY: 
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    ... ...
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}


// 默认 View 的 onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

没有 padding, marigin,子控件为 wrap_content,
那么子控件 resultSize = 500px; resultMode = MeasureSpec.AT_MOST 的 demo.
为何 button不占满全屏,因为传递给 button 的 widthMeasureSpec 或 heightMeasureSpec,基本属于 Size=500px, mode=MeasureSpec.AT_MOST.
因为button继承的 TextView,内部的 onMeasure 进行了处理的(代码可以自行查看).
在这里插入图片描述

// 我将 button 的 widthMeasureSpec 与 heightMeasureSpec 进行了一个处理
public class TestButton extends Button {
    ... ...
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

}

// 看了代码我们知道,父控件给 修改后的 TestButton 传递的 mode 是 MeasureSpec.AT_MOST,size 为 500px,
// 因为 getDefaultSize 进行了处理,result = specSize; 所以最后占满了父控件.
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;
}

最后 button占满了父控件

在这里插入图片描述

getWidth()和getMeasuredWidth()的区别
getMeasuredWidth(): 只要一执行完 setMeasuredDimension()方法, 就有值了, 并且不再改变.

getWidth(): 必须执行完 onMeasure() 才有值, 可能发生改变.

如果 onLayout 没有对子 View 实际显示的宽高进行修改, 那么 getWidth() 的值 == getMeasuredWidth() 的值.

1.2. View布局layout

measure完成之后,就会调用 layout.
从 ViewRootImpl 的 performTraversals->performLayout 函数开始,然后 DecorView.layout

// ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
    final View host = mView;
    ... ...
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ... ...
}

layout相对 measure 操作就简单一些了,将子控件放在合适的位置上.

// View.java
public void layout(int l, int t, int r, int b) {
    ... ...
    // 1. 调用 setFrame 位置保存起来,设定 view的四个位置(left, right, top, bottom).
    boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    ... ...
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        // 2. 子控件进行位置分配,一般是 继承ViewGroup后的控件 实现onLayout,比如FrameLayout等.
        onLayout(changed, l, t, r, b);
        // 3. 清除 PFLAG_LAYOUT_REQUIRED 标识,layout完成了操作.
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
        ... ...
    }
}

// ViewGroup.java 将 onLayout 设置成了 abstract 类型,所以,继承 ViewGroup 都需要实现 onLayout,比如 FrameLayout等.
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

// FrameLayout.java
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    ... ...
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            ... ...
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
    ... ...
}

看下整体的流程,不断的一层层往下调用.
在这里插入图片描述
这里就不过的讲解 layout, onLayout 具体可以参考 Android原生继承了 ViewGroup 实现 onLayout的控件.

1.3. View绘制流程

从 ViewRootImpl 的 performTraversals->performDraw 函数开始,然后 DecorView.draw

// DecorView,调用了 View 的 draw 函数.
@Override
 public void draw(Canvas canvas) {
     super.draw(canvas);
     ... ...
 }

看看 View.draw 函数做了一些什么事情,具体关注4个关键的函数.

1. 绘制背景(颜色值或图片,Drawable对象,比如 Shader, DrawableState等),
调用 setBackgroundColor, setBackgroundDrawable, setBackgroundResouce设置不同的背景.
2. 绘制自身内容,比如 ImageView(绘制图片), TextView(绘制文字), LinearLayout(Divider 分割线)
3. 绘制子控件,遍历所有子控件的 draw方法,一层层调用下去.
4. 绘制装饰 (foreground, scrollbars)

public void draw(Canvas canvas) {
    if (!dirtyOpaque) {
        drawBackground(canvas); // 1. 绘制背景
    }
    ... ...
    if (!verticalEdges && !horizontalEdges) {
        if (!dirtyOpaque) onDraw(canvas); // 2. 绘制自身内容
        dispatchDraw(canvas); // 3. 绘制子控件
        ... ...
        onDrawForeground(canvas); // 4. 绘制装饰
        ... ...
    }
    ... ...
}

虚线代表了 Override,比如 ImageView
在这里插入图片描述
TV开发经常要对 继承了ViewGroup上面onDraw绘制东西,是无法显示出来的,
是因为在Viewgroup 调用 initViewGroup,函数里面 setFlags(WILLL_NOT_DRAW,DRAW_MASK),相当于调用了setWillNotDraw(true),
如果想绘制内容->onDraw,使用 setWillNotDraw(false) 才能显示内容出来.
这就是为何在一些继承了ViewGroup的自定义控件上,想要绘制阴影,内容等 无法显示出来的原因以及解决方案.

// ViewGroup.java
private void initViewGroup() {
    setFlags(WILL_NOT_DRAW, DRAW_MASK);
    ... ...
}

TV开发中还会遇到 放大被挡住的问题,如何进行处理:
第一种方式 修改绘制顺序:

//ViewGroup.java
setChildrenDrawingOrderEnabled(true);

private int position = 0;

// 也可以重写此函数,bringToFront 调用的绘制顺序就被更改.
public void bringChildToFront(ViewGroup vg, View child) {
		position = vg.indexOfChild(child);
		if (position != -1) {
			vg.postInvalidate();
		}
}

	/**
	 * 此函数 dispatchDraw 中调用. <br>
	 * 原理就是和最后一个要绘制的view,交换了位置. <br>
	 * 因为dispatchDraw最后一个绘制的view是在最上层的. <br>
	 * 这样就避免了使用 bringToFront 导致焦点错乱问题. <br>
	 */
public int getChildDrawingOrder(int childCount, int i) {
	if (position != -1) {
		if (i == childCount - 1) {
			return position;
		}
		if (i == position) {
			return childCount - 1;
		}
	}
	return i;
}

RecyclerView的绘制顺序,可以参考Leanback的.
int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
        View view = findViewByPosition(mFocusPosition);
        if (view == null) {
            return i;
        }
        int focusIndex = recyclerView.indexOfChild(view);
        // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
        // drawing order is 0 1 2 3 9 8 7 6 5 4
        if (i < focusIndex) {
            return i;
        } else if (i < childCount - 1) {
            return focusIndex + childCount - 1 - i;
        } else {
            return focusIndex;
        }
}

第二种方式调用:bringToFront 函数. 这个函数建议尽量不使用,好像有问题.

1.3.1 绘制例子

  • draw,一般很少会去在上面做一些绘制的事情,除非有特定的需要,比如你绘制的东西需要在背景以及子控件之前.
  • onDraw,不必多说,比如LinearLayout的分割线等等,TV上的一些自绘的内容或者其它.
  • 真正的圆角控件,dispatchDraw
protected void dispatchDraw(Canvas canvas) {
    canvas.saveLayer(new RectF(0, 0, canvas.getWidth(),canvas.getHeight()), null, Canvas.ALL_SAVE_FLAG);
    // 绘制子控件
    super.dispatchDraw(canvas); //目的,需要显示的内容
    // 绘制带有圆角的 Path
    //roundPaint.setColor(Color.WHITE);
    roundPaint.setAntiAlias(true);
    // 绘制模式为填充
    roundPaint.setStyle(Paint.Style.FILL);
    // 混合模式为 DST_IN, 即仅显示当前绘制区域和背景区域交集的部分,并仅显示背景内容。
    roundPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
    Path mPath = new Path();
    float r = 300;
    PointF center = new PointF(canvas.getWidth() / 2, canvas.getHeight()  / 2);
    mPath.addCircle(center.x, center.y, r, Path.Direction.CW);
    // 圆角的矩形,使用此方法,addRoundRect
    mPath.moveTo(0, 0);  // 通过空操作让Path区域占满画布
    mPath.moveTo(canvas.getWidth(), canvas.getHeight());
    canvas.drawPath(mPath, roundPaint); // 源内容,用于遮罩层.
    canvas.restore();
}

在这里插入图片描述

子view集合,只在圆形里面显示.

在这里插入图片描述
与属性动画结合的绘制方法 参考 AndroidTvwidget 的 AnimView 的一个 demo.
https://gitee.com/kumei/android-tv-frame-new/blob/develop/AndroidTvWidget/app/src/main/java/com/open/test/view/AnimView.java

1.3.2 动画的原理简单了解

属性动画 关键的两个类ObjectAnimator和ValueAnimator
ObjectAnimator继承了ValueAnimator,ObjectAnimator#start(),ValueAnimator#start()
ValueAnimator.doAnimationFrame,animationFrame中将调用animateValue

void animateValue(float fraction) {
    fraction = mInterpolator.getInterpolation(fraction); // 插值器
    mCurrentFraction = fraction;
    int numValues = mValues.length;
    for (int i = 0; i < numValues; ++i) {
        mValues[i].calculateValue(fraction);
    }
    if (mUpdateListeners != null) {
        int numListeners = mUpdateListeners.size();
        for (int i = 0; i < numListeners; ++i) {
            mUpdateListeners.get(i).onAnimationUpdate(this); // 最后调用
        }
    }
}

最后调用 get与set方法(比如 setXXX getXXX)将会被调用属性(通过反射进行调用)的函数
比如 setTranslationX 或者 setScaleY 的调用,是一个什么过程,了解下

最后还是调用 invalidate

// ViewRootImpl.java,最后调用 scheduleTraversals

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //暂停了handler的后续消息处理,防止界面刷新的时候出现同步问题
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //将runnable发送给handler执行
        //performTraversals 在一个runnable中被调用的,通过将这个runnable加入队列来执行
        //performTraversals就是开头讲的 performMeasure->measure,performLayout->layout,performDraw->draw 的开始
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

2. 自定义控件了解

2.1 自定义属性

    public NewFrameLayout(Context context) {
        this(context, null);
    }
    
    public NewFrameLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        // 这里可以配合主题定义自己的属性
        // 比如 AppTheme.Light,里面的 <item name="cardViewStyle">@style/CardViewStyle.Light</item>
        // this(context, attrs, attr.NewFrameLayoutStyle);
    }
    
    public NewFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.NewFrameLayout, defStyleAttr, defStyleRes);
        ... ...
        a.recycle(); // 这个不要忘记写
    }

2.2 组合控件

参考 tvWidget 里面的多状态UI控件,了解下.

public class TVUICommonItemView extends RelativeLayout {
}

xml 布局使用 多个控件组合一起使用
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <!-- 左侧图标 -->
    <ImageView />
</merge>    

2.3 属性修改

简单的看一下.
又比如 LinearLayout 这些 Android原生的控件,还有网上的一些自定义控件,比如 FlowLayout.

比如 RecyclerView,addChild… 修改了 onMeasure,onLayout,onDraw,draw 来满足自己的一些特定需求.
LayoutManager : onLayoutChildren,measureChildWithMargins 等等.
如果想做一些相关自定义的 LayoutManager,只需要关注 LayoutManager的几个重要函数.:onLayoutChildren

https://blog.csdn.net/u010072711/article/details/78867096 图文详解LinearLayoutManager填充、测量、布局过程
https://blog.csdn.net/qibin0506/article/details/52676670?locationNum=2 RecyclerView自定义LayoutManager,打造不规则布局
https://www.jianshu.com/p/61a4811bb5ca 最清晰的 RecyclerView 使用及源码解析
https://juejin.im/entry/59c0ccfd6fb9a00a3c4b16d9 一个有特点的正六边形RecyclerView—HexagonRecyclerView详解篇
https://blog.csdn.net/u011387817/article/details/81875021 Android自定义LayoutManager第十一式之飞龙在天
https://github.com/JadynAi/InfinateCard 卡牌堆叠滑动效果,增加回滚动画
https://github.com/leochuan/ViewPagerLayoutManager ViewPager-LayoutManager
https://www.jianshu.com/p/715b59c46b74 你可能误会了!原来自定义LayoutManager可以这么简单
https://blog.csdn.net/lylodyf/article/details/52846602 自定义LayoutManager的详解及其使用

2.4 注意事项

onDraw或者相关绘制的函数中 避免重复创建变量,内存会抖动.
在 onDetachedFromWindow 做一些销毁的事情,避免出现不必要的麻烦… …
最好按照谷歌的制定的标准规范来写.
组合控件的布局建议使用 merge,减少层级.

3. 参考资料

谷歌源码 Android-24,Android内核剖析,Android 开发艺术探索 等等.
自定义控件测量模式真的和 match_parent,wrap_content 一一对应吗?

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值