实现自定义View并打包成aar

前言

据说自定义View是搞android进阶必须掌握的技能,加上最近研究android studio的内存工具,发现直接使用图片消耗的内存超乎想像,听说原生控件效率低,就做了一个自定义的控件,一来熟悉下自定义view,二来试试能不能通过这种方式减小内存开销,如果可以就直接代替之前用的控件。
先上截图,下面是项目中使用图片资源较多的一个页面,用android studio的内存工具测试发现,最下方的那个“完成”按钮是一个TextView,消耗了较多内存,它点击和不点击时呈现不一样的背景色。
不点击时

点击时

用安卓自身的TextView实现,background设置为一个drawable文件,里面定义一个selector,点击和不点击时显示的图片不一样,初学者都会。

android:background="@drawable/wancheng_selector"
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="false" android:drawable="@drawable/btn_wancheng_zc"/>
    <item android:state_pressed="true" android:drawable="@drawable/btn_wancheng_dj"/>

</selector>

当时UI直接把背景图片资源都提供好了,就直接拿来用的。这两个资源图都不大,一个900多B,一个800多B,连1KB都不到,下面用android studio自带的内存工具看下这个控件运行时消耗的内存:
这里写图片描述
有一项Dominating Size竟然有3MB,为了弄清它的含义,查到了android studio的文档
这里是官方文档的解释 我把图片截出来:
这里写图片描述

Depth是从根节点到对象最短的引用次数,Shallow Size是对象本身大小,Dominating Size是对象控制的内存,按照stackoverflow上老外的说法,就是它本身+直接和间接引用所占用的内存。(http://stackoverflow.com/questions/33399789/android-studio-heap-snapshot-analyzer-what-does-dominating-size-represent
这是直接使用android原生控件TextView+图片实现一个带点击效果按钮所占用的内存,换成自定义View我们再看看内存的消耗。

自定义View的实现

我的想法是将自定义View封装成一个aar包,这样便于不同app使用。打包aar的方法后面也会涉及。
先来理清需求,一个功能比较完整的自定义View需要预备哪些功能:

  • 能设置View中显示的文字,既然可以设置文字相应的需要设置文字大小,颜色
  • 能设置控件宽度、高度
  • 能设置控件背景颜色
  • 能设置点击和不点击状态下的背景颜色
  • 四个角为圆角
  • 点击事件

暂时就这些,已经可以满足通常情况下的需求。那么需要给外部提供相应接口:

    public void setWidth(int width){
        ...
    }

    public void setHeight(int height){
        ...
    }

    public void setText(String str){
        ...
    }

    public void setTextColor(int color){
        ...
    }

    public void setTextSize(int size){
        ...
    }

    public void setBackgroundColor(int color){
        ...
    }

    public void setPressedColor(int color){
        ...
    }

    public void setUnPressedColor(int color){
        ...
    }

点击事件要麻烦点,最后再说。
自定义View有三个构造函数:
这里写图片描述
第一个是在代码里直接new一个控件;第二个是在activity里通过findViewById方法去初始化控件,控件属性是在layout里面得到的,比如长度、宽度这些;第三个是接收一个style参数(没用过,网上说是由另外两个显示调用)。我的做法是让第一个和第二个构造函数都去调用第三个构造函数,并判断如果xml中没有赋值则给一些默认值:

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

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

    public RoundConerTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        InitAttribute(attrs, defStyleAttr);
    }

RoundConerTextView就是自定义View的名字,再定义一个属性xml文件attrs放在res\values:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="RoundConerTextView">
        <attr name="Text" format="string"/>
        <attr name="TextColor" format="color"/>
        <attr name="TextSize" format="dimension"/>
        <attr name="BackgroundColor" format="color"/>
        <attr name="ConerRadius" format="dimension"/>
        <attr name="widthSize" format="dimension"/>
        <attr name="HeightSize" format="dimension"/>
        <attr name="pressedColor" format="color"/>
        <attr name="unPressedColor" format="color"/>
    </declare-styleable>
</resources>

这个attrs.xml的作用是对自定义view自身属性作格式定义。
第二个构造函数是在layout中使用自定义view时调用的,先看下layout中的使用:

    <acxingyun.cetcs.com.roundconertextview.RoundConerTextView
        android:id="@+id/RoundConerTextView"
        android:layout_below="@id/hello"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        RoundConerTextView:Text="完成"
        RoundConerTextView:BackgroundColor="@color/roundconerunpressed"
        RoundConerTextView:ConerRadius="5dp"
        RoundConerTextView:HeightSize="20dp"
        RoundConerTextView:widthSize="40dp"
        RoundConerTextView:TextSize="10sp"
        RoundConerTextView:TextColor="@android:color/white"
        RoundConerTextView:unPressedColor="@color/finish_unpressed"
        RoundConerTextView:pressedColor="@color/finish_pressed"
        />

第二个构造函数直接调用了第三个构造函数,读取xml中的属性值:

private void InitAttribute(AttributeSet attrs, int defStyleAttr){
        TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.RoundConerTextView, defStyleAttr, 0);
        int count = typedArray.getIndexCount();
        for (int i = 0; i<count; i++){
            int index = typedArray.getIndex(i);
            if (index == R.styleable.RoundConerTextView_Text){
                mText = typedArray.getString(index);
            }else if (index == R.styleable.RoundConerTextView_TextColor){
                mTextColor = typedArray.getColor(index,mTextColor);
            }else if (index == R.styleable.RoundConerTextView_TextSize){
                mTextSize = typedArray.getDimensionPixelSize(index, mTextSize);
            }else if (index == R.styleable.RoundConerTextView_BackgroundColor){
                mBackgroundColor = typedArray.getColor(index, mBackgroundColor);
            }else if (index == R.styleable.RoundConerTextView_ConerRadius){
                mConerRadius = typedArray.getDimensionPixelSize(index, mConerRadius);
            }else if (index == R.styleable.RoundConerTextView_widthSize){
                mWidth = typedArray.getDimensionPixelSize(index, mWidth);
            }else if (index == R.styleable.RoundConerTextView_HeightSize){
                mHeight = typedArray.getDimensionPixelSize(index, mHeight);
            }else if (index == R.styleable.RoundConerTextView_pressedColor){
                mPressedColor = typedArray.getColor(index, mPressedColor);
            }else if (index == R.styleable.RoundConerTextView_unPressedColor){
                mUnPressedColor = typedArray.getColor(index, mUnPressedColor);
            }
        }
        typedArray.recycle();
        //如果在layout中没有定义,则赋默认值
        ..........
}        

到这里完成了对自定义view属性的初始化,下面是onMeasure()和onDraw()。
先看看onMeasure():

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        if (mWidthMode == 0){
            mWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        }

        if (mHeightMode == 0){
            mHeightMode = MeasureSpec.getMode(heightMeasureSpec);
        }

        int wideSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int computeWidth;
        int computeHeight;

        switch (mWidthMode){
            case MeasureSpec.EXACTLY:
                wideSize = mWidth;
                break;

            case MeasureSpec.AT_MOST:
                computeWidth = getPaddingLeft() + getPaddingRight() + mConerRadius * 2 + mWidth;
                wideSize = computeWidth < wideSize ? computeWidth : wideSize;
                break;

            case MeasureSpec.UNSPECIFIED:
                wideSize = getPaddingLeft() + getPaddingRight() + mConerRadius * 2 + mWidth;
                break;
        }

        switch (mHeightMode){
            case MeasureSpec.EXACTLY:
                heightSize = mHeight;
                break;

            case MeasureSpec.AT_MOST:
                computeHeight = getPaddingTop() + getPaddingBottom() + mConerRadius * 2 + mHeight;
                heightSize = computeHeight < heightSize ? computeHeight : heightSize;
                break;

            case MeasureSpec.UNSPECIFIED:
                heightSize = getPaddingTop() + getPaddingBottom() + mConerRadius * 2 + mHeight;
                break;
        }

        setMeasuredDimension(wideSize, heightSize);
    }

得到长宽测试模式,根据不同模式计算实际的长宽。
关于三种模式的解释:
http://blog.csdn.net/a396901990/article/details/36475213

  • 三种Mode:
    • 1.UNSPECIFIED
    • 父不没有对子施加任何约束,子可以是任意大小(也就是未指定)
    • (UNSPECIFIED在源码中的处理和EXACTLY一样。当View的宽高值设置为0的时候或者没有设置宽高时,模式为UNSPECIFIED
    • 2.EXACTLY
    • 父决定子的确切大小,子被限定在给定的边界里,忽略本身想要的大小。
    • (当设置width或height为match_parent时,模式为EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的)
    • 3.AT_MOST
    • 子最大可以达到的指定大小
    • (当设置为wrap_content时,模式为AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸)

除了EXACTLY这种mode,另外两种都需要自己计算大小,mHeight和mWidth都是在构造函数中从layout文件中读出来的。最后调用setMeasuredDimension(wideSize, heightSize)后会进入onSizeChanged():

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
    }

把自定义view的宽度和高度传给成员变量,w,h就是setMeasuredDimension传进来的。
最后是onDraw(),调用canvas画圆角矩形和文字,在画之前要定义画笔,计算位置,先上代码:

    @Override
    protected void onDraw(Canvas canvas) {
        //drawBackground
        int left = getPaddingLeft();
        int top = getPaddingTop();
        int right = getPaddingLeft() + mWidth;
        int bottom = getPaddingTop() + mHeight;
        mBackgroundPaint.setColor(mBackgroundColor);

        mRectF.set(left, top, right, bottom);
        canvas.drawRoundRect(mRectF, mConerRadius, mConerRadius, mBackgroundPaint);
        //drawText
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextSize(mTextSize);
        mFontMetrics = mTextPaint.getFontMetrics();

        float baseLine = mHeight/2 - (mFontMetrics.descent + mFontMetrics.ascent)/2;
        canvas.drawText(mText, mWidth/2, baseLine, mTextPaint);
    }

canvas.drawRoundRect传四个参数,矩形、圆角x方向半径、圆角y方向半径还有画笔;canvas.drawText就比较复杂了,先定义画笔:

        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextSize(mTextSize);
        mTextPaint.setTextAlign(Paint.Align.CENTER);

setTextAlign意思是让文字左右对称,结合canvas.drawText给x坐标传mWidth/2可以让文字布局在view的正中;麻烦的是第三个参数baseLine,它的含义是英文字母的基线,大部分字母的基线就是它的底部坐标,有些比如f,j这些基线是在中间偏下一点的位置,通过下面这个图我总结了baseLine的计算方法:
这里写图片描述
fontMetrics是根据画笔得到的:

mFontMetrics = mTextPaint.getFontMetrics();

它是一个包含textview的矩形。
实现一个控件还需要一个onLayout,但这个控件比较简单,没有涉及到,不需要override。
通过以上工作,完成了自定义view的代码编写,下面是打包成aar供应用使用。

打包aar

新建一个module,选择android library:
这里写图片描述
然后就开始码代码,下面是我这个module的结构截图,取名叫roundconertextview:
这里写图片描述
attrs.xml就是对控件属性的格式定义,代码都在RoundConerTextView.java里面。
代码写完后build一下,在下面这个路径就会生成一个debug版本的aar包:
这里写图片描述
重命名再拷贝到其它工程里就可以使用了,下面再附上具体的使用方法。

自定义控件的使用

之前说过,属性是从layout中赋值的,为了方便再贴一遍layout中的代码:

        <acxingyun.cetcs.com.roundconertextview.RoundConerTextView
            android:id="@+id/finishTV"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true"
            android:gravity="center"
            RoundCornerTextView:Text="@string/wancheng"
            RoundCornerTextView:BackgroundColor="@color/roundconerunpressed"
            RoundCornerTextView:ConerRadius="5dp"
            RoundCornerTextView:HeightSize="20dp"
            RoundCornerTextView:widthSize="40dp"
            RoundCornerTextView:TextSize="10sp"
            RoundCornerTextView:TextColor="@android:color/white"
            RoundCornerTextView:pressedColor="@color/finish_pressed"
            RoundCornerTextView:unPressedColor="@color/finish_unpressed"
            />

上面就是一个activity在layout中对控件的使用,属性都是在这里赋值,使用自定义view在最外层layout要加上下面一段代码:

xmlns:RoundCornerTextView="http://schemas.android.com/apk/res-auto"

在java代码中,调用自身方法设置长宽,这样做是方便自适应布局:

        finishTV.setHeight(110 * globalData.mScreenHeight / 1920);
        finishTV.setWidth(800 * globalData.mScreenWidth / 1080);
        finishTV.setTextSize(48 * globalData.mScreenHeight / 1920);
        finishTV.refreshView();

传入的参数是我自己计算的适应不同分辨率需要的比例,refreshView是调用view的invalidate()方法,它会调用view的onDraw(),重新绘制一次。

点击响应

最后是点击响应,就是override onTouch(),按下的时候改变画笔颜色,调用invalidate()刷新;抬起的时候恢复画笔颜色,再调用invalidate()刷新。
颜色只是视觉效果,点击事件需要一个回调接口,因此在控件里面还需要定义一个接口和对应的注册方法:

    public interface onTouchCallback{
        public void onRoundTextViewTouched();
    }

    onTouchCallback mOnTouchCallback;

    public void setOnTouchCallback(onTouchCallback cb){
        mOnTouchCallback = cb;
    }

在onTouch()里面调用 :

后来用的时候才发现有个小问题,就是按下后把手指移出控件范围,颜色仍然是按下时的颜色,这和原生控件效果不一样,为了完善体验,还要在onTouch()中计算按下时的坐标和控件的关系,并且要把onClick()一起override,下面是点击事件的完整实现:

class ActionListener implements OnTouchListener,OnClickListener{

        @Override
        public void onClick(View v) {

        }

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            int action = event.getAction();

            int viewHeight = getHeight();
            int viewWidth = getWidth();

            float touchX = event.getX();
            float touchY = event.getY();

            if (touchX < 0 || touchX > viewWidth || touchY < 0 || touchY > viewHeight){
                mBackgroundColor = mUnPressedColor;
                invalidate();
                return false;
            }

            boolean upAction;
            switch (action){
                case MotionEvent.ACTION_DOWN:
                    mBackgroundColor = mPressedColor;
                    upAction = false;
                    break;

                case MotionEvent.ACTION_UP:
                    mBackgroundColor = mUnPressedColor;
                    upAction = true;
                    break;

                default:
                    mBackgroundPaint.setColor(getResources().getColor(R.color.roundconerunpressed));
                    upAction = false;
                    break;
            }

            mBackgroundPaint.setStyle(Paint.Style.FILL);
            invalidate();
            if (upAction && mOnTouchCallback != null){
                mOnTouchCallback.onRoundTextViewTouched();
            }
            return false;
        }
    }

注册监听,放在构造函数中:

        mActionListener = new ActionListener();

        this.setOnClickListener(mActionListener);
        this.setOnTouchListener(mActionListener);

在应用中使用的时候:

mRoundConerTextView.setOnTouchCallback(new RoundConerTextView.onTouchCallback() {
            @Override
            public void onRoundTextViewTouched() {
                ......
            }
        });

有点晕,我按照自己的思路总结,当时也是一边写代码一边测试,发现问题再完善代码。

性能

最后来看看使用时的效果,样子就是最开始那两张图,下面是内存消耗:
这里写图片描述
Shallow Size这一项,之前是700多,现在是400多,Dominating Size就更明显了,以前3MB,现在1KB,自定义控件明显比原生控件效率更高。性能都是用数据对比出来的,一下觉得很有成就感,如果更多的使用自定义控件可以达到明显优化内存效果。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值