下面这篇文章明了书页翻页的原理,但是不是特别清晰,知道各种测量,但是不明白为什么要这么计算,以下以个人思路重新梳理一遍,方便理解
【转】Android 实现书籍翻页效果----原理篇
最终效果预览
文末附上完整demo代码
从折纸开始
由于原文公式太多,不便理解,从这里开始以我尝试开发的心路历程,抽丝剥茧演示如何实现。
首先先模拟一个纸张,没有Z轴的效果,将纸张的边线画出来。
1. 选定控制点
先选定手指触摸的纸张的触摸点,表示纸张的端点将被固定在这个位置,该点定义为A
(x,y)
被拖动的端点定义为O
(width, height),右下角的端点
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;
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;
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;
5. 补充剩余点
求相交点就很容易了,已知2点可以得出直线的方程式
2个方程式相等时就是交点,很容易求得(x,y)坐标,补充剩余点的位置
M
N
点为垂直平分线相交点,因为折痕上下的弧度应当相同,故取中点
6. 平滑过渡
此时三维的折痕雏形已经有了,但是直接连接AIKMGE
,过渡显得特别硬,这个时候就需要贝塞尔曲线登场了,这里用的是二阶贝塞尔曲线,这里的K
G
就是控制点,M为端点,连线后的效果
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);
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;
}
}
本文由 bt 创作,采用 知识共享署名4.0 国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。