Android 自定义View 总结

  自定义View入门还是很简单的,但是很少有程序员能做好它,因为涉及的面太广,网上经常有写文章标题是一篇就能搞定自定义View的,简直是too young too simple……可以说自定义View是从入门到懵逼,哈哈,其实没那么恐怖,慢慢积累就好了。

  自定义View可以分为三类:继续View Group来组合多个子View;继承特定的View(如TextView,ImageView, ViewPager等);直接继承自View

  组合和继承用的比较多,组合一般就是自定义一个View,继承自ViewGroup(如RelativeLayout,LinearLayout,FrameLayout), 这里就不多做介绍了。继承特定的View(如TextView,ViewPager等),添加特殊的一些事件或者逻辑处理。

  本文重点总结一下直接继承自View的方式。

1. 自定义属性

  这个就比较简单,在res/values/attr.xml中自定义属性,然后在自定义View的构造方法中通过TypedArray引入既可。

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <attr name="titleText" format="string" />
    <attr name="titleTextColor" format="color" />
    <attr name="titleTextSize" format="dimension" />

    <declare-styleable name="CustomTitleView">
        <attr name="titleText" />
        <attr name="titleTextColor" />
        <attr name="titleTextSize" />
    </declare-styleable>

</resources>

  format属性类型汇总:string,color,demension,integer,enum,reference,float,boolean,fraction,flag

/**
     * 文本
     */
    private String mTitleText;
    /**
     * 文本的颜色
     */
    private int mTitleTextColor;
    /**
     * 文本的大小
     */
    private int mTitleTextSize;

    /**
     * 绘制时控制文本绘制的范围
     */
    private Rect mBound;
    private Paint mPaint;

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

    public CustomTitleView(Context context)
    {
        this(context, null);
    }

    /**
     * 获得我自定义的样式属性
     * 
     * @param context
     * @param attrs
     * @param defStyle
     */
    public CustomTitleView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
        /**
         * 获得我们所定义的自定义样式属性
         */
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyle, 0);
        int n = a.getIndexCount();
        for (int i = 0; i < n; i++)
        {
            int attr = a.getIndex(i);
            switch (attr)
            {
            case R.styleable.CustomTitleView_titleText:
                mTitleText = a.getString(attr);
                break;
            case R.styleable.CustomTitleView_titleTextColor:
                // 默认颜色设置为黑色
                mTitleTextColor = a.getColor(attr, Color.BLACK);
                break;
            case R.styleable.CustomTitleView_titleTextSize:
                // 默认设置为16sp,TypeValue也可以把sp转化为px
                mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                break;

            }

        }
        a.recycle();

        /**
         * 获得绘制文本的宽和高
         */
        mPaint = new Paint();
        mPaint.setTextSize(mTitleTextSize);
        // mPaint.setColor(mTitleTextColor);
        mBound = new Rect();
        mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);

    }

2. 重写onMesure

  该方法也可以不重写,大多情况下,只要不是wrap_content,父容器都能正确的计算其尺寸,如果自定的View在布局文件中使用时,宽高指定为warp_content时,就需要我们重写onMesure,自定义ViewGroup的onMeasure,当其宽和高为warp_content时,要根据所有子View已经margin来计算其宽高值。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        /**
         * 获得此ViewGroup上级容器为其推荐的宽和高,以及计算模式
         */
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height ;
        if (widthMode == MeasureSpec.EXACTLY)
        {
            width = widthSize;
        } else
        {
            mPaint.setTextSize(mTitleTextSize);
            mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);
            float textWidth = mBounds.width();
            int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
            width = desired;
        }

        if (heightMode == MeasureSpec.EXACTLY)
        {
            height = heightSize;
        } else
        {
            mPaint.setTextSize(mTitleTextSize);
            mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);
            float textHeight = mBounds.height();
            int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
            height = desired;
        }
   }

  ViewGroup会为childView指定测量模式,下面简单介绍下三种测量模式:

测量模式 含义
EXACTLY 表示设置了精确的值,一般当childView设置其宽、高为精确值、match_parent时,ViewGroup会将其设置为EXACTLY
AT_MOST 表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST
UNSPECIFIED 表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见

  注:上面的每一行都有一个一般,意思上述不是绝对的,对于childView的mode的设置还会和ViewGroup的测量mode有一定的关系;当然了,这是第一篇自定义ViewGroup,而且绝大部分情况都是上面的规则,所以为了通俗易懂,暂不深入讨论其他内容。

3. 重写onDraw

  onDraw就是自定义View的核心部分了,通常,我们在自定义的构造函数里,会初始化好Paint(画笔对象),Canvas(画布对象),最后都是通过canvas.drawXXX( XXX, paint) 来进行绘制。

mPaint.setStrokeWidth(4);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.CYAN);
mPaint.setTextSize(mTitleTextSize);

   canvas可以调用各个方法来使用画笔对象进行绘制,除了一些长方形、圆等基本图形,canvas.drawPath可以绘制更为复杂的图形,包括内塞尔曲线等,具体可以参考下文:

   安卓自定义View进阶-Path基本操作

canvas.drawText()
canvas.drawBitmap()
canvas.drawPath(path, mPaint); 

  在onDraw中,要注意以下几点

  • 避免耗时操作,因为onDraw通常会被多次触发,
  • 避免内存泄露,主要针对View中含有线程或动画的情况:当View退出或不可见时,记得及时停止该View包含的线程和动画,否则会造成内存泄露问题。

    1. 启动线程/ 动画:使用view.onAttachedToWindow(),因为该方法调用的时机是当包含View的Activity启动的时刻
    2. 停止线程/ 动画:使用view.onDetachedFromWindow(),因为该方法调用的时机是当包含View的Activity退出或当前View被remove的时刻
  • 如果是自定义ViewGroup,要处理好支持支持padding & margin

  • 避免过度绘制,Canvas对象给我们提供了很便利的方法clipRect就可以很好的去解决这类问题。

  避免过度绘制可以参考鸿洋大神的这篇博客:
  Android UI性能优化实战 识别绘制中的性能问题

  主要思想就是,如果一个子View不需要全部显示出来,那么就可以利用canvas.clipRect()方法

@Override
    protected void onDraw(Canvas canvas)
    {
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(20, 120);
        for (int i = 0; i < mCards.length; i++)
        {
            canvas.translate(120, 0);
            canvas.save();
            if (i < mCards.length - 1)
            {
                canvas.clipRect(0, 0, 120, mCards[i].getHeight());
            }
            canvas.drawBitmap(mCards[i], 0, 0, null);
            canvas.restore();
        }
        canvas.restore();
    }

4. 重写onLayout

  onLayout的作用:对其所有childView进行定位(设置childView的绘制区域),所以只有自定义ViewGroup时,才需要重写onLayout

// abstract method in viewgroup
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b)
    {
        int cCount = getChildCount();
        int cWidth = 0;
        int cHeight = 0;
        MarginLayoutParams cParams = null;
        /**
         * 遍历所有childView根据其宽和高,以及margin进行布局
         */
        for (int i = 0; i < cCount; i++)
        {
            View childView = getChildAt(i);
            cWidth = childView.getMeasuredWidth();
            cHeight = childView.getMeasuredHeight();
            cParams = (MarginLayoutParams) childView.getLayoutParams();

            int cl = 0, ct = 0, cr = 0, cb = 0;

            switch (i)
            {
            case 0:
                cl = cParams.leftMargin;
                ct = cParams.topMargin;
                break;
            case 1:
                cl = getWidth() - cWidth - cParams.leftMargin
                        - cParams.rightMargin;
                ct = cParams.topMargin;
                break;
            case 2:
                cl = cParams.leftMargin;
                ct = getHeight() - cHeight - cParams.bottomMargin;
                break;
            case 3:
                cl = getWidth() - cWidth - cParams.leftMargin
                        - cParams.rightMargin;
                ct = getHeight() - cHeight - cParams.bottomMargin;
                break;
            }
            cr = cl + cWidth;
            cb = cHeight + ct;
            childView.layout(cl, ct, cr, cb);
        }
    }

  最后,要想能做好自定义View,以上介绍的方法和注意点还远远不够,像是Path、Matrix、事件分发和冲突解决、ViewDragHelper、GestureDetector等等都是要了解一些

  可以参考以下博文:

Android ViewDragHelper完全解析 自定义ViewGroup神器

Android Xfermode 实战 实现圆形、圆角图片

GcsSloop的自定义View系列

猜你喜欢

转载自blog.csdn.net/unicorn97/article/details/80878318