View背后不为人知的勾当(一)--自定义控件和测量过程

本节首先讲讲自定义控件套路,自定义属性,测量过程的问题,为了后面的几节能专注于实现特效,而不受本文这些惯用套路的影响

  • 特效系列的目录

    • 关于Draw
    • 关于动画
    • 关于滑动
    • 关于Layout
  • 要实现界面特效,首先得掌握:

    • View的简单原理
    • 自定义属性
    • measure过程
    • 这算是UI特效的周边技术,任何特效都得考虑这几个问题
  • 安卓特效的实现,需要借助以下四个技术点:

    • draw
    • 动画
    • 滑动
    • layout

1 View的原理

  • 原理
    • 一个Activity包含一个Window对象,也就是PhoneWindow
    • 在onResume()方法里,系统才会把DecoreView添加到PhoneWindow中
    • DecoreView将内容显示在PhoneWindow上,所有View的监听都通过WM接收,并通过Activity回调响应的onClickListener
    • DecoreView包含一个TitleView,一个ContentView,ContentView的id是android.R.id.content,所以如果你的自定义布局如SwipeBack需要将ToolBar包含在内,需要考虑把你的布局添加到DecoreView下,但国内一般不用ToolBar,而是用自己定义的TitleBar,看情况
    • ContentView下才是setContentView设置的内容
    • requestWindowFeature()必须在setContentView之前调用

2 自定义属性

声明和使用自定义属性:

定义attr:在values目录下,attrs.xml

<declare-styleable name="TopBar">
        <attr name="title" format="string" />
        <attr name="titleTextSize" format="dimension" />
        <attr name="titleTextColor" format="color" />
        <attr name="titleBg" format="reference|color" />
</declare-styleable>

使用attr:在任意布局文件里
<xx.xx.xx.TopBar
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:id="@+id/topbar"
    custom:title="title"
    custom:titleTextSize="15sp"
    custom:titleTextColor="#aaaaaa"
    custom:titleBg="@drawable/ic_launcher"
/>

处理自定义属性

取出xml中设置的属性

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
mTitle = ta.getString(R.styleable.TopBar_title);
mTitleTextSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
mTitleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
mTitleBg = ta.getDrawable(R.styleable.TopBar_titleBg);
ta.recycle();

3 measure过程

  • 需要知道的

    • View的测量过程是最复杂的,对于view来说,得考虑内容宽高的计算,对于ViewGroup来说,得考虑具体布局,padding,margin,weight等
    • 可能有时需要重复measure
    • MeasureSpec类:32位的int值,高2位是测量模式(最多表示4个模式),低30位是测量的大小,即宽或高的值
    • 3种测量模式
      • EXACTLY: 精确值
        • match_parent和dp,px都相当于指定具体数值
        • 特别注意一下match_parent,如果子View是match_parent, 父View是wrap_content,这里就有冲突了
          • 子会被降级为AT_MOST
      • AT_MOST
        • wrap_content表示控件尺寸随控件内容,但不能超过父控件给的最大尺寸
        • View默认是支持wrap_content的,很合理,一个空View是没有内容的
        • 一般各种具体View的wrap_content会根据以下因素决定:
          • 背景drawable的宽高,也就是getInstinctWidth和getInstinctHeight
          • padding
          • 控件内容,如TextView的内容是text,ImageView的内容是src
          • minWidth和minHeight值(如果计算出的宽高小于min,则取min)
      • UNSPECIFIED
        • 不指定测量模式,View想多大就多大
        • 这个模式也比较合理,但我也不知道能用在哪儿
        • 能用在滚动控件?如果控件可以横向滚动,则传给子View的measureSpec就是不控制你宽高?
        • 可能意思就是:还是wrap_content,但不限制你最大值,所以和AT_MOST有区别,这是我猜的
  • 如何确定测量模式:

    • 首先注意:padding属性影响测量过程,margin属性影响布局过程,也影响ViewGroup的测量过程

3.1 View的默认measure行为

View默认情况下,测量过程如下:

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

//取minWidth和背景drawable的getMinimumWidth的最大值
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

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;         //minWidth属性或者背景宽度:这个就可以认为是空白View的内容
        break;
    case MeasureSpec.AT_MOST:   //wrap_content,此时specSize是父控件给留的最大值
    case MeasureSpec.EXACTLY:   //match_parent或者具体值
        result = specSize;
        break;
    }
    return result;
}

可以看出,唯一没处理的就是wrap_content的情况

3.2 一个View的测量模板

一般具体带内容的View,按下面的套路测量
* 思路也很简单
* EXACTLY:就指定我多大,那我就多大,一般这时子控件就是match_parent,或者具体数值
* AT_MOST:specSize是我的最大值,我本身也带个内容宽高,二者比较,取小的就是了,我尽量给父控件省地方
* 除非父控件放不下我了,那我就得按父控件尺寸来
* 至于calculateContentWidth和calculateContentHeight,可能会被padding,背景,内容等因素影响

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
            mearureWidth(widthMeasureSpec),
            mearureHeight(heightMeasureSpec));
}

private int mearureWidth(int measureSpec){
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    if(specMode == MeasureSpec.EXACTLY){
        result = specSize;
    }else{
        result = calculateContentWidth();
        if(specMode == MeasureSpec.AT_MOST){
            result = Math.min(result, specSize);
        }
    }

    return result;
}

private int mearureHeight(int measureSpec){
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    if(specMode == MeasureSpec.EXACTLY){
        result = specSize;
    }else{
        result = calculateContentHeight();
        if(specMode == MeasureSpec.AT_MOST){
            result = Math.min(result, specSize);
        }
    }

    return result;
}

///你自己测量宽度:宽度wrap_content时,会走这
private int calculateContentWidth(){
    return 200;
}

///你自己测量高度:宽度wrap_content时,会走这
private int calculateContentHeight(){
    return 200;
}
  • 自己处理wrap_content时应注意的问题

    • 内容如果是图片,文字等可测量的,则内容本身才有个尺寸,其他的如你自己画的一个圈,可能就需要你给个固定值或者自定义属性什么的了
    • padding:内容宽度 + 左右padding就是测量宽度
    • max和min相关属性:也会影响你最终的测量值
  • padding影响还挺大

    • 你draw的时候,宽高是已知的,但是在这里padding也是要考虑的

3.3 ViewGroup的测量模板

模板参考:
http://www.jianshu.com/p/71e9cc942c97
http://blog.csdn.net/lmj623565791/article/details/38339817

其实ViewGroup的match_parent和固定值也好说,就是EXACTLY,主要还是wrap_content的问题

ViewGroup的onMeasure直接继承自View,所以没有实现对子控件的处理,
但是测量子控件的方法已经提供了,下面给出一个ViewGroup测量的模板

在给出模板之前,需要说明的是,关于这一节的模板和下一节的原理,不要太纠结,
一般情况下,你通过RelativeLayout或者FrameLayout的margin就可以实现任何形式的布局,
测量过程本身是很复杂的,如果不够复杂,可能你考虑的情况不够,参考LinearLayout的测量过程的代码,你就能知道为什么自己一般不要过多的干扰布局过程了

ViewGroup处理子控件的measure先不说
主要是ViewGroup在自己wrap_content时,宽高是随着子控件来的,并且和具体的布局方式还有关系,所以测量过程中计算ViewGroup自己本身宽高时,可能需要把布局算法先过一遍

模板1:不考虑子控件margin,也不考虑ViewGroup本身的wrap_content
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    final int count = getChildCount();
    if (count > 0) {
        measureChildren(widthMeasureSpec, heightMeasureSpec);
    }
}


模板2:考虑margin,和考虑wrap_content
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    final int count = getChildCount();
    // 临时ViewGroup大小值
    int viewGroupWidth = 0;
    int viewGroupHeight = 0;
    if (count > 0) {
        // 遍历childView
        for (int i = 0; i < count; i++) {
            // childView
            View child = getChildAt(i);
            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //测量childView包含外边距
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            // 计算父容器的期望值,下面代码我们注掉,因为这里的代码根据具体布局来
            //viewGroupWidth += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            //viewGroupHeight += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
        }


        //根据子控件计算ViewGroup宽高,取决于具体布局
        viewGroupWidth = calculateParentWidthBasedOnChilren();
        viewGroupHeight = calculateParentHeightBasedOnChilren();

        // ViewGroup内边距
        viewGroupWidth += getPaddingLeft() + getPaddingRight();
        viewGroupHeight += getPaddingTop() + getPaddingBottom();

        //和建议最小值进行比较
        viewGroupWidth = Math.max(viewGroupWidth, getSuggestedMinimumWidth());
        viewGroupHeight = Math.max(viewGroupHeight, getSuggestedMinimumHeight());
    }
    setMeasuredDimension(resolveSize(viewGroupWidth, widthMeasureSpec), resolveSize(viewGroupHeight, heightMeasureSpec));
}


///你自己来实现,根据具体布局和子控件们的信息,算出ViewGroup的wrap_content宽度
private int calculateParentWidthBasedOnChilren(){
    int viewGroupWidth = 0;
    final int count = getChildCount();
    for (int i = 0; i < count; i++){
        View child = getChildAt(i);
        LayoutParams lp = (LayoutParams) child.getLayoutParams();
        //lp.leftMargin
        //lp.rightMargin
        //child.getMeausredWidth()
        ...看你怎么算了
    }

    return viewGroupWidth;
}


///你自己来实现,根据具体布局和子控件们的信息,算出ViewGroup的wrap_content高度
private int calculateParentHeightBasedOnChilren(){
    int viewGroupHeight = 0;
    final int count = getChildCount();
    for (int i = 0; i < count; i++){
        View child = getChildAt(i);
        LayoutParams lp = (LayoutParams) child.getLayoutParams();
        //lp.topMargin
        //lp.bottomMargin
        //child.getMeausredHeight()
        ...看你怎么算了
    }

    return viewGroupHeight;
}



以下代码是ViewGroup本身提供的

///遍历测量所有子控件
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            //源码中是measureChild,注意还有个measureChildWithMargins
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

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


public static int resolveSize(int size, int measureSpec) {
    return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}


3.4 测量过程相关源码和流程简陋分析

参考开发艺术

再看ViewGroup的测量过程

  • 思路:

    • match_parent和具体值好说
    • 如果ViewGroup本身是wrap_content,那就需要根据所有子View来确定自己的宽高
    • 如果一个竖直方向的LinearLayout高度是wrap_content,让你测量,你怎么测量?
      • 对于每一个View,取其高度和margin
        • 如果子View是wrap_content或具体数值,还好说
        • 如果子View是match_parent,而咱这个LinearLayout又是个wrap_content
          • 那就把子View当wrap_content来处理
  • 注意:

    • ViewGroup提供了measureChildren, measureChildWithMargins, getChildMeasureSpec方法
    • 但没有提供具体的onMeasure实现,因为也没法提供
    • 再注意LinearLayout的vertical模式下,测量过程需要遍历子控件,see how tall everyone is, also remember max width
对于顶级View,即DecoreView,measure过程和普通View有点不同

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
        final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
    int childWidthMeasureSpec;
    int childHeightMeasureSpec;
    boolean windowSizeMayChange = false;

    if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(TAG,
            "Measuring " + host + " in display " + desiredWindowWidth
            + "x" + desiredWindowHeight + "...");

    boolean goodMeasure = false;
    if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
        // On large screens, we don't want to allow dialogs to just
        // stretch to fill the entire width of the screen to display
        // one line of text.  First try doing the layout at a smaller
        // size to see if it will fit.
        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(TAG, "Window " + mView + ": baseSize=" + baseSize);
        if (baseSize != 0 && desiredWindowWidth > baseSize) {
            childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
                    + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
            if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                goodMeasure = true;
            } else {
                // Didn't fit in that size... try expanding a bit.
                baseSize = (baseSize+desiredWindowWidth)/2;
                if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": next baseSize="
                        + baseSize);
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
                        + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                    if (DEBUG_DIALOG) Log.v(TAG, "Good!");
                    goodMeasure = true;
                }
            }
        }
    }

    if (!goodMeasure) {
        childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
            windowSizeMayChange = true;
        }
    }

    if (DBG) {
        System.out.println("======================================");
        System.out.println("performTraversals -- after measure");
        host.debug();
    }

    return windowSizeMayChange;
}

看这段
if (!goodMeasure) {
    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
        windowSizeMayChange = true;
    }
}

desire就是屏幕宽高

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

上面产生的就是DecoreView的measureSpec

注意一个问题,这个也曾经出现在阿里面试题里:
父是AT_MOST时,高度其实是根据子View来,
但如果此时子是match_parent,所以子也只能是AT_MOST了
Child wants to be our size, but our size is not fixed. Constrain child to not be bigger than us.

3.5 直接从RelativeLayout或者FrameLayout写自己的布局

实战:BlockLayout

现在需要一个BlockLayout,其子控件可以是任何控件,但不论其宽度指定成什么,
每一行都必须只能放3个子控件,而高度不论指定成什么,最后显示出来的都是个正方形
并且,每一行的中间那个控件,必须距左右各10dp的margin
行与行之间,也是10dp的margin

这估计是个最简单的Layout了

CubeSdk里的BlockLayout是继承RelativeLayout,实现比较简单比较巧妙,
利用了Relativelayout子控件的margin来控制,规避了自己measuue和layout,
我们应该学习

package in.srain.cube.views.block;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;

public class BlockListView extends RelativeLayout {

    public interface OnItemClickListener {
        void onItemClick(View v, int position);
    }

    private static final int INDEX_TAG = 0x04 << 24;

    private BlockListAdapter<?> mBlockListAdapter;

    private LayoutInflater mLayoutInflater;

    private OnItemClickListener mOnItemClickListener;

    public BlockListView(Context context) {
        this(context, null, 0);
    }

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

    public BlockListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mLayoutInflater = LayoutInflater.from(context);
    }

    public void setAdapter(BlockListAdapter<?> adapter) {
        if (adapter == null) {
            throw new IllegalArgumentException("adapter should not be null");
        }
        mBlockListAdapter = adapter;
        adapter.registerView(this);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (null != mBlockListAdapter) {
            mBlockListAdapter.registerView(null);
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (null != mBlockListAdapter) {
            mBlockListAdapter.registerView(this);
        }
    }

    public void setOnItemClickListener(OnItemClickListener listener) {
        mOnItemClickListener = listener;
    }

    OnClickListener mOnClickListener = new OnClickListener() {

        @Override
        public void onClick(View v) {
            int index = (Integer) v.getTag(INDEX_TAG);
            if (null != mOnItemClickListener) {
                mOnItemClickListener.onItemClick(v, index);
            }
        }
    };

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
    }

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

    public void onDataListChange() {

        removeAllViews();

        int len = mBlockListAdapter.getCount();
        int w = mBlockListAdapter.getBlockWidth();
        int h = mBlockListAdapter.getBlockHeight();
        int columnNum = mBlockListAdapter.getCloumnNum();

        int horizontalSpacing = mBlockListAdapter.getHorizontalSpacing();
        int verticalSpacing = mBlockListAdapter.getVerticalSpacing();

        boolean blockDescendant = getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS;

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

            RelativeLayout.LayoutParams lyp = new RelativeLayout.LayoutParams(w, h);
            int row = i / columnNum;
            int clo = i % columnNum;
            int left = 0;
            int top = 0;

            if (clo > 0) {
                left = (horizontalSpacing + w) * clo;
            }
            if (row > 0) {
                top = (verticalSpacing + h) * row;
            }
            lyp.setMargins(left, top, 0, 0);
            View view = mBlockListAdapter.getView(mLayoutInflater, i);
            if (!blockDescendant) {
                view.setOnClickListener(mOnClickListener);
            }
            view.setTag(INDEX_TAG, i);
            addView(view, lyp);
        }
        requestLayout();
    }
}

4 自定义控件的套路

  • 套路:

    • 继承View,通过onDraw实现效果,需要考虑支持wrap_content和padding
      • 默认是不支持的wrap_content的(设为wrap,实际还是使用match_parent效果)
    • 继承ViewGroup,需要合理处理measure和layout过程,得考虑padding和margin,否则这俩属性就失效了,同时处理子元素的测量和布局过程
      • 这种方法要想得到一个规范的Layout是很复杂的,看LinearLayout源码就知道
    • 对现有控件进行扩展,比较简单,不需要考虑wrap_cotent和padding
    • 创建复合控件,比较简单,也是最常见的
  • 几个重要的回调和注意点

    • onFinishInflate() 从xml加载组件完成
    • onSizeChanged() 组件大小改变
    • onAttachedToWindow
    • onDetachedFromWindow 动画,handler,线程之类的,应该在这里停止
    • View不可见时,也需要停止线程和动画,否则可能造成内存泄漏
    • 滑动冲突需要考虑
    • onMeasure
    • onLayout
    • onTouchEvent()
  • 其他:

    • 知道怎么自定义attr
    • 自定义Drawable也是个路子

5 自定义控件入门

在这里先给出入门的例子,为后面几节做准备

5.1 CircleView:版本1

效果就是画个圆,不考虑wrap_content和padding

public class CircleView  extends View{
    public CircleView(Context context) {
        super(context);
        init();
    }

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

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

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    private void init(){
        mPaint.setColor(mColor);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(width/2, height/2, radius, mPaint);
    }
}
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    >

    <org.ayo.ui.sample.view_learn.CircleView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#000000" />

</FrameLayout>
  • 效果:
    • 默认支持了margin,因为margin由父控件处理了
    • 但不支持padding,因为没有在onDraw里考虑padding
    • 也不支持wrap_content,而是当做match_parent处理
      • 要解决这个问题,这个例子里,需要指定个默认宽高,例如都是200px

5.2 CircleView:版本2

添加padding支持

@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);
}
<org.ayo.ui.sample.view_learn.CircleView
    android:layout_width="wrap_content"
    android:layout_height="100dp"
    android:layout_margin="20dp"
    android:padding="20dp"
    android:background="#000000" />

5.3 CircleView:版本3

添加wrap_content支持

//===========================================
//为了让控件支持wrap_content时,内容尺寸取200px,需要我们重写measure过程
//===========================================
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
            mearureWidth(widthMeasureSpec),
            mearureHeight(heightMeasureSpec));
}

private int mearureWidth(int measureSpec){
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    if(specMode == MeasureSpec.EXACTLY){
        result = specSize;
    }else{
        result = calculateContentWidth();
        if(specMode == MeasureSpec.AT_MOST){
            result = Math.min(result, specSize);
        }
    }

    return result;
}

private int mearureHeight(int measureSpec){
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    if(specMode == MeasureSpec.EXACTLY){
        result = specSize;
    }else{
        result = calculateContentHeight();
        if(specMode == MeasureSpec.AT_MOST){
            result = Math.min(result, specSize);
        }
    }

    return result;
}

private int calculateContentWidth(){
    return 200;
}

private int calculateContentHeight(){
    return 200;
}
<org.ayo.ui.sample.view_learn.CircleView2
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="20dp"
    android:padding="20dp"
    android:background="#000000" />

其实上面这段java代码,可以简化一下,参考开发艺术


///不考虑contentSize和specSize的大小关系,不考虑minWidth和minHeight
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(200, 200);
    }else if(widthSpecMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(200, heightSize);
    }else if(heightSpecMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(widthSize, 200);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值