引言
前面几篇文章
- Android进阶——自定义View之继承系统控件实现自带删除按钮动画效果和软键盘自动悬浮于文本框下方
- Android进阶——自定义View之重写ViewGroup组合系统控件实现自定义ToolBar模板
- Android进阶——自定义View之继承TextView巧用DrawableLeft实现自己的CheckableTextView
- Android进阶——自定义View之组合系统控件实现水珠形状的ItemView
继承或组合系统现有控件实现新控件,扩展新功能都是在对应的构造方法中去扩展的,但千万不要把思路局限于只能在构造方法中去扩展,这篇就简单地分享另一种思路,通过重写对应的周期方法实现扩展。
一、View中几种重要的方法
onFinishInflate:从XML加载了控件之后自动回调的
onSizeChanged:组件大小改变时回调,当第一次完成控件的测量也会触发回调
onMeasure:通过回调该方法来进行测量,具体参见我的另一篇文章
onLayout:通过回调该方法来确定显示的位置,即布局
onTouchEvent:当控件监听到触摸事件的时候回调
onDraw:控件最终呈现的效果都是由onDraw决定的
虽然我们在自定义View的时候不一定什么情况下都必须重写这所有的方法,我们克根据各自的业务来重写对应的方法,同时这也是Android控件架构强大灵活性的体现。
二、dp、sp和px的互相转换
- dp(dip): device independent pixels(设备独立像素).
不同设备有不同的显示效果,这个和设备硬件有关,一般我们为了支持WVGA、HVGA和QVGA 推荐使用这个,不依赖像素。如果设置表示长度、高度等属性时可以使用dp。dp是与密度无关,sp除了与密度无关外,还与scale无关。如果屏幕密度为160,这时dp和sp和px是一
样的。1dp=1sp=1px,但如果使用px作单位,如果屏幕大小不变(假设还是3.2寸),而屏幕密度变成了320。那么原来TextView的宽度设成160px,在密度为320的3.2寸屏幕里看要比在密度为160的3.2寸屏幕上看短了一半。但如果设置成160dp或160sp的话。系统会自动将width属性值设置成320px的。也就是160 * 320 / 160。其中320 /160可称为密度比例因子。也就是说,如果使用dp和sp,系统会根据屏幕密度的变化自动进行转换。 - px: pixels(像素)。不同设备显示效果相同,一般我们HVGA代表320x480像素,这个用的比较多。
- pt: point,是一个标准的长度单位,1pt=1/72英寸,用于印刷业,非常简单易用;
- sp: scaled pixels(放大像素).
主要用于字体显示best for textsize。
以下附一个转换工具类:
public class ScreenUtil {
/**
* 将px值转换为dip或dp值,保证尺寸大小不变
*
* @param pxValue
* @param scale (DisplayMetrics类中属性density)
* @return
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
/**
* 将dip或dp值转换为px值,保证尺寸大小不变
*
* @param dipValue
* @param scale (DisplayMetrics类中属性density)
* @return
*/
public static int dip2px(Context context, float dipValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
/**
* 将px值转换为sp值,保证文字大小不变
*
* @param pxValue
* @param fontScale (DisplayMetrics类中属性scaledDensity)
* @return
*/
public static int px2sp(Context context, float pxValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale + 0.5f);
}
/**
* 将sp值转换为px值,保证文字大小不变
*
* @param spValue
* @param fontScale (DisplayMetrics类中属性scaledDensity)
* @return
*/
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
}
二、重写系统控件的成员方法扩展新的功能
重写系统控件的成员方法扩展新的功能这也是一种自定义View的重要思想,很多时候原生的控件的功能对于我们来说只是欠缺了一点点,此时我们就可以把大部分的工作交给原生控件,然后再在原生的基础上进行再开发,而不一定要事必躬亲,完全继承View去绘制。以下直接通过两个简单的例子来说明。
1、实现自带边框和背景颜色图层的TextView
如上图所示,外边框为红色内部背景为绿色的简单TextView,可能利用Span家族也可以实现,但那不是重点,举这个例子仅仅是分享一种思想。首先,了解View的绘制流程的话,应该都知道onDraw是绘制UI效果的,那么TextView里的onDraw自然充当的是绘制要显示的文字的职责,这里顺道补充下常常在重写系统的一些方法的时候,默认地实现会去调用父类对应的方法,并不是所有的父类方法都需要手动去调用的,也不是所有的父类方法都可以不调用的,比如说这个小例子中的onDraw
@Override
protected void onDraw(Canvas canvas) {
drawOutsideRec(canvas);
drawInsideRec(canvas);
canvas.save();
Log.e("onDraw2","10px:"+ScreenUtil.px2dip(getContext(),30));
// canvas.translate(ScreenUtil.px2dip(getContext(),10),0);
canvas.translate(10,0);
//在回调父类方法之前,实现对应的逻辑,此例指的是在文字绘制之前
super.onDraw(canvas);
//在回调父类方法之后,实现对应的逻辑,此例指的是在文字绘制之后
canvas.restore();
}
总之,继承系统控件时候父类成员方法的调用与否,呈现的UI效果也不尽相同。接下来就实现这个带边框和带背景的TextView
/**
* Auther: Crazy.Mo
* DateTime: 2017/5/5 10:20
* Summary:
*/
public class ColorfulTextView extends TextView {
private Paint paintInside,paintOutside;
public ColorfulTextView(Context context) {
super(context);
init();
}
public ColorfulTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ColorfulTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
paintInside=new Paint();
paintInside.setColor(getResources().getColor(android.R.color.holo_green_dark));
paintInside.setStyle(Paint.Style.FILL);
paintInside.setAntiAlias(true);
paintInside.setDither(true);
paintOutside=new Paint();
paintOutside.setColor(getResources().getColor(android.R.color.holo_red_light));
paintOutside.setStyle(Paint.Style.STROKE);
paintOutside.setAntiAlias(true);
paintOutside.setDither(true);
}
private void drawInsideRec(Canvas canvas){
//canvas.drawRect(ScreenUtil.px2dip(getContext(),10),ScreenUtil.px2dip(getContext(),10),getMeasuredWidth()+ScreenUtil.px2dip(getContext(),10),getMeasuredHeight(),paintInside);
canvas.drawRect(10,10,getMeasuredWidth()-10,getMeasuredHeight()-10,paintInside);
}
private void drawOutsideRec(Canvas canvas){
// canvas.drawRect(0,0,ScreenUtil.px2dip(getContext(),getMeasuredWidth()),ScreenUtil.px2dip(getContext(),getMeasuredHeight()+20),paintOutside);
canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),paintOutside);
Log.e("onDraw2",ScreenUtil.px2dip(getContext(),getMeasuredWidth())+"measureWidth"+getMeasuredWidth()+"measureHeight"+getMeasuredHeight()+20);
}
@Override
protected void onDraw(Canvas canvas) {
drawOutsideRec(canvas);
drawInsideRec(canvas);
canvas.save();
Log.e("onDraw2","10px:"+ScreenUtil.px2dip(getContext(),30));
// canvas.translate(ScreenUtil.px2dip(getContext(),10),0);
canvas.translate(10,0);
super.onDraw(canvas);
canvas.restore();
}
}
2、实现文字闪烁动画的TextView
原理很简单,主要就是重写onSizeChanged方法中利用在getPaint()方法并拿到当前的Paint对象,再给Paint对象设置相应的Shader产生渐变效果,接着利用矩阵Matrix实现平移的动画效果,最后在onDraw里不断手动刷新即可。
package crazymo.training.colorfultextview.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.TextView;
/**
* Auther: Crazy.Mo
* DateTime: 2017/5/5 10:20
* Summary:
*/
public class ColorfulTextView extends TextView {
private Paint shineyPaint;
private LinearGradient linearGradient;
private Matrix shinerMatrix;
private int width,translate;
public ColorfulTextView(Context context) {
super(context);
Log.e("ViewRun","构造方法");
}
public ColorfulTextView(Context context, AttributeSet attrs) {
super(context, attrs);
Log.e("ViewRun","构造方法");
}
public ColorfulTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Log.e("ViewRun","构造方法");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Log.e("ViewRun","onLayout"+changed+"left:"+left+"top:"+ top+"right:"+ right+"bottom"+ bottom);
super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.e("ViewRun","onMeasure");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onFinishInflate() {
Log.e("ViewRun","onFinishInflate");
super.onFinishInflate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
Log.e("ViewRun","onSizeChanged"+"w"+w+"h"+h+"oldw"+oldw+"oldh"+oldh);
super.onSizeChanged(w, h, oldw, oldh);
if(width==0){//在View第一次呈现前初始化渐变对象,并拿到当前的Paint
width=getMeasuredWidth();
if(width>0){
shineyPaint=getPaint();//获取当前的Paint对象
linearGradient=new LinearGradient(0,0,width,0,new int[]{Color.GREEN,0xffffffff,Color.GREEN},null, Shader.TileMode.CLAMP);//构造和TextView一样宽度的渐变对象
shineyPaint.setShader(linearGradient);
shinerMatrix=new Matrix();
}
}
}
@Override
protected void onDraw(Canvas canvas) {
Log.e("ViewRun","onDraw");
super.onDraw(canvas);
if(shinerMatrix!=null){
translate+=width/5;
if(translate>width*2){
translate=-width;
}
shinerMatrix.setTranslate(translate,0);
linearGradient.setLocalMatrix(shinerMatrix);
postInvalidateDelayed(100);
}
}
}
小结
以上例子虽然很简单,主要是分享一种思路,在自定义View的时候不要只拘泥于重写构造方法,甚至是拘泥于onDraw方法,理论上任何成员方法都可以是被我们再开发的,前提是你得懂得基本的逻辑,重写View的成员方法扩展功能这就是我说的另一种思路。