实现自定义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,自定义控件明显比原生控件效率更高。性能都是用数据对比出来的,一下觉得很有成就感,如果更多的使用自定义控件可以达到明显优化内存效果。
- 实现自定义View并打包成aar
- android studio 打包AAR并将AAR引入u3d
- gradle 编译打包并使用 aar
- gradle 编译打包并使用 aar
- WebService打包成.aar文件
- aar打包
- Android 将Android项目打包成aar文件并在其他项目中引用,打包成jar包
- android studio打包生成aar文件并在其他工程引用aar包
- AS 将模块打包成 aar
- Android 完整项目打包成 aar 详解
- 自定义framework并打包
- ios中自定义alert view,并实现动画组合
- ios中自定义alert view,并实现动画组合
- Android studio 打包aar
- axis2 打包 aar文件
- Android打包jar,aar
- Andorid 代码打包--aar
- Android studio 打包aar、导入aar
- Android数据存储之Sqlite的介绍及使用
- 网络编程4--毕向东java基础教程视频学习笔记
- Python27 和 IronPython 处理文件读写的字符编码问题
- 多线程2--毕向东基础视频教程学习笔记
- 通过WebService接口生成WSDL文件
- 实现自定义View并打包成aar
- 集合1--毕向东java基础教程视频学习笔记
- 在JS API中使用代理访问安全服务时报PKIX path building failed简便解决方案
- .Net constants vs Settings vs Resources
- WebService的简单实例
- 软考题目之头结点、头指针和首元节点
- C# 自定义配置节点简例
- Execution failed for task ':XXXX:processDebugManifest'. > Manifest merger failed with multiple
- EJB的编程规则之Session Bean