Android進階——自定義View之擴充套件系統控制元件的另一種思路實現漸變文字動畫的TextView
引言
前面幾篇文章
繼承或組合系統現有控制元件實現新控制元件,擴充套件新功能都是在對應的構造方法中去擴充套件的,但千萬不要把思路侷限於只能在構造方法中去擴充套件,這篇就簡單地分享另一種思路,通過重寫對應的週期方法實現擴充套件。
一、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的成員方法擴充套件功能這就是我說的另一種思路。