原理介紹:
上一篇文章對過度繪制和View優化做了大概的簡介和介紹。我們知道,引起過度繪制的根本原因是 背景(背景包裹背景圖檔、顔色、形狀、邊框等等)。
那麼通常你在網絡查找過度繪制優化政策,不外乎減少View的層疊、多餘的控件、一個多餘的背景設定等等。可如果你真的去審查代碼,會發現你并沒有多少可優化的地方。這個時候,你就需要 Android過度繪制深度優化—View提前繪制
先說原理:不做處理的View繪制過程像一把刷子一層層去繪制View,第一層刷一個形狀,第二層刷背景圖檔/顔色,第三層刷文字等等,那麼,如果我們将View提前畫好,然後交給系統去繪制。這樣不管你的View之前刷過多少次,系統隻需要繪制一次。這樣,過度繪制就可以輕松解決啦!
先上效果圖:
優化前 & 優化後:
Tips:
1.首先控件的實作方式略有不同(帶圖示的輸入框),但對我們這片文章講的東西并沒有沖突。
2.優化後的圖檔,為了突出是以添加了帶圖案的背景圖檔
顔色說明:
優化前:
- Window(無色) 繪制一次
- 背景(藍色) 繪制2次
- Logo,輸入框帶小圖示的背景,按鈕,忘記密碼(綠色)繪制三次
- 輸入框本身,按鈕文字 (紅色) 繪制三次
- 輸入框内部文字 (深紅色) 繪制四次
優化後:
- 背景(無色) 繪制一次 : 背景設定到主題後,将取代原本Window預設背景。
- Logo,按鈕,帶形狀和顔色的輸入框背景,忘記密碼文字 (藍色) 繪制兩次: 這裡我們對輸入框和按鈕進行了提前繪制,并去掉了按鈕預設背景,是以按鈕加其内部文字隻繪制一次。帶形狀、顔色的輸入框隻繪制一次。
- 文本框上面的圖檔和文字(綠色)繪制三次
注意要點
View提前繪制可以從根本上去解決問題過度繪制問題,但并不是沒有代價的,但你在做優化前,需要注意一下幾個點:
- View的提前繪制隻能應用到那些靜态的View
- View的提前繪制并不能加過View的繪制速度,甚至會有小幅度的繪制時間增加。(提前在記憶體中進行View的繪制是有代價的)
- 界面是有一定重新整理頻率的,每一次重新整理都會調用View的onDraw方法,而View提前繪制就是在onDraw中進行。是以你需要考慮效率和性能問題。如:避免在onDraw建立對象,避免在onDraw進行繪制,應在構造函數中畫好,交給onDraw。
- 對于登入界面,提前繪制的工作量和其收益比起來是得不償失的,是以你需要進行權衡,哪裡需要提前繪制。通常我是這樣做的:大量被重用的控件,非常複雜的布局,布局之上有動畫效果(過度繪制對動畫的影響是極大的)
也許在你看到View的提前繪制是有代價的,就決定不在進行優化,這是錯誤的。除非極其複雜的View,負責提前繪制帶來收益是絕對大于那多出來的一丢丢繪制時間的。
另外即使類似于登入界面這樣的簡單布局沒有必要進行過度繪制優化,但依然有大量的優秀公司進行了優化。我認為有兩個點:
1.登入界面是應用打開的第一個界面,也是整個應用最簡單的界面之一,用它來試手肯定是第一選擇。
2.這是一個态度和逼格的問題。打開過度繪制調試後,别人家的應用藍藍的,怎麼你的應用就是紅彤彤一片?
正文
啊,好吧,廢話說的有點多了,下面我們上代碼。
Github連結:https://github.com/lizhaoxuan/SoftWidgetDemo.git
首先看目錄結構:
有四個核心類:
DrawingCanvas.java 我們提前繪制就在這個類上畫,然後交給onDraw,一氣呵成。
PaintBox.java 裡面包含了幾個繪制方法,比如畫背景顔色和形狀
SRoundRect_Button.java SRoundRect_LinearLayout.java 是兩個自定義控件,從名字上可以看出來,兩個圓角矩形的按鈕和線性布局
然後看布局檔案:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="10dp">
<ImageView
android:id="@+id/logoView"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerHorizontal="true"
android:layout_marginTop="80dp"
android:src="@drawable/logo"/>
<com.example.zhaoxuanli.softwidgetdemo.soft.widget.SRoundRect_LinearLayout
android:id="@+id/accountLayout"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="50dp"
android:layout_marginRight="20dp"
android:layout_marginLeft="20dp"
android:layout_below="@id/logoView"
android:orientation="horizontal"
android:paddingLeft="6dip"
android:paddingRight="6dip"
android:paddingTop="8dip"
app:bgcolor="#CFCFCF">
<ImageView
android:layout_width="wrap_content"
android:layout_height="22dip"
android:layout_gravity="center_vertical"
android:layout_marginLeft="8dip"
android:layout_marginRight="8dip"
android:paddingBottom="6dip"
android:src="@drawable/ic_login_user" />
<EditText
android:id="@+id/accountEdit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="8dip"
android:layout_weight="1"
android:background="@null"
android:digits="\@0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.,_-"
android:hint="賬号"
android:imeOptions="actionNext"
android:paddingBottom="8dip"
android:singleLine="true"
android:textColor="@color/black"
android:textCursorDrawable="@null" />
</com.example.zhaoxuanli.softwidgetdemo.soft.widget.SRoundRect_LinearLayout>
<com.example.zhaoxuanli.softwidgetdemo.soft.widget.SRoundRect_LinearLayout
android:id="@+id/passwordLayout"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="3dp"
android:layout_marginRight="20dp"
android:layout_marginLeft="20dp"
android:layout_below="@id/accountLayout"
android:orientation="horizontal"
android:paddingLeft="6dip"
android:paddingRight="6dip"
android:paddingTop="8dip"
app:bgcolor="#CFCFCF">
<ImageView
android:layout_width="wrap_content"
android:layout_height="22dip"
android:layout_gravity="center_vertical"
android:layout_marginLeft="8dip"
android:layout_marginRight="8dip"
android:paddingBottom="6dip"
android:src="@drawable/ic_login_password" />
<EditText
android:id="@+id/passwordEdit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="8dip"
android:layout_weight="1"
android:background="@null"
android:digits="\@0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.,_-"
android:hint="密碼"
android:imeOptions="actionNext"
android:paddingBottom="8dip"
android:singleLine="true"
android:textColor="@color/black"
android:textCursorDrawable="@null"
android:inputType="textPassword"/>
</com.example.zhaoxuanli.softwidgetdemo.soft.widget.SRoundRect_LinearLayout>
<com.example.zhaoxuanli.softwidgetdemo.soft.widget.SRoundRect_Button
android:id="@+id/login_btn"
android:layout_width="fill_parent"
android:layout_height="50dp"
android:layout_marginTop="20dp"
android:layout_below="@id/passwordLayout"
android:layout_alignLeft="@id/passwordLayout"
android:layout_alignRight="@id/passwordLayout"
android:layout_gravity="center"
android:text="登入"
android:textColor="@color/white"
android:textSize="20sp"
app:backgroundColor="#af0308"/>
<TextView
android:id="@+id/forgetText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_marginBottom="15dp"
android:layout_marginLeft="10dp"
android:gravity="center"
android:textColor="#af0308"
android:textSize="15sp"
android:text="忘記密碼"/>
<TextView
android:id="@+id/registerText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginBottom="15dp"
android:layout_marginRight="20dp"
android:gravity="center"
android:textColor="#af0308"
android:textSize="15sp"
android:text="注冊"/>
</RelativeLayout>
這裡要說一下:
不加背景和形狀的TextView 不需要進行優化,它隻進行一次繪制。
不加形狀的ImageView不需要進行優化,即使設定了background或src屬性,也隻繪制一次。
Button 本身是有背景的,是以你需要手動将它原來的背景去掉,在代碼裡:this.setBackgroundResource(0);
通過代碼我們可以看到,輸入框的布局和Button是自定義控件,是以我對這兩個做了提前繪制優化。理論上來說是可以将小圖示也提前繪制進去的,這樣輸入框的小圖示也将是藍色,但計算位置複雜,也不利于擴充。
DrawingCanvas.java
package com.example.zhaoxuanli.softwidgetdemo.soft.widget.PaintBox;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.RectF;
/**
* Created by zhaoxuan.li on 2015/8/11.
* 在這上面進行提前繪制
*/
public class DrawingCanvas extends Canvas {
private Bitmap output;
private Rect rect;
private RectF rectF;
private DrawingCanvas(Bitmap output_, float width_, float height_){
super(output_);
output = output_;
rect = new Rect(0,0,(int)width_,(int)height_);
rectF = new RectF(0, 0, width_, height_);
}
public static DrawingCanvas instance(float width_ , float height_){
Bitmap bitmap = Bitmap.createBitmap((int) width_, (int) height_,
Bitmap.Config.ARGB_8888);
return new DrawingCanvas(bitmap,width_,height_);
}
public Bitmap getOutput(){
return output;
}
public Rect getRect(){
return rect;
}
public RectF getRectF(){
return rectF;
}
}
這個類繼承了Canvas,并且已經傳入一個Bitmap
super(output_);
是以我們可以直接在這上面畫畫。并且還提供了一些Rect,RectF 屬性,供之後進行繪制時使用。
PaintBox.java
package com.example.zhaoxuanli.softwidgetdemo.soft.widget.PaintBox;
import android.graphics.Paint;
/**
* Created by zhaoxuan.li on 2015/8/12.
* 用來畫圖的盒子,裡面放着畫圖的方法
*/
public class PaintBox {
/**
* 畫圓角矩形
* @param canvas 畫布
* @param color 填充顔色
* @param alpha 透明度
*/
public static void drawRoundRect(DrawingCanvas canvas ,int color , int alpha){
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
paint.setColor(color);
paint.setAlpha(alpha);
canvas.drawRoundRect(canvas.getRectF(), 20, 20, paint);
}
/**
* 畫圓角矩形
* @param canvas 畫布
* @param color 填充顔色
* @param alpha 透明度
* @param border 帶邊框
*/
public static void drawRoundRect(DrawingCanvas canvas ,int color , int alpha,boolean border){
if(border){
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
paint.setColor(color);
paint.setAlpha(30);
canvas.drawRoundRect(canvas.getRectF(), 20, 20, paint);
paint.setStyle(Paint.Style.STROKE);
paint.setAlpha(70);
paint.setStrokeWidth(4);
canvas.drawRoundRect(canvas.getRectF(),20,20,paint);
}else{
drawRoundRect(canvas,color,alpha);
}
}
/**
* 填充顔色
* @param canvas
* @param color
*/
public static void drawColor(DrawingCanvas canvas ,int color ){
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
paint.setColor(color);
canvas.drawPaint(paint);
}
/**
* 寫字在View正中間
* @param canvas 畫布
* @param text 文字
* @param color 文字顔色
* @param size 文字大小
*/
public static void drawTextCenter(DrawingCanvas canvas , String text , int color,int size){
//建立畫筆
Paint pp = new Paint();
pp.setAntiAlias(true);
pp.setColor(color);
pp.setStrokeWidth(3);
pp.setTextSize(size);
pp.setTextAlign(Paint.Align.CENTER);
Paint.FontMetricsInt fontMetrics = pp.getFontMetricsInt();
float vertical = canvas.getRectF().top + (canvas.getRectF().bottom - canvas.getRectF().top - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top;
canvas.drawText(text, canvas.getRectF().centerX(), vertical, pp);
}
}
這個類比較簡單了,你有什麼需求,就在這裡建立一個方法就好了,然後傳入一個Canvas和一些必要參數,它幫你把你需要的東西畫到Canvas上。
值得注意的是最後一個方法:drawTextCenter。 将文字繪制到View中間。這個是沒有直接辦法實作的,是以你需要計算一下位置。
最後是我們的自定義控件,篇幅太長,是以我們隻看一個Button的,LinearLayout的可以下載下傳代碼看
package com.example.zhaoxuanli.softwidgetdemo.soft.widget;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.widget.Button;
import com.example.zhaoxuanli.softwidgetdemo.R;
import com.example.zhaoxuanli.softwidgetdemo.soft.widget.PaintBox.DrawingCanvas;
import com.example.zhaoxuanli.softwidgetdemo.soft.widget.PaintBox.PaintBox;
/**
* Created by zhaoxuan.li on 2015/8/12.
* 圓角矩形Button,無邊框。 可以放一個顔色當做背景,
*/
//
//字元編碼 UTF-8 無BOM
public class SRoundRect_Button extends Button {
private String text;
private int textColor;
private int textSize;
private int backgroundColor;
/**
* 提前繪制的畫布
* 之前為了追求性能,是以用了軟引用,但是在紅米2上發現Bug
* DrawingCanvas被系統收回了,導緻空指針異常。這裡還要好好研究一下
* 反編譯Path源碼後,這裡是采用軟引用的
*/
private DrawingCanvas localDrawingCanvas;
/**
* 一下是超類的三個構造方法
* @param context
*/
public SRoundRect_Button(Context context) {
super(context);
init(context,null , 0);
}
public SRoundRect_Button(Context context, AttributeSet attrs) {
super(context, attrs);
init(context,attrs , 0);
}
public SRoundRect_Button(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs , defStyle);
}
private void init(Context context , AttributeSet attrs , int defStyle ){ //初始化操作
this.setBackgroundResource(0); //去掉Button原來的背景
if(null == attrs)
return ;
/**
* View屬性的提取
*/
Resources resources = getResources();
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SRoundRect_Button);
text = a.getString(R.styleable.SRoundRect_Button_android_text);
textColor = a.getColor(R.styleable.SRoundRect_Button_android_textColor, resources.getColor(R.color.white));
textSize = a.getDimensionPixelSize(R.styleable.SRoundRect_Button_android_textSize, 20);
backgroundColor = a.getColor(R.styleable.SRoundRect_Button_backgroundColor, 0Xaf0308);
}
@Override
protected void onDraw(Canvas canvas) {
//取得View的長寬資訊,因為每一次View的載入長款
float width = this.getWidth();
float height = this.getHeight();
/**
* 因為界面有一定的重新整理率,每一次重新整理都會調用onDraw方法
* 是以為了效率和性能考慮,需要做一些判斷避免重複判斷
*/
if(localDrawingCanvas==null){
localDrawingCanvas = DrawingCanvas.instance(width,height);
PaintBox.drawRoundRect(localDrawingCanvas, backgroundColor, 30);
PaintBox.drawTextCenter(localDrawingCanvas,text,textColor,textSize);
}
canvas.drawBitmap(localDrawingCanvas.getOutput(),0,0,null);
}
}
這就是一個提前繪制的自定義控件的實作了。
這裡有一個疑問:android:background 屬性,我們可以設定圖檔、顔色、甚至是xml寫得背景。這個究竟是如何實作的?
因為實作不了這個效果,是以隻能添加了自定義屬性
<attr name="backgroundPic" format="reference"/>
<attr name="backgroundColor" format="color"/>
感覺很Low啊這樣!!!還期待大神幫解決。
哦,CSDN也上傳了源代碼。
http://download.csdn.net/detail/u010255127/9217927