前言
据说自定义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,自定义控件明显比原生控件效率更高。性能都是用数据对比出来的,一下觉得很有成就感,如果更多的使用自定义控件可以达到明显优化内存效果。