自定义View实战篇(二)实现小说翻页三 实现翻页动画、阴影、内容

自定义View实战篇(二)实现小说翻页一 基本原理

本文实现翻页完成动画,页面阴影,内容绘制三个部分,且小说翻页目前更新至这个部分,短期内不会更新。

实现翻页完成动画

实现翻页完成动画,我们可以借助一个控件Scoller,他可以帮我们完成动画效果。

public class PageView extends View {
    private Scroller mScroller;
    //....
     public PageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //设置匀速滑动的Scroller
        mScroller = new Scroller(context, new LinearInterpolator());
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            //.....
            case MotionEvent.ACTION_UP:
                //当手指弹起时,Scroller开始滚动,即调用computeScroll()方法
                mScroller.startScroll((int) a.x, (int) a.y, -(width * 2), 0, 500);
                break;
            default:
                break;
        }
        return true;
    }
    
    /**
    * 重写此方法,然后不断更新a的位置自动完成动画
    */ 
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            int currX = mScroller.getCurrX();
            int currY = mScroller.getCurrY();
            a.x = currX;
            a.y = currY;
            calculationPoint(a, f);
            postInvalidate();
        }
    }
     
}
自定义View实战篇(二)实现小说翻页三 实现翻页动画、阴影、内容_第1张图片

绘制阴影

从上面的GIF图可以看到,我们翻起页面后翻起的部分周围有渐变的阴影,这部分需要借助GradientDrawable,用其绘制一个简便的矩,然后旋转即可得到

投影在B上面的阴影

从下图可以看出,我们以C为定点,创建一个矩形,然后围绕C点旋转这个矩形即可

自定义View实战篇(二)实现小说翻页三 实现翻页动画、阴影、内容_第2张图片

下面的代码比较简单,值的注意的是(float) Math.toDegrees(Math.atan2(f.y - h.y, f.x - e.x));计算角度,其中Math.atan2(y,x)计算两点之间的弧度,y为角度对应对边y高度差,x为邻边X的差,然后用Math.toDegrees()将弧度转为角度即可。

private GradientDrawable mBGradientDrawable;
//创建一个线性渐变且渐变顺序为从右至左
mBGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, new int[]{0xff111111, 0x00111111});
mBGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);

 private void drawB(Canvas canvas) {
        canvas.save();
        
        //.....
        mBGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
        float afWidth = (float) Math.hypot((a.x - f.x), (a.y - f.y));//a到f的距离
        float screenWidth = (float) Math.hypot(width, height);
        mBGradientDrawable.setBounds((int) (c.x - afWidth / 4), (int) c.y, (int) c.x, (int) (c.y + screenWidth));
        float rotateDegrees = (float) Math.toDegrees(Math.atan2(f.y - h.y, f.x - e.x));
        canvas.rotate(-(rotateDegrees + 90), c.x, c.y);
        mBGradientDrawable.draw(canvas);
        canvas.restore();
}

A上左右投影和B上的投影原理相同,左边的投影是以e为旋转点,建立渐变矩形。右边的投影是以j为旋转点,建立渐变的矩形,然后旋转对应角度即可。

private GradientDrawable mALeftGradientDrawable;
private GradientDrawable mARightGradientDrawable;

mALeftGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, new int[]{0x80333333, 0x00333333});
mALeftGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
mARightGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0x80333333, 0x00111111});
mARightGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);

 private void drawA(Canvas canvas) {
        canvas.save();
        canvas.clipPath(getPathA());
        canvas.drawBitmap(mBitmapA, 0, 0, null);
        
        float aeWidth = (float) Math.hypot((a.x - e.x), (a.y - e.y));
        float afWidth = (float) Math.hypot((a.x - f.x), (a.y - f.y));
        mALeftGradientDrawable.setBounds((int) e.x, (int) e.y, (int) (e.x + 40), (int) (e.y + aeWidth));
        float rotateDegrees = (float) Math.toDegrees(Math.atan2(e.y - a.y, e.x - a.x));
        canvas.rotate(rotateDegrees + 90, e.x, e.y);
        mALeftGradientDrawable.draw(canvas);

        canvas.restore();
        canvas.save();
        float ahWidth = (float) Math.hypot((a.x - h.x), (a.y - h.y));
        mARightGradientDrawable.setBounds((int) h.x, (int) h.y, (int) (h.x + ahWidth + 40), (int) (h.y + 40));
        float mDegrees = (float) Math.toDegrees(Math.atan2(a.y - h.y, a.x - h.x));
        canvas.rotate(mDegrees, h.x, h.y);
        mARightGradientDrawable.draw(canvas);

        canvas.restore();
}

绘制文字

绘制文字除了绘制C界面上的较为简单,我们还是观察上面的GIF图,发现我们C界面成镜像,怎么实现哪?

实现原理和实现上面的阴影有些相似,我们观察下面的图就明白了(我们在翻页过程中对A页面进行复制,然后进行相应的变换并不断刷新即可)。


自定义View实战篇(二)实现小说翻页三 实现翻页动画、阴影、内容_第3张图片

我们用到MatrixBitmap进行变换,我们根据下图实现即可,说说旋转角度的问题,旋转的角度可以理解是先旋转90度,然后逆时针旋转对应的角度,这个角度怎么算?

我们想想之前计算投影在A上面左边的阴影,是不是只需要将其与这条边平行,方便旋转我们可以就是那ae和a到cf底边垂直线之间的角度即可,即90+(-角度),代码在文章末尾。

自定义View实战篇(二)实现小说翻页三 实现翻页动画、阴影、内容_第4张图片

自定义View小说翻页暂时就到这里了,本来计划将代码封装一下 ,整理覆盖翻页原理的,但是计划有变暂时不会花费太多时间整理写博客,后续会将完整代码整理后放在GITHUB上的,当然翻页需要的文本解析会有一篇文章去更新的。

截止目前,完整代码

/**
 * @author Active_Loser
 * @date 2018/11/18
 * Content: 自定义PageView
 * A: 表示当前页面
 * B: 表示上一页或下一页的页面
 * C: 表示翻起的页面,即当前页的背面
 */
public class PageView extends View {

    private static final String TAG = "PageView";

    private Path mPathA;
    private Path mPathB;
    private Path mPathC;
    private Bitmap mBitmapA;
    private Bitmap mBitmapB;
    private Bitmap mBitmapC;
    private Paint mPaintTxt;

    /**
     * 测量出view的宽高
     */
    private int width, height;
    private Point a, f, g, e, h, c, j, b, k, d, i;
    private Scroller mScroller;
    private GradientDrawable mBGradientDrawable;
    private GradientDrawable mALeftGradientDrawable;
    private GradientDrawable mARightGradientDrawable;
    private Bitmap mBitmapD;

    public PageView(Context context) {
        this(context, null);
    }

    public PageView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
        //设置匀速滑动的Scroller
        mScroller = new Scroller(context, new LinearInterpolator());
    }

    private void init() {
        a = new Point();
        f = new Point();
        g = new Point();
        e = new Point();
        h = new Point();
        c = new Point();
        j = new Point();
        b = new Point();
        k = new Point();
        d = new Point();
        i = new Point();

        mPathA = new Path();
        mPathB = new Path();
        mPathC = new Path();

        mPaintTxt = new Paint();
        mPaintTxt.setTextSize(60);
        mPaintTxt.setColor(Color.BLACK);

        mBGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, new int[]{0xff111111, 0x00111111});
        mBGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
        mALeftGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, new int[]{0x80333333, 0x00333333});
        mALeftGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
        mARightGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0x80333333, 0x00111111});
        mARightGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
    }


    @SuppressLint("DrawAllocation")
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = getDefaultSize(600, widthMeasureSpec);
        height = getDefaultSize(1000, heightMeasureSpec);
        setMeasuredDimension(width, height);

        f.x = width;
        f.y = height;

        a.x = -1;
        a.y = -1;

        mBitmapA = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        mBitmapB = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        mBitmapC = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        mBitmapD = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

        Canvas mCanvasA = new Canvas(mBitmapA);
        mCanvasA.drawColor(Color.GREEN);
        mCanvasA.drawText("优化硬件速度;\"Car Home\"程序;支持更多的屏幕分辨率;改良的用户界面;新的浏览器的用户接口和支持HTML5;新的联系人名单;更好的白色/黑色背景比率;改进Google Maps3.1.2;支持Microsoft Exchange;支持内置相机闪光灯;支持数码变焦;改进的虚拟键盘;支持蓝牙2.1;支持动态桌面的设计。", 0, height - 40, mPaintTxt);
        Canvas mCanvasB = new Canvas(mBitmapB);
        mCanvasB.drawColor(Color.YELLOW);
        mCanvasB.drawText("优化硬件速度;\"Car Home\"程序;支持更多的屏幕分辨率;改良的用户界面;新的浏览器的用户接口和支持HTML5;新的联系人名单;更好的白色/黑色背景比率;改进Google Maps3.1.2;支持Microsoft Exchange;支持内置相机闪光灯;支持数码变焦;改进的虚拟键盘;支持蓝牙2.1;支持动态桌面的设计。", 0, height - 40, mPaintTxt);
        Canvas mCanvasD = new Canvas(mBitmapD);
        mCanvasD.drawColor(Color.BLUE);
        Canvas mCanvasC = new Canvas(mBitmapC);
        mCanvasC.drawColor(Color.BLUE);
        mCanvasC.drawText("优化硬件速度;\"Car Home\"程序;支持更多的屏幕分辨率;改良的用户界面;新的浏览器的用户接口和支持HTML5;新的联系人名单;更好的白色/黑色背景比率;改进Google Maps3.1.2;支持Microsoft Exchange;支持内置相机闪光灯;支持数码变焦;改进的虚拟键盘;支持蓝牙2.1;支持动态桌面的设计。", 0, height - 40, mPaintTxt);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        a.x = x;
        a.y = y;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                calculationPoint(a, f);
//                if (c.x < 0) {
//                    calculationAByTouch();
//                    calculationPoint(a, f);
//                }
                postInvalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                calculationPoint(a, f);
//                if (c.x < 0) {
//                    calculationAByTouch();
//                    calculationPoint(a, f);
//                }
                postInvalidate();
                break;
            case MotionEvent.ACTION_UP:
                finishAnim();
                break;
            default:
                break;
        }
        return true;
    }

    private void finishAnim() {
        mScroller.startScroll((int) a.x, (int) a.y, -(width * 2), 0, 500);
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            int currX = mScroller.getCurrX();
            int currY = mScroller.getCurrY();
            a.x = currX;
            a.y = currY;
            calculationPoint(a, f);
            postInvalidate();
        }
    }

    /**
     * 如果c点x坐标小于0,根据触摸点重新测量a点坐标
     */
    private void calculationAByYouch() {
        float w0 = width - c.x;

        float w1 = Math.abs(f.x - a.x);
        float w2 = width * w1 / w0;
        a.x = Math.abs(f.x - w2);

        float h1 = Math.abs(f.y - a.y);
        float h2 = w2 * h1 / w1;
        a.y = Math.abs(f.y - h2);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        if (a.x == -1 && a.y == -1) {
            calculationPoint(a, f);
            drawA(canvas);
        } else {
            drawA(canvas);
            drawC(canvas);
            drawB(canvas);
        }
    }

    /**
     * 剪切A区域
     */
    private void drawA(Canvas canvas) {
        canvas.save();
        canvas.clipPath(getPathA());
        canvas.drawBitmap(mBitmapA, 0, 0, null);
        float aeWidth = (float) Math.hypot((a.x - e.x), (a.y - e.y));
        float afWidth = (float) Math.hypot((a.x - f.x), (a.y - f.y));
        mALeftGradientDrawable.setBounds((int) e.x, (int) e.y, (int) (e.x + 40), (int) (e.y + aeWidth));
        float rotateDegrees = (float) Math.toDegrees(Math.atan2(e.y - a.y, e.x - a.x));
        canvas.rotate(rotateDegrees + 90, e.x, e.y);
        mALeftGradientDrawable.draw(canvas);

        canvas.restore();
        canvas.save();
        float ahWidth = (float) Math.hypot((a.x - h.x), (a.y - h.y));
        mARightGradientDrawable.setBounds((int) h.x, (int) h.y, (int) (h.x + ahWidth + 40), (int) (h.y + 40));
        float mDegrees = (float) Math.toDegrees(Math.atan2(a.y - h.y, a.x - h.x));
        canvas.rotate(mDegrees, h.x, h.y);
        mARightGradientDrawable.draw(canvas);

        canvas.restore();
    }


    /**
     * 剪切C区域
     */
    private void drawC(Canvas canvas) {
        canvas.save();
        canvas.clipPath(getPathA());
        canvas.clipPath(getPathC(), Region.Op.REVERSE_DIFFERENCE);
        //设置线性渐变
        canvas.drawBitmap(mBitmapD, 0,0, null);
        Matrix matrix = new Matrix();
        matrix.postScale(-1, 1);
        matrix.postTranslate(2 * width, 0);
        float rotateDegrees = (float) Math.toDegrees(Math.atan2(e.x - a.x, e.y - a.y));
        matrix.postRotate(90-rotateDegrees, f.x, f.y);
        matrix.postTranslate(-(f.x - a.x), -(f.y - a.y));
        //设置线性渐变
        canvas.drawBitmap(mBitmapC, matrix, null);
        canvas.restore();
    }

    /**
     * 剪切B区域
     */
    private void drawB(Canvas canvas) {
        canvas.save();
        canvas.clipPath(getPathA());
        canvas.clipPath(getPathC(), Region.Op.UNION);
        canvas.clipPath(getPathB(), Region.Op.REVERSE_DIFFERENCE);
        canvas.drawBitmap(mBitmapB, 0, 0, null);

        mBGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
        float afWidth = (float) Math.hypot((a.x - f.x), (a.y - f.y));//a到f的距离
        float screenWidth = (float) Math.hypot(width, height);
        mBGradientDrawable.setBounds((int) (c.x - afWidth / 4), (int) c.y, (int) c.x, (int) (c.y + screenWidth));
        float rotateDegrees = (float) Math.toDegrees(Math.atan2(f.y - h.y, f.x - e.x));
        canvas.rotate(-(rotateDegrees + 90), c.x, c.y);
        mBGradientDrawable.draw(canvas);
        canvas.restore();
    }


    /**
     * 获取区域A的path
     */
    private Path getPathA() {
        mPathA.reset();
        mPathA.lineTo(0, height);
        mPathA.lineTo(c.x, c.y);
        mPathA.quadTo(e.x, e.y, b.x, b.y);
        mPathA.lineTo(a.x, a.y);
        mPathA.lineTo(k.x, k.y);
        mPathA.quadTo(h.x, h.y, j.x, j.y);
        mPathA.lineTo(width, 0);
        mPathA.close();
        return mPathA;
    }


    /**
     * 获取区域C的path
     */
    private Path getPathC() {
        mPathC.reset();
        mPathC.moveTo(i.x, i.y);
        mPathC.lineTo(d.x, d.y);
        mPathC.lineTo(b.x, b.y);
        mPathC.lineTo(a.x, a.y);
        mPathC.lineTo(k.x, k.y);
        mPathC.close();//闭合区域
        return mPathC;
    }

    /**
     * 获取区域B的path
     */
    private Path getPathB() {
        mPathB.reset();
        mPathB.lineTo(0, height);
        mPathB.lineTo(width, height);
        mPathB.lineTo(width, 0);
        mPathB.close();//闭合区域
        return mPathB;
    }

    /**
     * 计算各个点的坐标
     *
     * @param a a点的坐标
     * @param f f点的坐标
     */
    void calculationPoint(Point a, Point f) {
        g.x = (a.x + f.x) / 2;
        g.y = (a.y + f.y) / 2;

        e.x = g.x - (f.y - g.y) * (f.y - g.y) / (f.x - g.x);
        e.y = f.y;

        h.x = f.x;
        h.y = g.y - (f.x - g.x) * ((f.x - g.x) / (f.y - g.y));

        c.x = e.x - (f.x - e.x) / 2;
        c.y = f.y;

        j.x = f.x;
        j.y = h.y - (f.y - h.y) / 2;

        b.x = (a.x + e.x) / 2;
        b.y = (a.y + e.y) / 2;

        k.x = (a.x + h.x) / 2;
        k.y = (a.y + h.y) / 2;

        d.x = ((c.x + b.x) / 2 + e.x) / 2;
        d.y = ((c.y + b.y) / 2 + e.y) / 2;

        i.x = ((k.x + j.x) / 2 + h.x) / 2;
        i.y = ((k.y + j.y) / 2 + h.y) / 2;
    }
}

总体来说,通过这种方式实现的小说翻可能会存在各种问题,这里推荐OpenGL 实现的PageFlip,有兴趣的可以参看学习,但是实际开发中我们可能会自己去做自定义控件,后面会自己写一款小说APP集成这部分功能。

你可能感兴趣的:(自定义View实战篇(二)实现小说翻页三 实现翻页动画、阴影、内容)