版權聲明:本文爲博主原創文章,未經博主容許不得轉載html
系列教程:Android開發之從零開始系列java
源碼:AnliaLee/BookPage,歡迎starandroid
你們要是看到有錯誤的地方或者有啥好的建議,歡迎留言評論git
前言:前幾期博客中咱們分析了 書籍翻頁效果各部分的繪製原理,雖然效果都實現了,但測試過程當中卻發現咱們的View翻起頁來彷佛 不是很流暢,這期便帶你們一塊兒對View進行 性能優化github
本篇只着重於思路和實現步驟,裏面用到的一些知識原理不會很是細地拿來說,若是有不清楚的api或方法能夠在網上搜下相應的資料,確定有大神講得很是清楚的,我這就不獻醜了。本着認真負責的精神我會把相關知識的博文連接也貼出來(其實就是懶不想寫那麼多哈哈),你們能夠自行傳送。爲了照顧第一次閱讀系列博客的小夥伴,本篇會出現一些在以前系列博客就講過的內容,看過的童鞋自行跳過該段便可canvas
國際慣例,先上效果圖api
在進行性能優化以前,首先感謝@布隆提出的建議: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"屬性性能優化
相關博文連接
以前完成全部效果的繪製後,在手機上測試了下,發現翻頁不是很流暢,感受卡卡的,遂打開手機的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);
}
}
複製代碼
修改後從新測試,卡頓的問題獲得了明顯改善(開了手機錄屏對性能會有點影響),如圖
相關博文連接
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();
}
複製代碼
效果如圖
至此本篇教程到此結束,書籍翻頁效果的實現也暫時告一段落啦。若是你們看了感受還不錯麻煩點個贊,大家的支持是我最大的動力~