Android 自定义View贝塞尔曲线实现书籍翻页的效果(包含原理解释)

先看效果图:

Android 自定义View贝塞尔曲线实现书籍翻页的效果(包含原理解释)

我先来解释一下该翻页的实现原理,大家来看下面这张图:

Android 自定义View贝塞尔曲线实现书籍翻页的效果(包含原理解释)

我们可以把翻页时的图案分为三部分,分别是第一页的图案,第一页的背面图案,以及第二页的图案。

我们将图形进一步数学化:

Android 自定义View贝塞尔曲线实现书籍翻页的效果(包含原理解释)

其中,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;
}

/**
* 将sp值转换为px值
*/
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>

 

原创:https://www.panoramacn.com
源码网提供WordPress源码,帝国CMS源码discuz源码,微信小程序,小说源码,杰奇源码,thinkphp源码,ecshop模板源码,微擎模板源码,dede源码,织梦源码等。

专业搭建小说网站,小说程序,杰奇系列,微信小说系列,app系列小说

Android 自定义View贝塞尔曲线实现书籍翻页的效果(包含原理解释)

免责声明,若由于商用引起版权纠纷,一切责任均由使用者承担。

您必须遵守我们的协议,如果您下载了该资源行为将被视为对《免责声明》全部内容的认可-> 联系客服 投诉资源
www.panoramacn.com资源全部来自互联网收集,仅供用于学习和交流,请勿用于商业用途。如有侵权、不妥之处,请联系站长并出示版权证明以便删除。 敬请谅解! 侵权删帖/违法举报/投稿等事物联系邮箱:2640602276@qq.com
未经允许不得转载:书荒源码源码网每日更新网站源码模板! » Android 自定义View贝塞尔曲线实现书籍翻页的效果(包含原理解释)
关注我们小说电影免费看
关注我们,获取更多的全网素材资源,有趣有料!
120000+人已关注
分享到:
赞(0) 打赏

评论抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

您的打赏就是我分享的动力!

支付宝扫一扫打赏

微信扫一扫打赏