美文网首页Android自定义控件Android自定义View安卓开发博客
Android自定义View——从零开始实现覆盖翻页效果

Android自定义View——从零开始实现覆盖翻页效果

作者: Anlia | 来源:发表于2017-12-13 17:17 被阅读360次

    版权声明:本文为博主原创文章,未经博主允许不得转载。
    系列教程:Android开发之从零开始系列
    源码:github.com/AnliaLee/BookPage,欢迎star

    大家要是看到有错误的地方或者有啥好的建议,欢迎留言评论

    前言:之前讲了仿真书籍翻页效果,效果如图

    我们从原理分析、功能实现到性能优化完整地过了一遍,反响不错,于是有小伙伴私信让我把 覆盖翻页效果也讲了,所以这期的主角就是它了 ~

    本篇只着重于思路和实现步骤,里面用到的一些知识原理不会非常细地拿来讲,如果有不清楚的api或方法可以在网上搜下相应的资料,肯定有大神讲得非常清楚的,我这就不献丑了。本着认真负责的精神我会把相关知识的博文链接也贴出来(其实就是懒不想写那么多哈哈),大家可以自行传送。为了照顾第一次阅读系列博客的小伙伴,本篇可能会出现一些在之前系列博客就讲过的内容,看过的童鞋自行跳过该段即可

    国际惯例,先上效果图

    目录
    • 创建页面内容工厂类
    • 使用工厂类获取页面内容并绘制
    • 实现页面滑动效果
    • 实现上下翻页
    • 绘制页面阴影

    创建页面内容工厂类

    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设置了工厂类对象后便会绘制出当前页内容,效果如图


    实现页面滑动效果

    页面滑动效果的原理其实很简单,之前我们调用了canvas.drawBitmap方法将当前页内容绘制到View中,要实现页面滑动,只需要设置drawBitmap方法中的left值(bitmap的左边界值)即可。也就是说,我们可以通过记录手指在X轴上的滑动距离,计算出left值,从而改变当前页内容bitmap的起始位置,实现滑动效果,如图

    修改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 scroller类的使用
    Android学习之 Scroller的介绍与使用
    Android Scroller完全解析,关于Scroller你所需知道的一切
    Android -- Interpolator
    android动画 之Interpolator类

    要实现上下翻页效果我们需从两个方面入手,一是使用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——从零开始实现书籍翻页效果(四)一文中我们详细介绍了如何绘制页面的阴影,主要用到了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——从零开始实现覆盖翻页效果

        本文链接:https://www.haomeiwen.com/subject/npxnixtx.html