版權宣告:本文為博主原創文章,未經博主允許不得轉載
系列教程:Android開發之從零開始系列
原始碼:AnliaLee/BookPage,歡迎star
大家要是看到有錯誤的地方或者有啥好的建議,歡迎留言評論
前言:前幾期部落格中我們分析了 書籍翻頁效果各部分的繪製原理,雖然效果都實現了,但測試過程中卻發現我們的View翻起頁來似乎 不是很流暢,這期便帶大家一起對View進行 效能優化
本篇只著重於思路和實現步驟,裡面用到的一些知識原理不會非常細地拿來講,如果有不清楚的api或方法可以在網上搜下相應的資料,肯定有大神講得非常清楚的,我這就不獻醜了。本著認真負責的精神我會把相關知識的博文連結也貼出來(其實就是懶不想寫那麼多哈哈),大家可以自行傳送。為了照顧第一次閱讀系列部落格的小夥伴,本篇會出現一些在之前系列部落格就講過的內容,看過的童鞋自行跳過該段即可
國際慣例,先上效果圖
封裝View觸控事件
在進行效能優化之前,首先感謝@布隆提出的建議:BookPageView作為一個完整的書頁自定義View,那麼對觸控事件的管理建議放在View的onTouchEvent中,而不是在外部setOnTouchListener,這樣保證了View功能的完整性也提高了使用上的方便性。那麼按照這樣的要求,修改BookPageView
public class BookPageView extends View {
//省略部分程式碼...
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
float x = event.getX();
float y = event.getY();
if(x<=viewWidth/3){//左
style = STYLE_LEFT;
setTouchPoint(x,y,style);
}else if(x>viewWidth/3 && y<=viewHeight/3){//上
style = STYLE_TOP_RIGHT;
setTouchPoint(x,y,style);
}else if(x>viewWidth*2/3 && y>viewHeight/3 && y<=viewHeight*2/3){//右
style = STYLE_RIGHT;
setTouchPoint(x,y,style);
}else if(x>viewWidth/3 && y>viewHeight*2/3){//下
style = STYLE_LOWER_RIGHT;
setTouchPoint(x,y,style);
}else if(x>viewWidth/3 && x<viewWidth*2/3 && y>viewHeight/3 && y<viewHeight*2/3){//中
style = STYLE_MIDDLE;
}
break;
case MotionEvent.ACTION_MOVE:
setTouchPoint(event.getX(),event.getY(),style);
break;
case MotionEvent.ACTION_UP:
startCancelAnim();
break;
}
return true;
}
}
複製程式碼
修改之後我們在Activity中不再需要呼叫setOnTouchListener了,在xml樣式檔案中也不再需要設定android:clickable="true"屬性
各區域顯示內容繪製優化(重用內容Bitmap)
相關博文連結
之前完成所有效果的繪製後,在手機上測試了下,發現翻頁不是很流暢,感覺卡卡的,遂開啟手機的GPU呈現模式分析,重新試下翻頁,然後。。。
Σ( ° △ °|||)︴尼瑪手機這是要炸了麼,趕緊翻程式碼找原因。一番檢查後,發現View每次在執行觸控翻頁操作時,都新建了A、B、C區域內容Bitmap,造成了不必要的開銷,實際上如果各區域顯示內容不變的情況下,內容Bitmap只需要初始化一次,以後每次繪製時僅需要重用原來的Bitmap即可。同理,View中能重用的物件就要儘量重用,修改我們的BookPageView
public class BookPageView extends View {
//省略部分程式碼...
private float[] mMatrixArray = { 0, 0, 0, 0, 0, 0, 0, 0, 1.0f };
private Matrix mMatrix;
private GradientDrawable drawableLeftTopRight;
private GradientDrawable drawableLeftLowerRight;
private GradientDrawable drawableRightTopRight;
private GradientDrawable drawableRightLowerRight;
private GradientDrawable drawableHorizontalLowerRight;
private GradientDrawable drawableBTopRight;
private GradientDrawable drawableBLowerRight;
private GradientDrawable drawableCTopRight;
private GradientDrawable drawableCLowerRight;
private Bitmap pathAContentBitmap;//A區域內容Bitmap
private Bitmap pathBContentBitmap;//B區域內容Bitmap
private Bitmap pathCContentBitmap;//C區域內容Bitmap
private void init(Context context, @Nullable AttributeSet attrs){
//省略部分程式碼...
mMatrix = new Matrix();
createGradientDrawable();
}
private void drawPathAContentBitmap(Bitmap bitmap,Paint pathPaint){
Canvas mCanvas = new Canvas(bitmap);
//下面開始繪製區域內的內容...
mCanvas.drawPath(getPathDefault(),pathPaint);
mCanvas.drawText("這是在A區域的內容...AAAA", viewWidth-260, viewHeight-100, textPaint);
//結束繪製區域內的內容...
}
private void drawPathBContentBitmap(Bitmap bitmap,Paint pathPaint){
Canvas mCanvas = new Canvas(bitmap);
//下面開始繪製區域內的內容...
mCanvas.drawPath(getPathDefault(),pathPaint);
mCanvas.drawText("這是在B區域的內容...BBBB", viewWidth-260, viewHeight-100, textPaint);
//結束繪製區域內的內容...
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int height = measureSize(defaultHeight, heightMeasureSpec);
int width = measureSize(defaultWidth, widthMeasureSpec);
setMeasuredDimension(width, height);
viewWidth = width;
viewHeight = height;
a.x = -1;
a.y = -1;
pathAContentBitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.RGB_565);
drawPathAContentBitmap(pathAContentBitmap,pathAPaint);
pathBContentBitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.RGB_565);
drawPathBContentBitmap(pathBContentBitmap,pathBPaint);
pathCContentBitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.RGB_565);
drawPathAContentBitmap(pathCContentBitmap,pathCPaint);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(a.x==-1 && a.y==-1){
drawPathAContent(canvas,getPathDefault());
}else {
if(f.x==viewWidth && f.y==0){
drawPathAContent(canvas,getPathAFromTopRight());
drawPathCContent(canvas,getPathAFromTopRight());
drawPathBContent(canvas,getPathAFromTopRight());
}else if(f.x==viewWidth && f.y==viewHeight){
drawPathAContent(canvas,getPathAFromLowerRight());
drawPathCContent(canvas,getPathAFromLowerRight());
drawPathBContent(canvas,getPathAFromLowerRight());
}
}
}
/**
* 初始化各區域陰影GradientDrawable
*/
private void createGradientDrawable(){
int deepColor = 0x33333333;
int lightColor = 0x01333333;
int[] gradientColors = new int[]{lightColor,deepColor};//漸變顏色陣列
drawableLeftTopRight = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, gradientColors);
drawableLeftTopRight.setGradientType(GradientDrawable.LINEAR_GRADIENT);
drawableLeftLowerRight = new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, gradientColors);
drawableLeftLowerRight.setGradientType(GradientDrawable.LINEAR_GRADIENT);
deepColor = 0x22333333;
lightColor = 0x01333333;
gradientColors = new int[]{deepColor,lightColor,lightColor};
drawableRightTopRight = new GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP, gradientColors);
drawableRightTopRight.setGradientType(GradientDrawable.LINEAR_GRADIENT);
drawableRightLowerRight = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, gradientColors);
drawableRightLowerRight.setGradientType(GradientDrawable.LINEAR_GRADIENT);
deepColor = 0x44333333;
lightColor = 0x01333333;
gradientColors = new int[]{lightColor,deepColor};//漸變顏色陣列
drawableHorizontalLowerRight = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, gradientColors);;
drawableHorizontalLowerRight.setGradientType(GradientDrawable.LINEAR_GRADIENT);
deepColor = 0x55111111;
lightColor = 0x00111111;
gradientColors = new int[] {deepColor,lightColor};//漸變顏色陣列
drawableBTopRight =new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT,gradientColors);
drawableBTopRight.setGradientType(GradientDrawable.LINEAR_GRADIENT);//線性漸變
drawableBLowerRight =new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT,gradientColors);
drawableBLowerRight.setGradientType(GradientDrawable.LINEAR_GRADIENT);
deepColor = 0x55333333;
lightColor = 0x00333333;
gradientColors = new int[]{lightColor,deepColor};//漸變顏色陣列
drawableCTopRight = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, gradientColors);
drawableCTopRight.setGradientType(GradientDrawable.LINEAR_GRADIENT);
drawableCLowerRight = new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, gradientColors);
drawableCLowerRight.setGradientType(GradientDrawable.LINEAR_GRADIENT);
}
/**
* 繪製A區域內容
* @param canvas
* @param pathA
*/
private void drawPathAContent(Canvas canvas, Path pathA){
canvas.save();
canvas.clipPath(pathA, Region.Op.INTERSECT);//對繪製內容進行裁剪,取和A區域的交集
canvas.drawBitmap(pathAContentBitmap, 0, 0, null);
if(style.equals(STYLE_LEFT) || style.equals(STYLE_RIGHT)){
drawPathAHorizontalShadow(canvas,pathA);
}else {
drawPathALeftShadow(canvas,pathA);
drawPathARightShadow(canvas,pathA);
}
canvas.restore();
}
/**
* 繪製A區域左陰影
* @param canvas
*/
private void drawPathALeftShadow(Canvas canvas, Path pathA){
canvas.restore();
canvas.save();
int left;
int right;
int top = (int) e.y;
int bottom = (int) (e.y+viewHeight);
GradientDrawable gradientDrawable;
if (style.equals(STYLE_TOP_RIGHT)) {
gradientDrawable = drawableLeftTopRight;
left = (int) (e.x - lPathAShadowDis /2);
right = (int) (e.x);
} else {
gradientDrawable = drawableLeftLowerRight;
left = (int) (e.x);
right = (int) (e.x + lPathAShadowDis /2);
}
Path mPath = new Path();
mPath.moveTo(a.x- Math.max(rPathAShadowDis, lPathAShadowDis) /2,a.y);
mPath.lineTo(d.x,d.y);
mPath.lineTo(e.x,e.y);
mPath.lineTo(a.x,a.y);
mPath.close();
canvas.clipPath(pathA);
canvas.clipPath(mPath, Region.Op.INTERSECT);
float mDegrees = (float) Math.toDegrees(Math.atan2(e.x-a.x, a.y-e.y));
canvas.rotate(mDegrees, e.x, e.y);
gradientDrawable.setBounds(left,top,right,bottom);
gradientDrawable.draw(canvas);
}
/**
* 繪製A區域右陰影
* @param canvas
*/
private void drawPathARightShadow(Canvas canvas, Path pathA){
canvas.restore();
canvas.save();
float viewDiagonalLength = (float) Math.hypot(viewWidth, viewHeight);//view對角線長度
int left = (int) h.x;
int right = (int) (h.x + viewDiagonalLength*10);//需要足夠長的長度
int top;
int bottom;
GradientDrawable gradientDrawable;
if (style.equals(STYLE_TOP_RIGHT)) {
gradientDrawable = drawableRightTopRight;
top = (int) (h.y- rPathAShadowDis /2);
bottom = (int) h.y;
} else {
gradientDrawable = drawableRightLowerRight;
top = (int) h.y;
bottom = (int) (h.y+ rPathAShadowDis /2);
}
gradientDrawable.setBounds(left,top,right,bottom);
Path mPath = new Path();
mPath.moveTo(a.x- Math.max(rPathAShadowDis, lPathAShadowDis) /2,a.y);
// mPath.lineTo(i.x,i.y);
mPath.lineTo(h.x,h.y);
mPath.lineTo(a.x,a.y);
mPath.close();
canvas.clipPath(pathA);
canvas.clipPath(mPath, Region.Op.INTERSECT);
float mDegrees = (float) Math.toDegrees(Math.atan2(a.y-h.y, a.x-h.x));
canvas.rotate(mDegrees, h.x, h.y);
gradientDrawable.draw(canvas);
}
/**
* 繪製A區域水平翻頁陰影
* @param canvas
*/
private void drawPathAHorizontalShadow(Canvas canvas, Path pathA){
canvas.restore();
canvas.save();
canvas.clipPath(pathA, Region.Op.INTERSECT);
int maxShadowWidth = 30;//陰影矩形最大的寬度
int left = (int) (a.x - Math.min(maxShadowWidth,(rPathAShadowDis/2)));
int right = (int) (a.x);
int top = 0;
int bottom = viewHeight;
GradientDrawable gradientDrawable = drawableHorizontalLowerRight;
gradientDrawable.setBounds(left,top,right,bottom);
float mDegrees = (float) Math.toDegrees(Math.atan2(f.x-a.x,f.y-h.y));
canvas.rotate(mDegrees, a.x, a.y);
gradientDrawable.draw(canvas);
}
/**
* 繪製B區域內容
* @param canvas
* @param pathA
*/
private void drawPathBContent(Canvas canvas, Path pathA){
canvas.save();
canvas.clipPath(pathA);//裁剪出A區域
canvas.clipPath(getPathC(),Region.Op.UNION);//裁剪出A和C區域的全集
canvas.clipPath(getPathB(), Region.Op.REVERSE_DIFFERENCE);//裁剪出B區域中不同於與AC區域的部分
canvas.drawBitmap(pathBContentBitmap, 0, 0, null);
drawPathBShadow(canvas);
canvas.restore();
}
/**
* 繪製B區域陰影,陰影左深右淺
* @param canvas
*/
private void drawPathBShadow(Canvas canvas){
int deepOffset = 0;//深色端的偏移值
int lightOffset = 0;//淺色端的偏移值
float aTof =(float) Math.hypot((a.x - f.x),(a.y - f.y));//a到f的距離
float viewDiagonalLength = (float) Math.hypot(viewWidth, viewHeight);//對角線長度
int left;
int right;
int top = (int) c.y;
int bottom = (int) (viewDiagonalLength + c.y);
GradientDrawable gradientDrawable;
if(style.equals(STYLE_TOP_RIGHT)){//f點在右上角
//從左向右線性漸變
gradientDrawable = drawableBTopRight;
left = (int) (c.x - deepOffset);//c點位於左上角
right = (int) (c.x + aTof/4 + lightOffset);
}else {
//從右向左線性漸變
gradientDrawable = drawableBLowerRight;
left = (int) (c.x - aTof/4 - lightOffset);//c點位於左下角
right = (int) (c.x + deepOffset);
}
gradientDrawable.setBounds(left,top,right,bottom);//設定陰影矩形
float rotateDegrees = (float) Math.toDegrees(Math.atan2(e.x- f.x, h.y - f.y));//旋轉角度
canvas.rotate(rotateDegrees, c.x, c.y);//以c為中心點旋轉
gradientDrawable.draw(canvas);
}
/**
* 繪製C區域內容
* @param canvas
* @param pathA
*/
private void drawPathCContent(Canvas canvas, Path pathA){
canvas.save();
canvas.clipPath(pathA);
canvas.clipPath(getPathC(), Region.Op.REVERSE_DIFFERENCE);//裁剪出C區域不同於A區域的部分
canvas.drawPath(getPathC(),pathCPaint);//繪製背景色
float eh = (float) Math.hypot(f.x - e.x,h.y - f.y);
float sin0 = (f.x - e.x) / eh;
float cos0 = (h.y - f.y) / eh;
//設定翻轉和旋轉矩陣
mMatrixArray[0] = -(1-2 * sin0 * sin0);
mMatrixArray[1] = 2 * sin0 * cos0;
mMatrixArray[3] = 2 * sin0 * cos0;
mMatrixArray[4] = 1 - 2 * sin0 * sin0;
mMatrix.reset();
mMatrix.setValues(mMatrixArray);//翻轉和旋轉
mMatrix.preTranslate(-e.x, -e.y);//沿當前XY軸負方向位移得到 矩形A₃B₃C₃D₃
mMatrix.postTranslate(e.x, e.y);//沿原XY軸方向位移得到 矩形A4 B4 C4 D4
canvas.drawBitmap(pathCContentBitmap, mMatrix, null);
drawPathCShadow(canvas);
canvas.restore();
}
/**
* 繪製C區域陰影,陰影左淺右深
* @param canvas
*/
private void drawPathCShadow(Canvas canvas){
int deepOffset = 1;//深色端的偏移值
int lightOffset = -30;//淺色端的偏移值
float viewDiagonalLength = (float) Math.hypot(viewWidth, viewHeight);//view對角線長度
int midpoint_ce = (int) (c.x + e.x) / 2;//ce中點
int midpoint_jh = (int) (j.y + h.y) / 2;//jh中點
float minDisToControlPoint = Math.min(Math.abs(midpoint_ce - e.x), Math.abs(midpoint_jh - h.y));//中點到控制點的最小值
int left;
int right;
int top = (int) c.y;
int bottom = (int) (viewDiagonalLength + c.y);
GradientDrawable gradientDrawable;
if (style.equals(STYLE_TOP_RIGHT)) {
gradientDrawable = drawableCTopRight;
left = (int) (c.x - lightOffset);
right = (int) (c.x + minDisToControlPoint + deepOffset);
} else {
gradientDrawable = drawableCLowerRight;
left = (int) (c.x - minDisToControlPoint - deepOffset);
right = (int) (c.x + lightOffset);
}
gradientDrawable.setBounds(left,top,right,bottom);
float mDegrees = (float) Math.toDegrees(Math.atan2(e.x- f.x, h.y - f.y));
canvas.rotate(mDegrees, c.x, c.y);
gradientDrawable.draw(canvas);
}
}
複製程式碼
修改後重新測試,卡頓的問題得到了明顯改善(開了手機錄屏對效能會有點影響),如圖
當前頁背面(C區域)背景繪製優化
相關博文連結
Android 常用的效能分析工具詳解:GPU呈現模式, TraceView, Systrace, HirearchyViewer
經過優化重用Bitmap後,測試中又發現了新的問題,當觸控點向左下角方向移動到一定距離時,會發現卡頓現象越來越明顯,超過一定的臨界值後,卡頓現象又突然消失了,如下圖所示
通過一番除錯後,發現是這句程式碼導致的繪製卡頓(紅框處)
那麼為什麼我們繪製那麼多陰影沒有問題,偏偏是這個drawPath導致了卡頓呢?我們定格繪製卡頓的時刻,觀察Gpu呈現模式分析的條形圖
可以發現深綠色和紅色線條特別長,其中深綠色線條表示主執行緒(Main Thread)執行任務的時間,過長意味著主執行緒執行了太多的任務,導致UI渲染跟不上vSync的訊號而出現掉幀的情況;紅色線條則表示Android進行2D渲染顯示列表(Display List)的時間。利用Systrace測試工具觀察具體的繪製時間分佈,找到繪製卡頓中的一幀
可以發現Choreographer.doFrame耗時過長,我們利用TraceView測試工具分析Choreographer.doFrame,一層層向下尋找耗時過長的子方法,最後定位到了updateRootDisplayList和nSyncAndDrawFrame方法,如圖所示
這兩個方法的作用是什麼呢?我們要從Android繪製View的過程說起,通過網上查閱的資料(相關資料博文已貼出),簡單總結一下:在使用GPU進行繪製前,需要對繪製的內容進行渲染,即需要渲染Display List。Display List包含了Android應用程式視窗所有的繪製命令,只要對Display List進行了渲染,就可以得到整個Android應用程式視窗的UI,而Android應用程式視窗的UI渲染分為兩步
- 第一步是由應用程式程式的Main Thread構建Display List,即updateRootDisplayList方法,對應Gpu呈現模式分析的深綠色線條,其中軟體渲染的子檢視需要先繪製在一個Bitmap上,然後這個Bitmap再記錄在父檢視的Display List中,繪製的檢視內容越多,構建Display List的耗時越長
- 第二步由應用程式程式的Render Thread渲染Display List,即nSyncAndDrawFrame方法,對應Gpu呈現模式分析的紅色線條,其中執行渲染需要得到Main Thread的通知,此通知在Main Thread與Render Thread資訊同步完畢後發出。資訊同步過程中,Display List引用到的Bitmap會封裝成Open GL紋理上傳至GPU。當全部Open GL紋理上傳完畢,說明引用到的Bitmap全部同步完成。同樣,繪製的檢視內容越多,則引用到的Bitmap越大,進而導致上傳耗時增加,Render Thread執行渲染等待通知的時間也就相應變長
分析完繪製過程後,回到Systrace工具的測試圖,可以看到Open GL紋理上傳耗時過長,繪製的Path太大了,如圖紅框區域
我們之前通過除錯程式碼知道“罪魁禍首”是canvas.drawPath(getPathC(),pathCPaint)這句程式碼,說明是PathC太大了。我們知道PathC是由i、d、b、a、k五個點連線而成,將觸控點移動到繪製卡頓的區域,發現i的Y座標遠小於0,證明此時PathC的面積非常大,我們的結論是正確的,如圖
那為什麼又會出現觸控點移動到某個臨界值後卡頓現象突然消失的現象呢?查閱資料後知道,Open GL紋理是有大小限制的,如果超出這個限制,那麼就會導至某些Bitmap不能作為Open GL紋理上傳到GPU,利用Systrace工具找到此臨界點,測試結果見下圖
可以發現卡頓突然消失的原因確實是Open GL紋理太大導致不能上傳至GPU,所以少了這個上傳的過程繪製速度也就變快了。既然知道了原因,那就動手改程式碼吧,改動非常簡單,只需要在繪製A、B、C區域之前為canvas繪製背景色即可,修改BookPageView
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.YELLOW);//繪製和C區域顏色相同的背景色
if(a.x==-1 && a.y==-1){
drawPathAContent(canvas,getPathDefault(),pathAPaint);
}else {
if(f.x==viewWidth && f.y==0){
drawPathAContent(canvas,getPathAFromTopRight(),pathAPaint);
drawPathCContent(canvas,getPathAFromTopRight(),pathCContentPaint);
drawPathBContent(canvas,getPathAFromTopRight(),pathBPaint);
}else if(f.x==viewWidth && f.y==viewHeight){
beginTrace("drawPathA");
drawPathAContent(canvas,getPathAFromLowerRight(),pathAPaint);
endTrace();
beginTrace("drawPathC");
drawPathCContent(canvas,getPathAFromLowerRight(),pathCContentPaint);
endTrace();
beginTrace("drawPathB");
drawPathBContent(canvas,getPathAFromLowerRight(),pathBPaint);
endTrace();
}
}
/**
* 繪製C區域內容
* @param canvas
* @param pathA
* @param pathPaint
*/
private void drawPathCContent(Canvas canvas, Path pathA){
canvas.save();
canvas.clipPath(pathA);
canvas.clipPath(getPathC(), Region.Op.REVERSE_DIFFERENCE);//裁剪出C區域不同於A區域的部分
// canvas.drawPath(getPathC(),pathCPaint);//幹掉這個(* ̄︿ ̄)
float eh = (float) Math.hypot(f.x - e.x,h.y - f.y);
float sin0 = (f.x - e.x) / eh;
float cos0 = (h.y - f.y) / eh;
//設定翻轉和旋轉矩陣
mMatrixArray[0] = -(1-2 * sin0 * sin0);
mMatrixArray[1] = 2 * sin0 * cos0;
mMatrixArray[3] = 2 * sin0 * cos0;
mMatrixArray[4] = 1 - 2 * sin0 * sin0;
mMatrix.reset();
mMatrix.setValues(mMatrixArray);//翻轉和旋轉
mMatrix.preTranslate(-e.x, -e.y);//沿當前XY軸負方向位移得到 矩形A₃B₃C₃D₃
mMatrix.postTranslate(e.x, e.y);//沿原XY軸方向位移得到 矩形A4 B4 C4 D4
canvas.drawBitmap(pathCContentBitmap, mMatrix, null);
drawPathCShadow(canvas);
canvas.restore();
}
複製程式碼
效果如圖
至此本篇教程到此結束,書籍翻頁效果的實現也暫時告一段落啦。如果大家看了感覺還不錯麻煩點個贊,你們的支援是我最大的動力~