实现自定义View并打包成aar

来源:互联网 发布:python append用法 编辑:程序博客网 时间:2024/06/16 00:29

前言

据说自定义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 0