IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    仿书页翻页效果实现思路

    bt发表于 2023-07-14 17:09:00
    love 0

    下面这篇文章明了书页翻页的原理,但是不是特别清晰,知道各种测量,但是不明白为什么要这么计算,以下以个人思路重新梳理一遍,方便理解
    【转】Android 实现书籍翻页效果----原理篇

    最终效果预览

    文末附上完整demo代码
    2023-07-14T09:13:44.png

    从折纸开始

    由于原文公式太多,不便理解,从这里开始以我尝试开发的心路历程,抽丝剥茧演示如何实现。
    首先先模拟一个纸张,没有Z轴的效果,将纸张的边线画出来。

    1. 选定控制点

    先选定手指触摸的纸张的触摸点,表示纸张的端点将被固定在这个位置,该点定义为A(x,y)
    被拖动的端点定义为O(width, height),右下角的端点
    2023-07-14T09:18:42.png

    2. 根据触摸点绘制折痕

    当前折痕不存在Z轴概念,直接绘制,折痕的位置一定是落在AO连接线的中点B,且因为是折痕,两边一定是延折痕对称,那么镜像的点的连线一定是垂直于折痕的。
    根据相似三角形,对应边的比例是相等的,能够求得C,D点的位置

    centerAO.x = touchP.x / 2 + startP.x /2;
    centerAO.y = touchP.y / 2 + startP.y /2;
    // 相似三角形,不用取绝对值。分子必定为正数。根据分母的正负决定C点位置
    pointC.x  = centerAO.x - (startP.y - centerAO.y ) * (startP.y - centerAO.y) / (startP.x - centerAO.x);
    pointC.y  = startP.y;
    pointD.y  = centerAO.y - (startP.x - centerAO.x ) * (startP.x - centerAO.x) / (startP.y - centerAO.y);
    pointD.x  = startP.x;

    2023-07-17T08:00:29.png

    3. 模拟Z轴空间的折痕

    现在视觉效果已经大致有了,翻过来的折页紧贴着纸张,没有空间感,下面开始优化
    下面需要在AB范围内选择一个CD的平行线作为折痕,因为存在Z轴,部分纸张被弯曲了,实际折痕一定无法达到CD位置
    定义一个折痕比率foldRatio,越大表示离CD越远,暂时取值中点,绘制出EF点,分别与XY轴边线相交

    // 计算三维空间中Z轴底部的折痕位置
    pointE.x = pointC.x - (startP.x - pointC.x) * foldRatio;
    pointE.y = pointC.y;
    pointF.y = pointD.y - (startP.y - pointD.y) * foldRatio;
    pointF.x = pointD.x;

    2023-07-17T08:09:58.png

    4. 折痕过渡参考线

    选取CD EF中间等分位置绘制出一个参考线,理论上线条到这个线的时候就应该拐弯了,拐弯的部分代表折痕卷曲空鼓的部分,这里当然不一定是正中间,感兴趣的也可以定义一个参数去控制

    // 卷曲边缘在折痕和预期折痕的中间位置
    pointG.x = pointE.x /2 + pointC.x / 2;
    pointG.y = pointE.y;
    pointH.x = pointF.x;
    pointH.y = pointF.y / 2 + pointD.y /2;

    2023-07-17T08:20:11.png

    5. 补充剩余点

    求相交点就很容易了,已知2点可以得出直线的方程式
    2个方程式相等时就是交点,很容易求得(x,y)坐标,补充剩余点的位置
    M N点为垂直平分线相交点,因为折痕上下的弧度应当相同,故取中点
    2023-07-17T08:34:59.png

    6. 平滑过渡

    此时三维的折痕雏形已经有了,但是直接连接AIKMGE,过渡显得特别硬,这个时候就需要贝塞尔曲线登场了,这里用的是二阶贝塞尔曲线,这里的K G就是控制点,M为端点,连线后的效果
    2023-07-17T08:43:41.png

    7. 上色

    这里定义3个颜色,下一页的颜色,上一页的颜色,背面的颜色
    下一页的颜色:最好绘制,因为最终会被盖住一部分,所以默认直接绘制全屏即可
    背面颜色:AIMNJ区域,是个规则图形,这个也比较好计算,直接path连接即可得到
    上一页的颜色:上一页根据拖拽的位置不同,产生的区域也是不规则的,所以使用canvas的裁切,将不需要的部分裁切掉,剩余部分上色即可

    clip.reset();
    clip.moveTo(touchP.x, touchP.y);
    clip.lineTo(pointI.x, pointI.y);
    clip.quadTo(pointK.x, pointK.y, pointM.x, pointM.y);
    clip.quadTo(pointG.x, pointG.y, pointE.x, pointE.y);
    clip.lineTo(startP.x, startP.y);
    clip.lineTo(pointF.x, pointF.y);
    clip.quadTo(pointH.x, pointH.y,pointN.x, pointN.y);
    clip.quadTo(pointL.x, pointL.y,pointJ.x, pointJ.y);
    clip.close();
    canvas.save();
    canvas.clipPath(clip, Region.Op.DIFFERENCE);;
    pagePaint.setColor(Color.GREEN);
    canvas.drawRect(canvas.getClipBounds(),pagePaint);
    canvas.restore();
    back.reset();
    back.moveTo(touchP.x, touchP.y);
    back.lineTo(pointI.x, pointI.y);
    back.quadTo(pointK.x, pointK.y, pointM.x, pointM.y);
    back.lineTo(pointN.x, pointN.y);
    back.quadTo(pointL.x, pointL.y,pointJ.x, pointJ.y);
    back.close();
    canvas.drawPath(back, linePaint);

    2023-07-17T08:49:04.png

    8.完整代码

    现在实现整体思路已完成,只需要在touch事件里动态修改A点坐标即可实现动画。
    翻页点通过修改O点坐标即可实现

    package com.example.myapplication.page;
    
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.graphics.Path;
    import android.graphics.Point;
    import android.graphics.PointF;
    import android.graphics.Region;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.View;
    
    import androidx.annotation.Nullable;
    
    public class TurnPageView extends View {
        /**
         * 折痕比率,越大纸的翻起高度越高
         */
        private float foldRatio = 0.5f;
        private Paint textPaint;
        private Paint pagePaint;
        private Paint linePaint;
        private Paint guideLinePaint;
    
        private PointF touchP = new PointF();
        private PointF startP = new PointF();
        private PointF centerAO = new PointF();
        private PointF pointC = new PointF();
        private PointF pointD = new PointF();
        private PointF pointE = new PointF();
        private PointF pointF = new PointF();
        private PointF pointG = new PointF();
        private PointF pointH = new PointF();
        private PointF pointI = new PointF();
        private PointF pointJ = new PointF();
        private PointF pointK = new PointF();
        private PointF pointL = new PointF();
        private PointF pointM = new PointF();
        private PointF pointN = new PointF();
        private Path clip = new Path();
        private Path back = new Path();
        public TurnPageView(Context context) {
            this(context, null);
        }
    
        public TurnPageView(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public TurnPageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            this(context, attrs, defStyleAttr, 0);
        }
    
        public TurnPageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
            guideLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            pagePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    
            textPaint.setTextSize(40);
            textPaint.setStrokeWidth(3);
            textPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            textPaint.setColor(Color.BLACK);
    
            linePaint.setColor(Color.RED);
            linePaint.setStyle(Paint.Style.FILL_AND_STROKE);
            linePaint.setStrokeWidth(2);
    
            pagePaint.setStyle(Paint.Style.FILL);
    
            guideLinePaint.setColor(Color.BLACK);
            guideLinePaint.setAlpha(100);
            guideLinePaint.setStyle(Paint.Style.STROKE);
            guideLinePaint.setStrokeWidth(2);
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            touchP.x = right  - left - 300;
            touchP.y = bottom - top - 600;
            startP.x = right - left;
            startP.y = bottom - top;
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            calculate();
            drawGuideLines(canvas);
            drawPoints(canvas);
    
        }
        private void calculate(){
            centerAO.x = touchP.x / 2 + startP.x /2;
            centerAO.y = touchP.y / 2 + startP.y /2;
            // 相似三角形,不用取绝对值。分子必定为正数。根据分母的正负决定C点位置
            pointC.x  = centerAO.x - (startP.y - centerAO.y ) * (startP.y - centerAO.y) / (startP.x - centerAO.x);
            pointC.y  = startP.y;
            // 相似三角形,不用取绝对值。分子必定为正数。根据分母的正负决定C点位置
            pointD.y  = centerAO.y - (startP.x - centerAO.x ) * (startP.x - centerAO.x) / (startP.y - centerAO.y);
            pointD.x  = startP.x;
            // 计算三维空间中Z轴底部的折痕位置
            pointE.x = pointC.x - (startP.x - pointC.x) * foldRatio;
            pointE.y = pointC.y;
            pointF.y = pointD.y - (startP.y - pointD.y) * foldRatio;
            pointF.x = pointD.x;
            // 卷曲边缘在折痕和预期折痕的中间位置
            pointG.x = pointE.x /2 + pointC.x / 2;
            pointG.y = pointE.y;
            pointH.x = pointF.x;
            pointH.y = pointF.y / 2 + pointD.y /2;
            // 获取底部折痕和翻过来的纸的相交点,理论上从这里就应该要开始变化成弧线了,需要作为贝塞尔曲线的起始点
            getNodeForTwoLine(pointI, pointE, pointF, touchP, pointC);
            getNodeForTwoLine(pointJ, pointE, pointF, touchP, pointD);
            // 弧线的控制点,2条线刚好是弧线的切线
            getNodeForTwoLine(pointK, pointG, pointH, touchP, pointC);
            getNodeForTwoLine(pointL, pointG, pointH, touchP, pointD);
            // C点到GH边缘的垂线交点,理论上交点应当是弧线开始变方向的点,刚好GH是其切线
            pointM.x = pointG.x /2 + pointK.x /2;
            pointM.y = pointG.y /2 + pointK.y /2;
            pointN.x = pointL.x /2 + pointH.x /2;
            pointN.y = pointL.y /2 + pointH.y /2;
        }
        private void drawGuideLines(Canvas canvas){
            pagePaint.setColor(Color.BLUE);
            canvas.drawRect(canvas.getClipBounds(),pagePaint);
            // 绘制AO
            canvas.drawLine(startP.x, startP.y, touchP.x, touchP.y, guideLinePaint);
            // 绘制AC,OC
            canvas.drawLine(pointC.x, pointC.y, touchP.x, touchP.y, guideLinePaint);
            canvas.drawLine(startP.x, startP.y, pointC.x, pointC.y, guideLinePaint);
            // 绘制AD,OD
            canvas.drawLine(pointD.x, pointD.y, touchP.x, touchP.y, guideLinePaint);
            canvas.drawLine(startP.x, startP.y, pointD.x, pointD.y, guideLinePaint);
            // 绘制CD
            canvas.drawLine(pointD.x, pointD.y, pointC.x, pointC.y, guideLinePaint);
            // 绘制EF
            canvas.drawLine(pointE.x, pointE.y, pointF.x, pointF.y, guideLinePaint);
            // 绘制gh
            canvas.drawLine(pointG.x, pointG.y, pointH.x, pointH.y, guideLinePaint);
    
    
            clip.reset();
            clip.moveTo(touchP.x, touchP.y);
            clip.lineTo(pointI.x, pointI.y);
            clip.quadTo(pointK.x, pointK.y, pointM.x, pointM.y);
            clip.quadTo(pointG.x, pointG.y, pointE.x, pointE.y);
            clip.lineTo(startP.x, startP.y);
            clip.lineTo(pointF.x, pointF.y);
            clip.quadTo(pointH.x, pointH.y,pointN.x, pointN.y);
            clip.quadTo(pointL.x, pointL.y,pointJ.x, pointJ.y);
            clip.close();
            canvas.save();
            canvas.clipPath(clip, Region.Op.DIFFERENCE);;
            pagePaint.setColor(Color.GREEN);
            canvas.drawRect(canvas.getClipBounds(),pagePaint);
            canvas.restore();
            back.reset();
            back.moveTo(touchP.x, touchP.y);
            back.lineTo(pointI.x, pointI.y);
            back.quadTo(pointK.x, pointK.y, pointM.x, pointM.y);
            back.lineTo(pointN.x, pointN.y);
            back.quadTo(pointL.x, pointL.y,pointJ.x, pointJ.y);
            back.close();
            canvas.drawPath(back, linePaint);
        }
        private void drawPoints(Canvas canvas) {
            // 触摸点
            drawPoint(canvas, "A", touchP.x, touchP.y);
            // 起始端点
            drawPoint(canvas, "O", startP.x, startP.y);
            // AO中点
            drawPoint(canvas, "B", centerAO.x, centerAO.y);
            // C点:垂直平分线和O点水平相交
            drawPoint(canvas, "C", pointC.x, pointC.y);
            // D点:垂直平分线和O点垂直相交
            drawPoint(canvas, "D", pointD.x, pointD.y);
            // EF点:z轴底部折痕
            drawPoint(canvas, "E", pointE.x, pointE.y);
            drawPoint(canvas, "F", pointF.x, pointF.y);
    
            // GH点:弯曲边缘(弧线控制点)
            drawPoint(canvas, "G", pointG.x, pointG.y);
            drawPoint(canvas, "H", pointH.x, pointH.y);
            // 相交点
            drawPoint(canvas, "I", pointI.x, pointI.y);
            drawPoint(canvas, "J", pointJ.x, pointJ.y);
            // 相交点
            drawPoint(canvas, "K", pointK.x, pointK.y);
            drawPoint(canvas, "L", pointL.x, pointL.y);
            // 相交点
            drawPoint(canvas, "M", pointM.x, pointM.y);
            drawPoint(canvas, "N", pointN.x, pointN.y);
    
        }
        private void drawPoint(Canvas canvas, String text, float x, float y) {
            canvas.drawCircle(x, y, 5, textPaint);
            canvas.drawText(text, x - 40, y, textPaint);
        }
    
    
        private void getNodeForTwoLine(PointF pointF,PointF lineA1, PointF lineA2, PointF lineB1, PointF lineB2){
            // y = ax + b;
            float a1,b1,a2,b2;
            // 实际就是求斜率和偏移
            a1 = (lineA1.y - lineA2.y) / (lineA1.x - lineA2.x);
            b1 = lineA1.y - a1 * lineA1.x;
            a2 = (lineB1.y - lineB2.y) / (lineB1.x - lineB2.x);
            b2 = lineB1.y - a2 * lineB1.x;
    
            //a1x +b1 = a2x + b2
            //x = b2-b1/a1-a2
            pointF.x = (b2 - b1) / (a1 - a2);
            pointF.y = a1 * pointF.x + b1;
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            touchP.x = event.getX();
            touchP.y = event.getY();
            invalidate();
            return true;
        }
    }
    


沪ICP备19023445号-2号
友情链接