Android自定义View,你必须知道的几点

来源:互联网 发布:cin2男士内裤 知乎 编辑:程序博客网 时间:2024/05/15 23:39

参考:
CSDN博客:http://blog.csdn.net/jxxfzgy/article/details/43613261 Android 微信6.1 tab栏图标和字体颜色渐变的实现

  1. 自定义View ,需要掌握的几个点是什么呢?
    我们先把自定义View细分一下,分为两种:
    1) 自定义ViewGroup
    2) 自定义View
    其实ViewGroup最终还是继承之View,当然它内部做了许多操作;继承之ViewGroup的
    View我们一般称之为容器。
  2. 自定义View 需要掌握的几点,主要就是两点
    1)重写 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}方法。
    2)重写 protected void onDraw(Canvas canvas) {}方法
  3. 我们可以把View理解为一张白纸,而自定义View就是在这张白纸上画上我们自己绘制 的图案,可以在绘制任何图案,也可以在白纸的任何位置绘制,那么问题来了,白纸
    哪里来?图案哪里来?位置如何计算?
    a)白纸好说,只要我们继承之View,在onDraw(Canvas canvas)中的canvas就是我们所说的白纸。
public class CustomView extends View {    public CustomView(Context context) {        super(context);    }    public CustomView(Context context, AttributeSet attrs) {        super(context, attrs);    }    public CustomView(Context context, AttributeSet attrs, int  defStyleAttr) {        super(context, attrs, defStyleAttr);    }    @Override    protected void onDraw(Canvas canvas) {        // canvas 即为白纸        super.onDraw(canvas);    }}
b)图案呢?这里的图案就是有图片和文字组成,这个也好说,定义一个Bitmap 成员变量,和一个String的成员变量。
private Bitmap mBitmap ;private String mName ;mName = "这里直接赋值";mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher) ;

图片可以通过资源文件可以拿到。
c)计算位置
所以最核心的也是我们认为最麻烦的地方就是计算绘制的位置,计算位置就得先测量
自身的大小,也就是我们必须掌握的两点中的第一点:需要重写 protected void
onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}方法,先来看一下google写的TextView的onMeasure()方法是如何实现的。

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    int widthMode = MeasureSpec.getMode(widthMeasureSpec);    int heightMode = MeasureSpec.getMode(heightMeasureSpec);    int widthSize = MeasureSpec.getSize(widthMeasureSpec);    int heightSize = MeasureSpec.getSize(heightMeasureSpec);    int width;    int height;    BoringLayout.Metrics boring = UNKNOWN_BORING;    BoringLayout.Metrics hintBoring = UNKNOWN_BORING;    if (mTextDir == null) {        mTextDir = getTextDirectionHeuristic();    }    int des = -1;    boolean fromexisting = false;    if (widthMode == MeasureSpec.EXACTLY) {        // Parent has told us how big to be. So be it.        width = widthSize;    } else {        if (mLayout != null && mEllipsize == null) {            des = desired(mLayout);        }        if (des < 0) {            boring = BoringLayout.isBoring(mTransformed, mTextPaint,  mTextDir, mBoring);            if (boring != null) {                mBoring = boring;            }        } else {            fromexisting = true;        }        if (boring == null || boring == UNKNOWN_BORING) {            if (des < 0) {                des = (int) FloatMath.ceil(Layout.getDesiredWidth (mTransformed, mTextPaint));            }            width = des;        } else {            width = boring.width;        }        final Drawables dr = mDrawables;        if (dr != null) {            width = Math.max(width, dr.mDrawableWidthTop);            width = Math.max(width, dr.mDrawableWidthBottom);        }        if (mHint != null) {            int hintDes = -1;            int hintWidth;            if (mHintLayout != null && mEllipsize == null) {                hintDes = desired(mHintLayout);            }            if (hintDes < 0) {                hintBoring = BoringLayout.isBoring(mHint, mTextPaint,  mTextDir, mHintBoring);                if (hintBoring != null) {                    mHintBoring = hintBoring;                }            }            if (hintBoring == null || hintBoring == UNKNOWN_BORING) {                if (hintDes < 0) {                    hintDes = (int) FloatMath.ceil(Layout.getDesiredWidth (mHint, mTextPaint));                }                hintWidth = hintDes;            } else {                hintWidth = hintBoring.width;            }            if (hintWidth > width) {                width = hintWidth;            }        }        width += getCompoundPaddingLeft() + getCompoundPaddingRight();        if (mMaxWidthMode == EMS) {            width = Math.min(width, mMaxWidth * getLineHeight());        } else {            width = Math.min(width, mMaxWidth);        }        if (mMinWidthMode == EMS) {            width = Math.max(width, mMinWidth * getLineHeight());        } else {            width = Math.max(width, mMinWidth);        }        // Check against our minimum width        width = Math.max(width, getSuggestedMinimumWidth());        if (widthMode == MeasureSpec.AT_MOST) {            width = Math.min(widthSize, width);        }    }    int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight ();    int unpaddedWidth = want;    if (mHorizontallyScrolling) want = VERY_WIDE;    int hintWant = want;    int hintWidth = (mHintLayout == null) ? hintWant :  mHintLayout.getWidth();    if (mLayout == null) {        makeNewLayout(want, hintWant, boring, hintBoring,                      width - getCompoundPaddingLeft() -  getCompoundPaddingRight(), false);    } else {        final boolean layoutChanged = (mLayout.getWidth() != want) ||                (hintWidth != hintWant) ||                (mLayout.getEllipsizedWidth() !=                        width - getCompoundPaddingLeft() -  getCompoundPaddingRight());        final boolean widthChanged = (mHint == null) &&                (mEllipsize == null) &&                (want > mLayout.getWidth()) &&                (mLayout instanceof BoringLayout || (fromexisting && des >=  0 && des <= want));        final boolean maximumChanged = (mMaxMode != mOldMaxMode) ||  (mMaximum != mOldMaximum);        if (layoutChanged || maximumChanged) {            if (!maximumChanged && widthChanged) {                mLayout.increaseWidthTo(want);            } else {                makeNewLayout(want, hintWant, boring, hintBoring,                        width - getCompoundPaddingLeft() -  getCompoundPaddingRight(), false);            }        } else {            // Nothing has changed        }    }    if (heightMode == MeasureSpec.EXACTLY) {        // Parent has told us how big to be. So be it.        height = heightSize;        mDesiredHeightAtMeasure = -1;    } else {        int desired = getDesiredHeight();        height = desired;        mDesiredHeightAtMeasure = desired;        if (heightMode == MeasureSpec.AT_MOST) {            height = Math.min(desired, heightSize);        }    }    int unpaddedHeight = height - getCompoundPaddingTop() -  getCompoundPaddingBottom();    if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {        unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop (mMaximum));    }    /*     * We didn't let makeNewLayout() register to bring the cursor into  view,     * so do it here if there is any possibility that it is needed.     */    if (mMovement != null ||        mLayout.getWidth() > unpaddedWidth ||        mLayout.getHeight() > unpaddedHeight) {        registerForPreDraw();    } else {        scrollTo(0, 0);    }    setMeasuredDimension(width, height);}

哇!好长!而且方法中还嵌套方法,如果真要算下来,代码量不会低于500行,看到这 么多代码,头都大了,我想这也是我们为什么在学习Android自定义View的时候觉得如 此困难的原因。大多数情况下,因为我们是自定义的View,可以说是根据我们的需求 定制的View,所以很多里面的功能我们完全没必要,只需要几十行代码就能搞定。看 到几十行代码就能搞定,感觉顿时信心倍增。在重写这个方法之前,得先了解一个类 MeasureSpec ,如果不了解,没关系,下面就一起来了解一下这个类。

public static class MeasureSpec {    private static final int MODE_SHIFT = 30;    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;    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;    public static int makeMeasureSpec(int size, int mode) {        if (sUseBrokenMakeMeasureSpec) {            return size + mode;        } else {            return (size & ~MODE_MASK) | (mode & MODE_MASK);        }    }    public static int getMode(int measureSpec) {        return (measureSpec & MODE_MASK);    }    public static int getSize(int measureSpec) {        return (measureSpec & ~MODE_MASK);    }}

这里我把里面一些我认为没必要的代码都去掉了,只留了以上几行代码,这样看起来 很清晰,也非常容易理解。我们先做个转化,把上面几个成员变量转化成二进制,这 个就不需要转化了,这里代表的只是一个移动的位置,也就是一个单纯的数字。
private static final int MODE_SHIFT = 30;
0x3 就是 11 左移30位 ,就是补30个0;
private static final int MODE_MASK = 1100 0000 0000 0000 0000 0000 0000 0000 ;
00 左移30位
public static final int UNSPECIFIED = 0000 0000 0000 0000 0000 0000 0000 0000 ;
01 左移30位
public static final int EXACTLY = 0100 0000 0000 0000 0000 0000 0000 0000 ;
10 左移30位
public static final int AT_MOST = 1000 0000 0000 0000 0000 0000 0000 0000 ;
每个方法中都有一个 & 的操作,所以我们接下来看看这集几个方法的含义是什么,先从下往上看,先易后难。
1 ) public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
顾名思义,通过measureSpec这个参数,获取size ,两个都是int类型,怎么通过一个int类型的数获取另一个int类型的数。我们在学习java的时候知道,一个int类型是32位,任何int类型的数都是有32位,比如一个int类型的数值3,它也是占有32位,只是高30位全部为0。google 也是利用这一点,让这个int类型的measureSpec数存了两个信息,一个就是size,保存在int类型的低30位,另一个就是mode,保存在int类型的高2位。前面我们看到了有几个成员变量,UNSPECIFIED,EXACTLY,AT_MOST者就是mode的三种选择,目前也只有这三种选择,所以只需要2位就能实现。
2) public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
这也好理解,获取模式,但这些模式有啥用处呢?
2-1.EXACTLY 模式: 准确的、精确的;这种模式,是最容易理解和处理的,可以理解为大小固定,比如在定义layout_width的时候,定义为固定大小 10dp,20dp,或者match_parent(此时父控件是固定的)这时候,获取出来的mode就是EXACTLY。
2-2.AT_MOST 模式: 最大的;这种模式稍微难处理些,不过也好理解,就是View的大小最大不能超过父控件,超过了,取父控件的大小,没有,则取自身大小,这种情况一般都是在layout_width设为warp_content时。
2-3.UNSPECIFIED 模式:不指定大小,这种情况,我们几乎用不上,它是什么意思呢,就是View的大小想要多大,就给多大,不受父View的限制,几个例子就好理解了,ScrollView控件就是。
3)public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
这个方法也好理解,封装measureSpec的值,在定义一个View的大小时,我们只是固定了大小,你下次想要获取mode的时候,肯定无法拿到,所以就得自己把模式添加进去,这个方法,在自定义View中,也基本不需要用到,他所使用的场所,是在设置子View的大小的时候需要用到,所以如果是自定义ViewGroup的话,就需要用到。
接下来就来重写onMeasure()方法:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {     //这里方法套路都是一样,不管三七 二十一,上来就先把mode 和 size 获取出 来。      int widthMode = MeasureSpec.getMode(widthMeasureSpec);      int heightMode = MeasureSpec.getMode(heightMeasureSpec);      int widthSize = MeasureSpec.getSize(widthMeasureSpec);      int heightSize = MeasureSpec.getSize(heightMeasureSpec);      //View 真正需要显示的大小      int width = 0, height = 0;      //这里是去测量字体大小      measureText();      //字体宽度加图片宽度取最大宽度,这里因为字体和图片是上下排列      int contentWidth = Math.max(mBoundText.width(), mIconNormal.getWidth ());     // 我们渴望得到的宽度      int desiredWidth = getPaddingLeft() + getPaddingRight() +  contentWidth;      //重点来了,判断模式,这个模式哪里来的呢,就是在编写xml的时候,设置的 layout_width      switch (widthMode) {      //如果是AT_MOST,不能超过父View的宽度          case MeasureSpec.AT_MOST:              width = Math.min(widthSize, desiredWidth);              break;              //如果是精确的,好说,是多少,就给多少;          case MeasureSpec.EXACTLY:              width = widthSize;              break;              //这种情况,纯属在这里打酱油的,可以不考虑          case MeasureSpec.UNSPECIFIED://我是路过的              width = desiredWidth;              break;      }      int contentHeight = mBoundText.height() + mIconNormal.getHeight();      int desiredHeight = getPaddingTop() + getPaddingBottom() +  contentHeight;      switch (heightMode) {          case MeasureSpec.AT_MOST:              height = Math.min(heightSize, desiredHeight);              break;          case MeasureSpec.EXACTLY:              height = heightSize;              break;          case MeasureSpec.UNSPECIFIED:              height = contentHeight;              break;      }      //最后不要忘记了,调用父类的测量方法      setMeasuredDimension(width, height);}

到这里,就算View的大小就已经完成了,自定义View的计算过程和以上方法基本类似。接着就是计算需要显示的图标和字体的位置。这里希望图片和字体垂直排列,并居中显示在View当中,因为当前的View的宽高已经测量好了,接下来的计算也就非常简单了,这里就放在onDraw()方法中计算。
d)绘制图标和字体
绘制图标,可以用canvas.drawBitmap(Bitmap bitmap, int left, int top ,Paint paint)方法,bitmap 已经有了,如果不需要对图片作特殊处理 paint 可以传入null表示原图原样的绘制在白纸上,所以就差绘制的位置 left ,top前面已经分析过了,需要把图绘制在View的中间,当然这里还需包含字体,所以可以这样计算left 和top。

int left = (mViewWidth - mIconNormal.getWidth())/2 ; int top = (mViewHeight - mIconNormal.getHeight() - mBoundText.height()) /2 ;

mViewWidth —>View的宽度,mIconNormal —>图片的宽度, mBoundText.height() —>字体的高度;绘制字体,绘制字体,就比绘制图片稍微麻烦点,因为绘制字体需要用到画笔Paint ,这里定义一个画笔Paint,直接new 一个出来:

mTextPaintNormal = new Paint();//设置字体大小mTextPaintNormal.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, mTextSize, getResources().getDisplayMetrics()));//设置画笔颜色,也就是字体颜色mTextPaintNormal.setColor(mTextColorNormal);//设置抗锯齿mTextPaintNormal.setAntiAlias(true);

这里也是调用Canvas的方法 canvas.drawText(mTextValue,x,y, mTextPaintNormal);mTextValue需要绘制的字体内容, mTextPaintNormal画笔,x,y需要绘制的位置:

float x = (mViewWidth - mBoundText.width())/2.0f ;float y = (mViewHeight + mIconNormal.getHeight() + mBoundText.height()) /2.0F ;//整体来说代码还是相当少的。下面把onDraw的代码也贴出来@Overrideprotected void onDraw(Canvas canvas) {    drawBitmap(canvas) ;    drawText(canvas) ;}private void drawBitmap(Canvas canvas) {    int left = (mViewWidth - mIconNormal.getWidth())/2 ;    int top = (mViewHeight - mIconNormal.getHeight() -  mBoundText.height()) /2 ;    canvas.drawBitmap(mIconNormal, left, top ,null);}private void drawText(Canvas canvas) {     float x = (mViewWidth - mBoundText.width())/2.0f ;     float y = (mViewHeight + mIconNormal.getHeight() +  mBoundText.height()) /2.0F ;     canvas.drawText(mTextValue,x,y, mTextPaintNormal);}

总结:
onMeasure() 方法只要了解了 MeasureSpec 类就不是什么问题,而MeasureSpec 也很简单,onDraw() 方法就需要了解Canvas 类的绘制方法,并且通过简单的Api查询,就基本能实现我们所需的要求。对于自定义View,如果你会重写 测量 和 onDraw 方法,那么就具备了此技能,而如果需要了解更深,自定义有个性,更绚丽的View,就还得深入了解Canvas 、Paint等方法。

原创粉丝点击