Android自定义View——从零开始实现书籍翻页效果

简单总结一下,a是触摸点,f是触摸点相对的边缘角,eh我们设置为af的垂直平分线,则gaf的中点,abakdj直线曲线cdb是起点为c,控制点为e,终点为b二阶贝塞尔曲线曲线kij是起点为k,控制点为h,终点为j二阶贝塞尔曲线,区域ABC就由这些点和线划分开来。我们将这些点称为标识点,下一步就是模拟设定af点的位置,将这组标识点绘制到屏幕上来验证我们的计算公式是否正确,创建BookPageView

public class BookPageView extends View {
private Paint pointPaint;//绘制各标识点的画笔
private Paint bgPaint;//背景画笔

private MyPoint a,f,g,e,h,c,j,b,k,d,i;

private int defaultWidth;//默认宽度
private int defaultHeight;//默认高度
private int viewWidth;
private int viewHeight;

public BookPageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context,attrs);
}

private void init(Context context, @Nullable AttributeSet attrs){
defaultWidth = 600;
defaultHeight = 1000;

viewWidth = defaultWidth;
viewHeight = defaultHeight;

a = new MyPoint(400,800);
f = new MyPoint(viewWidth,viewHeight);
g = new MyPoint();
e = new MyPoint();
h = new MyPoint();
c = new MyP 《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》无偿开源 徽信搜索公众号【编程进阶路】 oint();
j = new MyPoint();
b = new MyPoint();
k = new MyPoint();
d = new MyPoint();
i = new MyPoint();
calcPointsXY(a,f);

pointPaint = new Paint();
pointPaint.setColor(Color.RED);
pointPaint.setTextSize(25);

bgPaint = new Paint();
bgPaint.setColor(Color.GREEN);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//为了看清楚点与View的位置关系绘制一个背景
canvas.drawRect(0,0,viewWidth,viewHeight,bgPaint);
//绘制各标识点
canvas.drawText(“a”,a.x,a.y,pointPaint);
canvas.drawText(“f”,f.x,f.y,pointPaint);
canvas.drawText(“g”,g.x,g.y,pointPaint);

canvas.drawText(“e”,e.x,e.y,pointPaint);
canvas.drawText(“h”,h.x,h.y,pointPaint);

canvas.drawText(“c”,c.x,c.y,pointPaint);
canvas.drawText(“j”,j.x,j.y,pointPaint);

canvas.drawText(“b”,b.x,b.y,pointPaint);
canvas.drawText(“k”,k.x,k.y,pointPaint);

canvas.drawText(“d”,d.x,d.y,pointPaint);
canvas.drawText(“i”,i.x,i.y,pointPaint);
}

/**

  • 计算各点坐标
  • @param a
  • @param f
    */
    private void calcPointsXY(MyPoint a, MyPoint 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 = getIntersectionPoint(a,e,c,j);
k = getIntersectionPoint(a,h,c,j);

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

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

/**

  • 计算两线段相交点坐标
  • @param lineOne_My_pointOne
  • @param lineOne_My_pointTwo
  • @param lineTwo_My_pointOne
  • @param lineTwo_My_pointTwo
  • @return 返回该点
    */
    private MyPoint getIntersectionPoint(MyPoint lineOne_My_pointOne, MyPoint lineOne_My_pointTwo, MyPoint lineTwo_My_pointOne, MyPoint lineTwo_My_pointTwo){
    float x1,y1,x2,y2,x3,y3,x4,y4;
    x1 = lineOne_My_pointOne.x;
    y1 = lineOne_My_pointOne.y;
    x2 = lineOne_My_pointTwo.x;
    y2 = lineOne_My_pointTwo.y;
    x3 = lineTwo_My_pointOne.x;
    y3 = lineTwo_My_pointOne.y;
    x4 = lineTwo_My_pointTwo.x;
    y4 = lineTwo_My_pointTwo.y;

float pointX =((x1 - x2) * (x3 * y4 - x4 * y3) - (x3 - x4) * (x1 * y2 - x2 * y1))
/ ((x3 - x4) * (y1 - y2) - (x1 - x2) * (y3 - y4));
float pointY =((y1 - y2) * (x3 * y4 - x4 * y3) - (x1 * y2 - x2 * y1) * (y3 - y4))
/ ((y1 - y2) * (x3 - x4) - (x1 - x2) * (y3 - y4));

return new MyPoint(pointX,pointY);
}
}

实体类MyPoint用来存放我们的标识点坐标

public class MyPoint {
public float x,y;
public MyPoint(){}
public MyPoint(float x, float y){
this.x = x;
this.y = y;
}
}

界面布局:

<?xml version="1.0" encoding="utf-8"?>


<com.anlia.pageturn.BookPageView
android:id=“@+id/view_book_page”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:layout_marginLeft=“15dp”
android:layout_marginTop=“15dp”/>

在Activity中进行注册

bookPageView = (BookPageView) findViewById(R.id.view_book_page);

效果如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x0N8yOka-1651038048886)(https://user-gold-cdn.xitu.io/2017/12/14/16053a51334b380a?imageView2/0/w/1280/h/960/ignore-error/1)]


连接各标识点绘制A、B、C区域

相关博文链接

[Android-贝塞尔曲线](()

[安卓自定义View进阶:Path基本操作](()

[android 自定义view 缓存技术](()

[Android中Canvas绘图之PorterDuffXfermode使用及工作原理详解](()

[Android 自定义View学习(五)——Paint 关于PorterDuffXfermode学习](()

前文我们提到abakdj直线曲线cdb是起点为c,控制点为e,终点为b二阶贝塞尔曲线曲线kij是起点为k,控制点为h,终点为j二阶贝塞尔曲线。通过观察分析得知,区域A是由View左上角左下角曲线cdb, 直线abak曲线kij右上角连接而成的区域,修改BookPageView,利用path绘制处区域A

public class BookPageView extends View {
//省略部分代码…
private Paint pathAPaint;//绘制A区域画笔
private Path pathA;
private Bitmap bitmap;//缓存bitmap
private Canvas bitmapCanvas;

private void init(Context context, @Nullable AttributeSet attrs){
//省略部分代码…
pathAPaint = new Paint();
pathAPaint.setColor(Color.GREEN);
pathAPaint.setAntiAlias(true);//设置抗锯齿

pathA = new Path();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//省略部分代码…
bitmap = Bitmap.createBitmap((int) viewWidth, (int) viewHeight, Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmap);
bitmapCanvas.drawPath(getPathAFromLowerRight(),pathAPaint);
canvas.drawBitmap(bitmap,0,0,null);
}

/**

  • 获取f点在右下角的pathA
  • @return
    */
    private Path getPathAFromLowerRight(){
    pathA.reset();
    pathA.lineTo(0, viewHeight);//移动到左下角
    pathA.lineTo(c.x,c.y);//移动到c点
    pathA.quadTo(e.x,e.y,b.x,b.y);//从c到b画贝塞尔曲线,控制点为e
    pathA.lineTo(a.x,a.y);//移动到a点
    pathA.lineTo(k.x,k.y);//移动到k点
    pathA.quadTo(h.x,h.y,j.x,j.y);//从k到j画贝塞尔曲线,控制点为h
    pathA.lineTo(viewWidth,0);//移动到右上角
    pathA.close();//闭合区域
    return pathA;
    }
    }

效果如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q0ip4Bgk-1651038048888)(https://user-gold-cdn.xitu.io/2017/12/14/16053a51342e05a9?imageView2/0/w/1280/h/960/ignore-error/1)]

区域C理论上应该是由点a,b,d,i,k连接而成的闭合区域,但由于di是曲线上的点,我们没办法直接从d出发通过path绘制路径连接b点(i,k同理),也就不能只用path的情况下直接绘制出区域C,我们需要用PorterDuffXfermode方面的知识“曲线救国”。我们试着先将点a,b,d,i,k连接起来,观察闭合区域与区域A之间的联系。修改BookPageView

private void init(Context context, @Nullable AttributeSet attrs){
//省略部分代码…
pathCPaint = new Paint();
pathCPaint.setColor(Color.YELLOW);
pathCPaint.setAntiAlias(true);//设置抗锯齿

pathC = new Path();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

bitmap = Bitmap.createBitmap((int) viewWidth, (int) viewHeight, Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmap);
bitmapCanvas.drawPath(getPathAFromLowerRight(),pathAPaint);
bitmapCanvas.drawPath(getPathC(),pathCPaint);
canvas.drawBitmap(bitmap,0,0,null);
}

/**

  • 绘制区域C
  • @return
    */
    private Path getPathC(){
    pathC.reset();
    pathC.moveTo(i.x,i.y);//移动到i点
    pathC.lineTo(d.x,d.y);//移动到d点
    pathC.lineTo(b.x,b.y);//移动到b点
    pathC.lineTo(a.x,a.y);//移动到a点
    pathC.lineTo(k.x,k.y);//移动到k点
    pathC.close();//闭合区域
    return pathC;
    }

效果如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WlMY2CS4-1651038048888)(https://user-gold-cdn.xitu.io/2017/12/14/16053a51328cc528?imageView2/0/w/1280/h/960/ignore-error/1)]

我们将两条曲线也画出来对比观察

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sp8r8fRd-1651038048889)(https://user-gold-cdn.xitu.io/2017/12/14/16053a5158cd3dc7?imageView2/0/w/1280/h/960/ignore-error/1)]

观察分析后可以得出结论,区域C由直线ab,bd,dj,ik,ak连接而成的区域 减去 与区域A交集部分 后剩余的区域。于是我们设置区域C画笔Xfermode模式为DST_ATOP

pathCPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP));

效果如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-936jq5BE-1651038048889)(https://user-gold-cdn.xitu.io/2017/12/14/16053a515892fd66?imageView2/0/w/1280/h/960/ignore-error/1)]

最后是区域B,因为区域B处于最底层,我们直接将区域B画笔Xfermode模式设为DST_ATOP,在区域A、C之后绘制即可,修改BookPageView

private void init(Context context, @Nullable AttributeSet attrs){
//省略部分代码…
pathBPaint = new Paint();
pathBPaint.setColor(getResources().getColor(R.color.blue_light));
pathBPaint.setAntiAlias(true);//设置抗锯齿
pathBPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP));

pathB = new Path();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//省略部分代码…
bitmap = Bitmap.createBitmap((int) viewWidth, (int) viewHeight, Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmap);
bitmapCanvas.drawPath(getPathAFromLowerRight(),pathAPaint);
bitmapCanvas.drawPath(getPathC(),pathCPaint);
bitmapCanvas.drawPath(getPathB(),pathBPaint);
canvas.drawBitmap(bitmap,0,0,null);
}

/**

  • 绘制区域B
  • @return
    */
    private Path getPathB(){
    pathB.reset();
    pathB.lineTo(0, viewHeight);//移动到左下角
    pathB.lineTo(viewWidth,viewHeight);//移动到右下角
    pathB.lineTo(viewWidth,0);//移动到右上角
    pathB.close();//闭合区域
    return pathB;
    }

pathCPaint);
bitmapCanvas.drawPath(getPathB(),pathBPaint);
canvas.drawBitmap(bitmap,0,0,null);
}

/**

  • 绘制区域B
  • @return
    */
    private Path getPathB(){
    pathB.reset();
    pathB.lineTo(0, viewHeight);//移动到左下角
    pathB.lineTo(viewWidth,viewHeight);//移动到右下角
    pathB.lineTo(viewWidth,0);//移动到右上角
    pathB.close();//闭合区域
    return pathB;
    }

版权声明:本文为m0_70082703原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。