Copyright notice: This article is the blogger’s original article, shall not be reproduced without the permission of the blogger

Android Development from Scratch series

Source: AnliaLee/BookPage, welcome star

If you see any mistakes or have any good suggestions, please leave a comment

preface: I talked about it earlierSimulation book page turning effect, the effect is shown as follows:

We from the principle analysis, function implementation to performance optimization of a complete over, the response is good, so a friend private letter asked me toOverlay page turning effectAlso said, so the protagonist of this period is it ~

This article only focuses on the ideas and implementation steps, some knowledge principles used in it will not be very detailed. If there are unclear APIS or methods, you can search the corresponding information on the Internet, there must be a god to explain very clearly, I will not present the ugly. In the spirit of serious and responsible, I will post the relevant knowledge of the blog links (in fact, is lazy do not want to write so much ha ha), you can send their own. For the benefit of those who are reading this series of blogs for the first time, this post may contain some content that has been covered in the previous series of blogs

International convention, go up effect drawing first


Create a page content factory class

To fill a View is to draw all the page elements onto a bitmap, and then draw the bitmap into the View. We encapsulate the process of drawing page content bitmap, which is convenient for users to call, create PageFactory abstract class, and realize the abstract method of drawing page content internally

public abstract class PageFactory {
    public boolean hasData = false;// Whether there is data
    public int pageTotal = 0;// Total number of pages

    public PageFactory(a){}

    /** * Draw the previous bitmap *@param bitmap
     * @param pageNum
     */
    public abstract void drawPreviousBitmap(Bitmap bitmap, int pageNum);

    /** * Draw the current page bitmap *@param bitmap
     * @param pageNum
     */
    public abstract void drawCurrentBitmap(Bitmap bitmap, int pageNum);

    /** * Draw the next bitmap *@param bitmap
     * @param pageNum
     */
    public abstract void drawNextBitmap(Bitmap bitmap, int pageNum);

    /** * get the corresponding contents in the collection by index *@param index
     * @return* /
    public abstract Bitmap getBitmapByIndex(int index);
}
Copy the code

We take pure image content drawing as an example, create PicturesPageFactory inherit PageFactory, in addition to achieve the specific logic of content drawing, set up a variety of initialization methods, convenient for users to use different paths under the image collection

public class PicturesPageFactory extends PageFactory {
    private Context context;
    
    public int style;// Set type
    public final static int STYLE_IDS = 1;// Drawable directory image collection type
    public final static int STYLE_URIS = 2;// Phone local directory image collection type

    private int[] picturesIds;
    /** * initialize the set of image ids * in the drawable directory@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;
    /** * Initializes the set of image URIs * in the local directory@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; }}/** * Get bitmap * from id collection@param index
     * @return* /
    private Bitmap getBitmapFromIds(int index){
        return BitmapUtils.drawableToBitmap(
                context.getResources().getDrawable(picturesIds[index]),
                ScreenUtils.getScreenWidth(context),
                ScreenUtils.getScreenHeight(context)
        );
    }

    /** * Get bitmap * from uri collection@param index
     * @return* /
    private Bitmap getBitmapFromUris(int index){
        return null;// You can write this in your spare time}}Copy the code

The basic structure of BitmapUtils and ScreenUtils is like this. As for the novel text class, it is more complicated to parse. Maybe we will write an extra chapter on this. So let’s start with how do we use this factory class in our custom View


Use the factory class to get the page content and draw it

Create a CoverPageView that provides an external interface to set up the factory class

public class CoverPageView extends View {
    private int defaultWidth;// Default width
    private int defaultHeight;// Default height
    private int viewWidth;
    private int viewHeight;
    private int pageNum;// The current number of pages

    private PageFactory pageFactory;

    private Bitmap currentPage;// Current page 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;
    }

    /** * Sets the factory class *@param factory
     */
    public void setPageFactory(final PageFactory factory){
        // Ensure that the View has completed the measurement and the page bitmap is initialized
        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw(a) {
                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); }}/** * Draws the current page *@param canvas
     */
    private void drawCurrentPage(Canvas canvas){
        canvas.drawBitmap(currentPage, 0.0.null); }}Copy the code

To initialize the Activity, I used images from the Drawable directory as page content

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));
Copy the code
<?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>
Copy the code

CoverPageView sets the factory object and draws the current page, as shown in the figure below


Realize the page sliding effect

The principle of the page sliding effect is actually very simple, before we called the canvas.drawBitmap method to draw the current page content into the View, to achieve the page sliding, only need to set the left value of the drawBitmap method (the left boundary value of the bitmap). In other words, we can calculate the left value by recording the sliding distance of the finger on the X-axis, so as to change the starting position of the bitmap of the current page content to achieve the sliding effect, as shown in the figure

Modify CoverPageView to listen for touch events

public class CoverPageView extends View {
	// omit some code...
    private float xDown;// Record the x coordinates of the initial touch
    private float scrollPageLeft;// Slide the left edge of the page
	
    private MyPoint touchPoint;/ / touch point
    private Bitmap nextPage;// Next page bitmap

    private int touchStyle;// Touch type
    public static final int TOUCH_MIDDLE = 0;// Click the middle area
    public static final int TOUCH_LEFT = 1;// Click the left area
    public static final int TOUCH_RIGHT = 2;// Click the right area

    private void init(Context context){
        // omit some code...
        scrollPageLeft = 0;
        touchStyle = TOUCH_RIGHT;
        touchPoint = new MyPoint(-1, -1);
    }

    /** * Sets the factory class *@param factory
     */
    public void setPageFactory(final PageFactory factory){remember to use pageFactory. DrawNextBitmap (nextPage, pageNum) draw the content of the page, or sliding occurs the current page background blank no content}@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); }}}/** * Draws the current page *@param canvas
     */
    private void drawCurrentPage(Canvas canvas){
        canvas.drawBitmap(currentPage, scrollPageLeft, 0.null);// Change the left value
    }

    /** * draw the next page *@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) {/ / left
                    touchStyle = TOUCH_LEFT;
                }else if(x>viewWidth*2/3) {/ / right
                    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;
    }

    /** * Calculates the position of the left edge of the slide page to achieve the effect of sliding the current page *@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(); }}Copy the code

The effect is shown in figure


Realize page up and down

Related blog links

  • The use of Android Scroller class
  • Android learning Scroller introduction and use
  • Android Scroller fully resolved, everything you need to know about Scroller
  • Android — Interpolator
  • Interpolator class for Android animation

To realize the page-turning effect, we need two aspects. One is to use knowledge of Scroller and Interpolator to realize the page-turning effect automatically. The second is to update the content of the previous page, the current page and the next page at the appropriate time, so that the whole page turning connection is more fluent

Said the first thing first, automatically turn to page on the page and on the difference between the sliding direction is different, with our slide page right boundary (because left boundary in the View of the outside, so choose the right boundary for reference, facilitate everybody understand) the location of the change, for example, the content of the page on the back right boundary sliding from left to right, gradually to cover the current page content, When turning to the next page, the right boundary of the current page slides from right to left to gradually display the next page. The specific calculation method is as follows

/** * Automatically completes the next page */
private void autoScrollToNextPage(a){
	pageState = PAGE_NEXT;

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

	int time =(int) ((1+scrollPageLeft/viewWidth) * scrollTime);// Calculate the actual animation time according to the proportion of the sliding distance
	mScroller.startScroll((int) (viewWidth+scrollPageLeft), (int) touchPoint.y, dx, dy, time);
}

/** * Automatically completes returns to previous page */
private void autoScrollToPreviousPage(a){
	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);
}
Copy the code

The second point is about the timing of updating page content. Mentioned above we update the page content you need to call pageFactory. DrawXxxBitmap method to draw the page content, the content of the data is too large, drawing speed will slow, if in the View within the ontouch method to perform this operation, will cause caton. Therefore, we need to draw the content bitmap before onDraw. When the View is redrawn depends on the touch, so you should start updating when you listen for ACTION_DOWN. For example, if the current page number is 2, and the operation is to turn to the next page, the content of the second page will be drawn to the previousPage (previousPage) when the finger drops to the right area (touchStyle == TOUCH_RIGHT). The contents of page 3 are drawn to currentPage (the currentPage) as follows

pageNum++;
pageFactory.drawPreviousBitmap(previousPage,pageNum);
pageFactory.drawCurrentBitmap(currentPage,pageNum);
pageNum--;
Copy the code

Finally, the View computeScroll() method determines the position of the slide page. If the slide page reaches the specified position (leaving the View), the operation of increasing the number of pages is performed. The specific code is as follows (if the text analysis does not understand clearly, you can refer to the code step by step)

public class CoverPageView extends View {
	// omit some code...
    private int scrollTime;// Slide the animation time
    private Scroller mScroller;

    private int pageState;// Page-turning state, used to limit the touch operation before the page-turning animation ends
    public static final int PAGE_STAY = 0;// At rest
    public static final int PAGE_NEXT = 1;Turn to the next page
    public static final int PAGE_PREVIOUS = 2;// Turn to the previous page

    private void init(Context context){
		// omit some code...
        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); }}}}/** * draw the previous page *@param canvas
     */
    private void drawPreviousPage(Canvas canvas){
        canvas.drawBitmap(previousPage, scrollPageLeft, 0.null);
    }

    /** * Draws the current page *@param canvas
     */
    private void drawCurrentPage(Canvas canvas){
		// Note that the contents of the slide page are different when you scroll up and down the page
        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) {/ / left
                        touchStyle = TOUCH_LEFT;
                        if(pageNum>1){ pageNum--; pageFactory.drawCurrentBitmap(currentPage,pageNum); pageFactory.drawNextBitmap(nextPage,pageNum); pageNum++; }}else if(x>viewWidth*2/3) {/ / right
                        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(a) {
        if (mScroller.computeScrollOffset()) {
            float x = mScroller.getCurrX();
            float y = mScroller.getCurrY();
            scrollPageLeft = 0 - (viewWidth - x);

            if (mScroller.getFinalX() == x && mScroller.getFinalY() == y){// Slide the page to the specified position
                if(touchStyle == TOUCH_RIGHT){
                    pageNum++;
                }else if(touchStyle == TOUCH_LEFT){ pageNum--; } resetView(); } postInvalidate(); }}/** * Calculates the position of the left edge of the slide page to achieve the effect of sliding the current page *@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();
    }

    /** * automatically complete the sliding operation */
    private void autoScroll(a){
        switch (touchStyle){
            case TOUCH_LEFT:
                if(pageNum>1){
                    autoScrollToPreviousPage();
                }
                break;
            case TOUCH_RIGHT:
                if(pageNum<pageFactory.pageTotal){
                    autoScrollToNextPage();
                }
                break; }}/** * Automatically completes the next page */
    private void autoScrollToNextPage(a){
        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);
    }

    /** * Automatically completes returns to previous page */
    private void autoScrollToPreviousPage(a){
        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);
    }

    /** * reset operation */
    private void resetView(a){
        scrollPageLeft = 0;
        touchPoint.x = -1;
        touchPoint.y = -1; }}Copy the code

The effect is shown in figure


Draw page shadows

In Android Custom View, we’ve covered shading on a page in detail, using GradientDrawable. The shadow drawing here is much simpler than the simulation of page turning, we do not need to consider how to intercept and rotate the shadow area, just draw to the right edge of the slide page, the code is as follows

public class CoverPageView extends View {
	// omit some code...
    private GradientDrawable shadowDrawable;

    private void init(Context context){
		// omit some code...
        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); }}}}/** * Draw shadow *@param canvas
     */
    private void drawShadow(Canvas canvas){
        int left = (int)(viewWidth + scrollPageLeft);
        shadowDrawable.setBounds(left, 0, left + 30, viewHeight); shadowDrawable.draw(canvas); }}Copy the code

The effect is shown in figure

This is the end of this tutorial. If you enjoy it, please give me a thumbs up. Your support is my biggest motivation