Android自定義View——從零開始實現覆蓋翻頁效果

Anlia發表於2017-12-14

版權宣告:本文為博主原創文章,未經博主允許不得轉載

系列教程:Android開發之從零開始系列

原始碼:AnliaLee/BookPage,歡迎star

大家要是看到有錯誤的地方或者有啥好的建議,歡迎留言評論

前言:之前講了模擬書籍翻頁效果,效果如圖

Android自定義View——從零開始實現覆蓋翻頁效果

我們從原理分析、功能實現到效能優化完整地過了一遍,反響不錯,於是有小夥伴私信讓我把 覆蓋翻頁效果也講了,所以這期的主角就是它了 ~

本篇只著重於思路和實現步驟,裡面用到的一些知識原理不會非常細地拿來講,如果有不清楚的api或方法可以在網上搜下相應的資料,肯定有大神講得非常清楚的,我這就不獻醜了。本著認真負責的精神我會把相關知識的博文連結也貼出來(其實就是懶不想寫那麼多哈哈),大家可以自行傳送。為了照顧第一次閱讀系列部落格的小夥伴,本篇可能會出現一些在之前系列部落格就講過的內容,看過的童鞋自行跳過該段即可

國際慣例,先上效果圖

Android自定義View——從零開始實現覆蓋翻頁效果


建立頁面內容工廠類

Android自定義View——從零開始實現書籍翻頁效果(三)一文中提到了向View填充內容實際上就是將所有頁面元素繪製到一個bitmap上,然後再將這個bitmap繪製到View中。我們把繪製頁面內容bitmap的過程封裝起來,方便使用者呼叫,建立PageFactory抽象類,在內部實現繪製頁面內容的抽象方法

public abstract class PageFactory {
    public boolean hasData = false;//是否含有資料
    public int pageTotal = 0;//頁面總數

    public PageFactory(){}

    /**
     * 繪製上一頁bitmap
     * @param bitmap
     * @param pageNum
     */
    public abstract void drawPreviousBitmap(Bitmap bitmap, int pageNum);

    /**
     * 繪製當前頁bitmap
     * @param bitmap
     * @param pageNum
     */
    public abstract void drawCurrentBitmap(Bitmap bitmap, int pageNum);

    /**
     * 繪製下一頁bitmap
     * @param bitmap
     * @param pageNum
     */
    public abstract void drawNextBitmap(Bitmap bitmap, int pageNum);

    /**
     * 通過索引在集合中獲取相應內容
     * @param index
     * @return
     */
    public abstract Bitmap getBitmapByIndex(int index);
}
複製程式碼

我們以純影象內容的繪製為例,建立PicturesPageFactory繼承PageFactory,除了實現內容繪製的具體邏輯以外,設定多種初始化方法,方便使用者使用不同路徑下的影象集合

public class PicturesPageFactory extends PageFactory {
    private Context context;
    
    public int style;//集合型別
    public final static int STYLE_IDS = 1;//drawable目錄圖片集合型別
    public final static int STYLE_URIS = 2;//手機本地目錄圖片集合型別

    private int[] picturesIds;
    /**
     * 初始化drawable目錄下的圖片id集合
     * @param context
     * @param pictureIds
     */
    public PicturesPageFactory(Context context, int[] pictureIds){
        this.context = context;
        this.picturesIds = pictureIds;
        this.style = STYLE_IDS;
        if (pictureIds.length > 0){
            hasData = true;
            pageTotal = pictureIds.length;
        }
    }

    private String[] picturesUris;
    /**
     * 初始化本地目錄下的圖片uri集合
     * @param context
     * @param picturesUris
     */
    public PicturesPageFactory(Context context, String[] picturesUris){
        this.context = context;
        this.picturesUris = picturesUris;
        this.style = STYLE_URIS;
        if (picturesUris.length > 0){
            hasData = true;
            pageTotal = picturesUris.length;
        }
    }

    @Override
    public void drawPreviousBitmap(Bitmap bitmap, int pageNum) {
        Canvas canvas = new Canvas(bitmap);
        canvas.drawBitmap(getBitmapByIndex(pageNum-2),0,0,null);
    }

    @Override
    public void drawCurrentBitmap(Bitmap bitmap, int pageNum) {
        Canvas canvas = new Canvas(bitmap);
        canvas.drawBitmap(getBitmapByIndex(pageNum-1),0,0,null);
    }

    @Override
    public void drawNextBitmap(Bitmap bitmap, int pageNum) {
        Canvas canvas = new Canvas(bitmap);
        canvas.drawBitmap(getBitmapByIndex(pageNum),0,0,null);
    }

    @Override
    public Bitmap getBitmapByIndex(int index) {
        if(hasData){
            switch (style){
                case STYLE_IDS:
                    return getBitmapFromIds(index);
                case STYLE_URIS:
                    return getBitmapFromUris(index);
                default:
                    return null;
            }
        }else {
            return null;
        }
    }

    /**
     * 從id集合獲取bitmap
     * @param index
     * @return
     */
    private Bitmap getBitmapFromIds(int index){
        return BitmapUtils.drawableToBitmap(
                context.getResources().getDrawable(picturesIds[index]),
                ScreenUtils.getScreenWidth(context),
                ScreenUtils.getScreenHeight(context)
        );
    }

    /**
     * 從uri集合獲取bitmap
     * @param index
     * @return
     */
    private Bitmap getBitmapFromUris(int index){
        return null;//這個有空再寫啦,大家可自行補充完整
    }
}
複製程式碼

基本架構就是這樣(BitmapUtilsScreenUtils兩個工具類大家自己去看下原始碼吧,就不在這展開說了~),至於小說文字類的解析比較複雜,以後可能會出一個番外篇專門講這個。下面我們開始介紹如何在自定義View中使用這個工廠類


使用工廠類獲取頁面內容並繪製

建立CoverPageView,提供一個對外的介面用以設定工廠類

public class CoverPageView extends View {
    private int defaultWidth;//預設寬度
    private int defaultHeight;//預設高度
    private int viewWidth;
    private int viewHeight;
    private int pageNum;//當前頁數

    private PageFactory pageFactory;

    private Bitmap currentPage;//當前頁bitmap

    public CoverPageView(Context context) {
        super(context);
        init(context);
    }

    public CoverPageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    private void init(Context context){
        defaultWidth = 600;
        defaultHeight = 1000;
        pageNum = 1;
    }

    /**
     * 設定工廠類
     * @param factory
     */
    public void setPageFactory(final PageFactory factory){
        //保證View已經完成了測量工作,各頁bitmap已初始化
        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                getViewTreeObserver().removeOnPreDrawListener(this);
                if(factory.hasData){
                    pageFactory = factory;
                    pageFactory.drawCurrentBitmap(currentPage,pageNum);
                    postInvalidate();
                }
                return true;
            }
        });
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int height = ViewUtils.measureSize(defaultHeight, heightMeasureSpec);
        int width = ViewUtils.measureSize(defaultWidth, widthMeasureSpec);
        setMeasuredDimension(width, height);

        viewWidth = width;
        viewHeight = height;

        currentPage = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.RGB_565);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(pageFactory !=null){
            drawCurrentPage(canvas);
        }
    }

    /**
     * 繪製當前頁
     * @param canvas
     */
    private void drawCurrentPage(Canvas canvas){
        canvas.drawBitmap(currentPage, 0, 0,null);
    }
}
複製程式碼

Activity中進行初始化,這裡我用了drawable目錄下的一些圖片作為頁面內容

int[] pIds = new int[]{R.drawable.test1,R.drawable.test2,R.drawable.test3};
coverPageView = (CoverPageView) findViewById(R.id.view_cover_page);
coverPageView.setPageFactory(new PicturesPageFactory(this,pIds));
複製程式碼
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:splitMotionEvents="false">
    <com.anlia.pageturn.view.CoverPageView
        android:id="@+id/view_cover_page"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="10dp"/>
</RelativeLayout>
複製程式碼

CoverPageView設定了工廠類物件後便會繪製出當前頁內容,效果如圖

Android自定義View——從零開始實現覆蓋翻頁效果


實現頁面滑動效果

頁面滑動效果的原理其實很簡單,之前我們呼叫了canvas.drawBitmap方法將當前頁內容繪製到View中,要實現頁面滑動,只需要設定drawBitmap方法中的left值(bitmap的左邊界值)即可。也就是說,我們可以通過記錄手指在X軸上的滑動距離,計算出left值,從而改變當前頁內容bitmap的起始位置,實現滑動效果,如圖

Android自定義View——從零開始實現覆蓋翻頁效果

修改CoverPageView,監聽觸控事件

public class CoverPageView extends View {
	//省略部分程式碼...
    private float xDown;//記錄初始觸控的x座標
    private float scrollPageLeft;//滑動頁左邊界
	
    private MyPoint touchPoint;//觸控點
    private Bitmap nextPage;//下一頁bitmap

    private int touchStyle;//觸控型別
    public static final int TOUCH_MIDDLE = 0;//點選中間區域
    public static final int TOUCH_LEFT = 1;//點選左邊區域
    public static final int TOUCH_RIGHT = 2;//點選右邊區域

    private void init(Context context){
        //省略部分程式碼...
        scrollPageLeft = 0;
        touchStyle = TOUCH_RIGHT;
        touchPoint = new MyPoint(-1,-1);
    }

    /**
     * 設定工廠類
     * @param factory
     */
    public void setPageFactory(final PageFactory factory){
        記得使用pageFactory.drawNextBitmap(nextPage,pageNum)繪製下一頁的內容,不然滑動當前頁時會出現背景空白沒有內容
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(pageFactory !=null){
            if(touchPoint.x ==-1 && touchPoint.y ==-1){
                drawCurrentPage(canvas);
            }else{
                drawNextPage(canvas);
                drawCurrentPage(canvas);
            }
        }
    }

    /**
     * 繪製當前頁
     * @param canvas
     */
    private void drawCurrentPage(Canvas canvas){
        canvas.drawBitmap(currentPage, scrollPageLeft, 0,null);//修改left值
    }

    /**
     * 繪製下一頁
     * @param canvas
     */
    private void drawNextPage(Canvas canvas){
        canvas.drawBitmap(nextPage, 0, 0, null);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                xDown = x;
                if(x<=viewWidth/3){//左
                    touchStyle = TOUCH_LEFT;
                }else if(x>viewWidth*2/3){//右
                    touchStyle = TOUCH_RIGHT;
                }else if(x>viewWidth/3 && x<viewWidth*2/3){//中
                    touchStyle = TOUCH_MIDDLE;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                scrollPage(x,y);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

    /**
     * 計算滑動頁面左邊界位置,實現滑動當前頁效果
     * @param x
     * @param y
     */
    private void scrollPage(float x, float y){
        touchPoint.x = x;
        touchPoint.y = y;

        if(touchStyle == TOUCH_RIGHT){
            scrollPageLeft = touchPoint.x - xDown;
        }else if(touchStyle == TOUCH_LEFT){
            scrollPageLeft =touchPoint.x - xDown - viewWidth;
        }

        if(scrollPageLeft > 0){
            scrollPageLeft = 0;
        }
        postInvalidate();
    }
}
複製程式碼

效果如圖

Android自定義View——從零開始實現覆蓋翻頁效果


實現上下翻頁

相關博文連結

要實現上下翻頁效果我們需從兩個方面入手,一是使用scrollerInterpolator插值器方面的知識完成自動翻頁的效果;二是在恰當的時機更新上頁、當前頁、下頁的內容,使得整個翻頁銜接更為流暢

先說第一點,自動翻到上頁和下頁區別在於頁面滑動的方向不同,我們以滑動頁的右邊界(因為左邊界View的範圍之外,所以選取右邊界作為參考,方便大家理解)的位置變化為例,翻到上頁時上一頁的內容右邊界從左向右滑動,逐漸覆蓋當前頁內容,而翻到下頁時,則是當前頁內容右邊界從右向左滑動,逐漸顯示出下頁內容,具體計算的方法如下

/**
 * 自動完成翻到下一頁操作
 */
private void autoScrollToNextPage(){
	pageState = PAGE_NEXT;

	int dx,dy;
	dx = (int) -(viewWidth+scrollPageLeft);
	dy = (int) (touchPoint.y);

	int time =(int) ((1+scrollPageLeft/viewWidth) * scrollTime);//按已滑動的距離佔比計算實際的動畫時間
	mScroller.startScroll((int) (viewWidth+scrollPageLeft), (int) touchPoint.y, dx, dy, time);
}

/**
 * 自動完成返回上一頁操作
 */
private void autoScrollToPreviousPage(){
	pageState = PAGE_PREVIOUS;

	int dx,dy;
	dx = (int) -scrollPageLeft;
	dy = (int) (touchPoint.y);

	int time =(int) (-scrollPageLeft/viewWidth * scrollTime);
	mScroller.startScroll((int) (viewWidth+scrollPageLeft), (int) touchPoint.y, dx, dy, time);
}
複製程式碼

第二點,關於更新頁面內容的時機。前文我們提到更新頁面內容需要呼叫pageFactory.drawXxxBitmap方法重新繪製頁面內容,內容資料太大時,繪製速度就會變慢,如果在ViewonDraw方法內執行此操作,就會造成卡頓。因此,我們需要在onDraw之前繪製好內容bitmap。View何時重繪和觸控操作有關,所以在監聽到ACTION_DOWN時就應該要開始更新內容了。舉個例子,如果當前頁數為2,執行翻到下頁的操作,既然要提前更新頁面內容,那麼當手指落下的區域為右區域(touchStyle == TOUCH_RIGHT)時,第2頁的內容就要繪製到previousPage(上頁)中,第3頁的內容繪製到currentPage(當前頁)中,具體程式碼實現如下

pageNum++;
pageFactory.drawPreviousBitmap(previousPage,pageNum);
pageFactory.drawCurrentBitmap(currentPage,pageNum);
pageNum--;
複製程式碼

最後在ViewcomputeScroll()方法中判斷滑動頁的位置,如果滑動頁到了指定的位置(離開View),執行頁數增加的操作。具體程式碼如下(文字分析理解不清楚的可以對照著程式碼一步步看)

public class CoverPageView extends View {
	//省略部分程式碼...
    private int scrollTime;//滑動動畫時間
    private Scroller mScroller;

    private int pageState;//翻頁狀態,用於限制翻頁動畫結束前的觸控操作
    public static final int PAGE_STAY = 0;//處於靜止狀態
    public static final int PAGE_NEXT = 1;//翻至下一頁
    public static final int PAGE_PREVIOUS = 2;//翻至上一頁

    private void init(Context context){
		//省略部分程式碼...
        pageState = PAGE_STAY;
        mScroller = new Scroller(context,new LinearInterpolator());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(pageFactory !=null){
            if(touchPoint.x ==-1 && touchPoint.y ==-1){
                drawCurrentPage(canvas);
                pageState = PAGE_STAY;
            }else{
                if(touchStyle == TOUCH_RIGHT){
                    drawCurrentPage(canvas);
                    drawPreviousPage(canvas);
                }else {
                    drawNextPage(canvas);
                    drawCurrentPage(canvas);
                }
            }
        }
    }

    /**
     * 繪製上一頁
     * @param canvas
     */
    private void drawPreviousPage(Canvas canvas){
        canvas.drawBitmap(previousPage, scrollPageLeft, 0,null);
    }

    /**
     * 繪製當前頁
     * @param canvas
     */
    private void drawCurrentPage(Canvas canvas){
		//注意上下翻頁時的滑動頁的內容不一樣
        if(touchStyle == TOUCH_RIGHT){
            canvas.drawBitmap(currentPage, 0, 0,null);
        }else if(touchStyle == TOUCH_LEFT){
            canvas.drawBitmap(currentPage, scrollPageLeft, 0,null);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        float x = event.getX();
        float y = event.getY();
        if(pageState == PAGE_STAY){
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    xDown = x;
                    if(x<=viewWidth/3){//左
                        touchStyle = TOUCH_LEFT;
                        if(pageNum>1){
                            pageNum--;
                            pageFactory.drawCurrentBitmap(currentPage,pageNum);
                            pageFactory.drawNextBitmap(nextPage,pageNum);
                            pageNum++;
                        }
                    }else if(x>viewWidth*2/3){//右
                        touchStyle = TOUCH_RIGHT;
                        if(pageNum<pageFactory.pageTotal){
                            pageNum++;
                            pageFactory.drawPreviousBitmap(previousPage,pageNum);
                            pageFactory.drawCurrentBitmap(currentPage,pageNum);
                            pageNum--;
                        }

                    }else if(x>viewWidth/3 && x<viewWidth*2/3){//中
                        touchStyle = TOUCH_MIDDLE;
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    if(touchStyle == TOUCH_LEFT){
                        if(pageNum>1){
                            scrollPage(x,y);
                        }
                    }else if(touchStyle == TOUCH_RIGHT){
                        if(pageNum<pageFactory.pageTotal){
                            scrollPage(x,y);
                        }
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    autoScroll();
                    break;
            }
        }
        return true;
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            float x = mScroller.getCurrX();
            float y = mScroller.getCurrY();
            scrollPageLeft = 0 - (viewWidth - x);

            if (mScroller.getFinalX() == x && mScroller.getFinalY() == y){//滑動頁到達指定位置
                if(touchStyle == TOUCH_RIGHT){
                    pageNum++;
                }else if(touchStyle == TOUCH_LEFT){
                    pageNum--;
                }
                resetView();
            }
            postInvalidate();
        }
    }

    /**
     * 計算滑動頁面左邊界位置,實現滑動當前頁效果
     * @param x
     * @param y
     */
    private void scrollPage(float x, float y){
        touchPoint.x = x;
        touchPoint.y = y;

        if(touchStyle == TOUCH_RIGHT){
            scrollPageLeft = touchPoint.x - xDown;
        }else if(touchStyle == TOUCH_LEFT){
            scrollPageLeft =touchPoint.x - xDown - viewWidth;
        }

        if(scrollPageLeft > 0){
            scrollPageLeft = 0;
        }
        postInvalidate();
    }

    /**
     * 自動完成滑動操作
     */
    private void autoScroll(){
        switch (touchStyle){
            case TOUCH_LEFT:
                if(pageNum>1){
                    autoScrollToPreviousPage();
                }
                break;
            case TOUCH_RIGHT:
                if(pageNum<pageFactory.pageTotal){
                    autoScrollToNextPage();
                }
                break;
        }
    }

    /**
     * 自動完成翻到下一頁操作
     */
    private void autoScrollToNextPage(){
        pageState = PAGE_NEXT;

        int dx,dy;
        dx = (int) -(viewWidth+scrollPageLeft);
        dy = (int) (touchPoint.y);

        int time =(int) ((1+scrollPageLeft/viewWidth) * scrollTime);
        mScroller.startScroll((int) (viewWidth+scrollPageLeft), (int) touchPoint.y, dx, dy, time);
    }

    /**
     * 自動完成返回上一頁操作
     */
    private void autoScrollToPreviousPage(){
        pageState = PAGE_PREVIOUS;

        int dx,dy;
        dx = (int) -scrollPageLeft;
        dy = (int) (touchPoint.y);

        int time =(int) (-scrollPageLeft/viewWidth * scrollTime);
        mScroller.startScroll((int) (viewWidth+scrollPageLeft), (int) touchPoint.y, dx, dy, time);
    }

    /**
     * 重置操作
     */
    private void resetView(){
        scrollPageLeft = 0;
        touchPoint.x = -1;
        touchPoint.y = -1;
    }
}
複製程式碼

效果如圖

Android自定義View——從零開始實現覆蓋翻頁效果


繪製頁面陰影

Android自定義View——從零開始實現書籍翻頁效果(四)一文中我們詳細介紹瞭如何繪製頁面的陰影,主要用到了GradientDrawable方面的知識。這裡的陰影繪製比模擬翻頁的要簡單許多,我們不需要考慮如何擷取和旋轉陰影區域,只需要繪製到滑動頁右邊界處就行,程式碼如下

public class CoverPageView extends View {
	//省略部分程式碼...
    private GradientDrawable shadowDrawable;

    private void init(Context context){
		//省略部分程式碼...
        int[] mBackShadowColors = new int[] { 0x66000000,0x00000000};
        shadowDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors);
        shadowDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(pageFactory !=null){
            if(touchPoint.x ==-1 && touchPoint.y ==-1){
                drawCurrentPage(canvas);
                pageState = PAGE_STAY;
            }else{
                if(touchStyle == TOUCH_RIGHT){
                    drawCurrentPage(canvas);
                    drawPreviousPage(canvas);
                    drawShadow(canvas);
                }else {
                    drawNextPage(canvas);
                    drawCurrentPage(canvas);
                    drawShadow(canvas);
                }
            }
        }
    }

    /**
     * 繪製陰影
     * @param canvas
     */
    private void drawShadow(Canvas canvas){
        int left = (int)(viewWidth + scrollPageLeft);
        shadowDrawable.setBounds(left, 0, left + 30 , viewHeight);
        shadowDrawable.draw(canvas);
    }
}
複製程式碼

效果如圖

Android自定義View——從零開始實現覆蓋翻頁效果

至此本篇教程到此結束,如果大家看了感覺還不錯麻煩點個贊,你們的支援是我最大的動力~


相關文章