先看效果图:
我先来解释一下该翻页的实现原理,大家来看下面这张图:
我们可以把翻页时的图案分为三部分,分别是第一页的图案,第一页的背面图案,以及第二页的图案。
我们将图形进一步数学化:
其中,c、d、b是以e为控制点的贝塞尔曲线上的点。同样,j、i、k是以h为控制点的贝塞尔曲线上的点。
a为翻角的顶点,线段eh为线段af的中垂线。
根据上图,我们可以对线段af左侧做出假设(另一边也同理):
ce=ef/2
p是线段cb的中点
d是线段pe的中点
b是ae和cj的交点
由于红色部分中的曲线db和曲线ik我们无法得知它的函数式,所以红色部分不能直接绘制,但是我们可以绘制出红色部分加黄色部分的区域,记为PathC;黄色区域我们也可以单独绘制(c和d按直线相连),记为PathB。然后我们用clipPath(PathB,Region.Op.DIFFERENCE)来切割出红色区域,Op.DIFFERENCE指的是取PathB中与PathC不相同的区域。
另外一说,绿色区域我们也可以单独绘制。这样一来,三个区域我们就都能得到了。
所以,我们最重要的工作就是计算每个点的坐标,下面我来陈述一下每个点的计算方法:
a点是我们已知的,记为(ax,ay)
f点是屏幕右下角的点,也是已知的,记为(fx,fy)
g是线段af和eh的交点,记为(gx,gy),有gx=(ax+fx)/2,gy=(ay+fy)/2
直线eh的斜率记为Keh,有Keh=(-1)*(fx-ax)/(fy-gy)
直线eh的函数式为:y=Keh*(x-gx)+gy
e点记为(ex,ey),ey=fy,带入直线eh函数式可得ex=gx+(fy-gy)/Keh
h点记为(hx,hy),hx=fx,带入直线eh函数式可得hy=gy+Keh(fx-gx)
c点记为(cx,cy),因为ce=ef/2,则cx=ex-ce=ex-ef/2=ex-(fx-ex)/2,cy=fy
j点记为(jx,jy),和c点同理,jh=hf/2,有jy=hy-jh=hy-fh/2=hy-(fy-hy)/2,jx=fx
d点记为(dx,dy),因为p是cb中点,d是pe中点,有dx=(ex+px)/2=(ex+(cx+bx)/2)/2,dy=(ey+(cy+by)/2)/2
i点记为(ix,iy),和d点同理,有ix=(hx+(kx+jx)/2)/2,iy=(hy+(ky+jy)/2)/2
直线cj的函数式为:y=Kcj(x-jx)+jy,斜率为Kcj=(jy-cy)/(jx-cx)
直线ae的函数式为:y=Kae(x-ax)+ay,斜率为Kae=(ay-ey)/(ax-ex)
直线ah的函数式为:y=Kah(x-ax)+ay,斜率为Kah=(ay-hy)/(ax-hx)
b点记为(bx,by),因为b点cj和ae的交点,计算可得bx=(ay-Kae*ax+Kcj*jx-jy)/(Kcj-Kae),by=Kcj(bx-jx)+jy
k点记为(kx,ky),因为k点cj和ah的交点,计算可得kx=(ay-Kah*ax+Kcj*jx-jy)/(Kcj-Kah),ky=Kcj(kx-jx)+jy
以上就是每个点的计算方式,下面我们创建一个PaperPoint类,来将计算过程写成代码:
public class PaperPoint {
//拉拽点
private MyPoint a;
//右下角的点
private MyPoint f;
//贝塞尔点(e为控制点)
private MyPoint c,d,b,e;
//贝塞尔点(h为控制点)
private MyPoint i,j,k,h;
//eh实际为af中垂线,g为ah和af的交点
private MyPoint g;
public PaperPoint(){
a=new MyPoint();f=new MyPoint();
g=new MyPoint();e=new MyPoint();
h=new MyPoint();c=new MyPoint();
j=new MyPoint();d=new MyPoint();
i=new MyPoint();b=new MyPoint();
k=new MyPoint();
}
//每个点的计算公式
private void calculate(){
g.setX((a.getX()+f.getX())/2);
g.setY((a.getY()+f.getY())/2);
float slopeKeh=-(f.getX()-a.getX())/(f.getY()-a.getY());
e.setX(g.getX()+(f.getY()-g.getY())/slopeKeh);
e.setY(f.getY());
h.setX(f.getX());
h.setY(g.getY()+slopeKeh*(f.getX()-g.getX()));
c.setX(e.getX()-(f.getX()-e.getX())/2);
c.setY(f.getY());
j.setX(f.getX());
j.setY(h.getY()-(f.getY()-h.getY())/2);
float slopeKcj=(j.getY()-c.getY())/(j.getX()-c.getX());
float slopeKae=(a.getY()-e.getY())/(a.getX()-e.getX());
float slopeKah=(a.getY()-h.getY())/(a.getX()-h.getX());
b.setX((a.getY()-slopeKae*a.getX()+slopeKcj*j.getX()-j.getY())/(slopeKcj-slopeKae));
b.setY(slopeKcj*(b.getX()-j.getX())+j.getY());
k.setX((a.getY()-slopeKah*a.getX()+slopeKcj*j.getX()-j.getY())/(slopeKcj-slopeKah));
k.setY(slopeKcj*(k.getX()-j.getX())+j.getY());
d.setX(b.getX()/4+c.getX()/4+e.getX()/2);
d.setY(b.getY()/4+c.getY()/4+e.getY()/2);
i.setX(j.getX()/4+k.getX()/4+h.getX()/2);
i.setY(j.getY()/4+k.getY()/4+h.getY()/2);
}
public void set(MyPoint a,MyPoint f) {
this.a = a;
this.f = f;
calculate();
}
public MyPoint getA() {
return a;
}
public MyPoint getF() {
return f;
}
public MyPoint getC() {
return c;
}
public MyPoint getD() {
return d;
}
public MyPoint getB() {
return b;
}
public MyPoint getE() {
return e;
}
public MyPoint getI() {
return i;
}
public MyPoint getJ() {
return j;
}
public MyPoint getK() {
return k;
}
public MyPoint getH() {
return h;
}
public MyPoint getG() {
return g;
}
}
我们计算好每个点后,就要开始绘制任务了,首先我们来绘制绿色区域的内容:
private PaperPoint pp;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//......
//绘制第一页
canvas.save();
canvas.clipPath(getPathA());
canvas.drawBitmap(bitmapBg,matrix,null);
//绘制文章内容
drawArticle(canvas,0);
canvas.restore();
//......
}
//得到第一页图形
private Path getPathA(){
path.reset();
path.lineTo(0,height);
path.lineTo(pp.getC().getX(),height);
path.quadTo(pp.getE().getX(),pp.getE().getY(),pp.getB().getX(),pp.getB().getY());
path.lineTo(pp.getA().getX(),pp.getA().getY());
path.lineTo(pp.getK().getX(),pp.getK().getY());
path.quadTo(pp.getH().getX(),pp.getH().getY(),pp.getJ().getX(),pp.getJ().getY());
path.lineTo(width,0);
path.close();
return path;
}
绘制黄色区域加上红色区域所在的总区域:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//......
//绘制第二页和翻转背面的内容
canvas.save();
canvas.clipPath(getPathC());
canvas.drawBitmap(bitmapBg,matrix,null);
drawArticle(canvas,1);
canvas.restore();
//......
}
//得到第二页和翻转背面的图形
private Path getPathC(){
pathC.reset();
pathC.moveTo(pp.getJ().getX(),pp.getJ().getY());
pathC.quadTo(pp.getH().getX(),pp.getH().getY(), pp.getK().getX(), pp.getK().getY());
pathC.lineTo(pp.getA().getX(),pp.getA().getY());
pathC.lineTo(pp.getB().getX(),pp.getB().getY());
pathC.quadTo(pp.getE().getX(),pp.getE().getY(),pp.getC().getX(),pp.getC().getY());
pathC.lineTo(pp.getF().getX(),pp.getF().getY());
pathC.close();
return pathC;
}
用clipPath切分出红色的翻角区域:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//......
//绘制第一页的背面
canvas.save();
canvas.clipPath(pathC);
canvas.clipPath(getPathB(), Region.Op.DIFFERENCE);//difference最取出两段path中不同的地方
canvas.drawBitmap(bitmapBg,matrix,null);
canvas.restore();
}
//得到第二页的图形
private Path getPathB(){
pathB.reset();
pathB.moveTo(pp.getC().getX(), pp.getC().getY());
pathB.lineTo(pp.getD().getX(),pp.getD().getY());
pathB.lineTo(pp.getI().getX(),pp.getI().getY());
pathB.lineTo(pp.getJ().getX(),pp.getJ().getY());
pathB.lineTo(pp.getF().getX(),pp.getF().getY());
pathB.close();
return pathB;
}
到这里,翻页效果已经实现,最后我们来监听点击事件,以此来设置a点坐标:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
pp.set(new MyPoint(event.getX(),event.getY()),new MyPoint(Constants.SCREEN_WIDTH,Constants.SCREEN_HEIGHT));
invalidate();
break;
}
return true;
}
下面是自定义view源码:
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Region;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.hualinfo.bean.beizer.MyPoint;
import com.hualinfo.utils.PaperPoint;
import androidx.annotation.Nullable;
public class MyBeizerView extends View {
private PaperPoint pp;
private Path path,pathB,pathC; //第一页路径,第二页路径,第二页和翻转背面的路径
private Paint txtPaint;
private Matrix matrix;
private int width= Constants.SCREEN_WIDTH;
private int height= Constants.SCREEN_HEIGHT;
private String[] strs=new String[2]; //文本
private Bitmap bitmapBg;
public MyBeizerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
initBitmap();
path=new Path();pathB=new Path();pathC=new Path();
txtPaint=new Paint();
txtPaint.setColor(Color.WHITE);
txtPaint.setTextSize(sp2px(16));
pp=new PaperPoint();
pp.set(new MyPoint(Constants.SCREEN_WIDTH,Constants.SCREEN_HEIGHT),new MyPoint(Constants.SCREEN_WIDTH,Constants.SCREEN_HEIGHT));
strs[0]=getResources().getString(R.string.str2);
strs[1]=getResources().getString(R.string.str3);
}
private void initBitmap(){
matrix=new Matrix();
bitmapBg= BitmapFactory.decodeResource(getResources(),R.mipmap.bg_article);
float scaleX=1,scaleY=1;
//如果图片与圆的直径不一致,等比例缩放图片
if(bitmapBg.getWidth()!=width||bitmapBg.getHeight()!=height){
scaleX=width/(bitmapBg.getWidth()*1.0f);
scaleY=height/(bitmapBg.getHeight()*1.0f);
}
matrix.setScale(scaleX,scaleY);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(pp.getA().getX()==Constants.SCREEN_WIDTH&&pp.getA().getY()==Constants.SCREEN_HEIGHT){
canvas.drawBitmap(bitmapBg,matrix,null);
drawArticle(canvas,0);
return;
}
//绘制第一页
canvas.save();
canvas.clipPath(getPathA());
canvas.drawBitmap(bitmapBg,matrix,null);
//绘制文章内容
drawArticle(canvas,0);
canvas.restore();
//绘制第二页和翻转背面的内容
canvas.save();
canvas.clipPath(getPathC());
canvas.drawBitmap(bitmapBg,matrix,null);
drawArticle(canvas,1);
canvas.restore();
//绘制第一页的背面
canvas.save();
canvas.clipPath(pathC);
canvas.clipPath(getPathB(), Region.Op.DIFFERENCE);//difference最取出两段path中不同的地方
canvas.drawBitmap(bitmapBg,matrix,null);
canvas.restore();
}
//绘制文章的文本
private void drawArticle(Canvas canvas,int pos){
int lineNum=(int)(getWidth()/txtPaint.getTextSize());
int size=strs[pos].length()/lineNum;
for(int i=0;i<=size;i++){
int endPos=(i+1)*lineNum;
if(endPos>=strs[pos].length())endPos=strs[pos].length()-1;
canvas.drawText(strs[pos],i*lineNum,endPos,0,sp2px(25*i+20),txtPaint);
}
}
//得到第一页图形
private Path getPathA(){
path.reset();
path.lineTo(0,height);
path.lineTo(pp.getC().getX(),height);
path.quadTo(pp.getE().getX(),pp.getE().getY(),pp.getB().getX(),pp.getB().getY());
path.lineTo(pp.getA().getX(),pp.getA().getY());
path.lineTo(pp.getK().getX(),pp.getK().getY());
path.quadTo(pp.getH().getX(),pp.getH().getY(),pp.getJ().getX(),pp.getJ().getY());
path.lineTo(width,0);
path.close();
return path;
}
//得到第二页的图形
private Path getPathB(){
pathB.reset();
pathB.moveTo(pp.getC().getX(), pp.getC().getY());
pathB.lineTo(pp.getD().getX(),pp.getD().getY());
pathB.lineTo(pp.getI().getX(),pp.getI().getY());
pathB.lineTo(pp.getJ().getX(),pp.getJ().getY());
pathB.lineTo(pp.getF().getX(),pp.getF().getY());
pathB.close();
return pathB;
}
//得到第二页和翻转背面的图形
private Path getPathC(){
pathC.reset();
pathC.moveTo(pp.getJ().getX(),pp.getJ().getY());
pathC.quadTo(pp.getH().getX(),pp.getH().getY(), pp.getK().getX(), pp.getK().getY());
pathC.lineTo(pp.getA().getX(),pp.getA().getY());
pathC.lineTo(pp.getB().getX(),pp.getB().getY());
pathC.quadTo(pp.getE().getX(),pp.getE().getY(),pp.getC().getX(),pp.getC().getY());
pathC.lineTo(pp.getF().getX(),pp.getF().getY());
pathC.close();
return pathC;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
pp.set(new MyPoint(event.getX(),event.getY()),new MyPoint(Constants.SCREEN_WIDTH,Constants.SCREEN_HEIGHT));
invalidate();
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
public int sp2px(float spValue) {
float fontScale = getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
}
MyPoint类:
public class MyPoint {
private float x;
private float y;
public MyPoint() {
}
public MyPoint(float x, float y) {
this.x = x;
this.y = y;
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
}
xml布局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">
<com.myviewtext.MyBeizerView
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>