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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android自定义View——从零开始实现书籍翻页效果(一)相关的知识,希望对你有一定的参考价值。

参考技术A

前言 :本篇是系列博客的第三篇,这次我们要研究 书籍翻页效果 。不知道大家平时有没用过iReader、掌阅这些小说软件,里面的翻页效果感觉十分的酷炫。有心想研究研究如何实现,于是网上找了找,发现这方面的教学资料非常少,所幸能找到 何明桂大大 的 android 实现书籍翻页效果----原理篇 这样的入门博客(感谢大大 Orz),我们就以这篇博客为切入点从零实现我们自己的翻页效果。由于这次坑比较深,预计会写好几期,感兴趣的小伙伴可以点下关注以便及时收到更新提醒,谢谢大家的支持 ~

本篇只着重于思路和实现步骤,里面用到的一些知识原理不会非常细地拿来讲,如果有不清楚的api或方法可以在网上搜下相应的资料,肯定有大神讲得非常清楚的,我这就不献丑了。本着认真负责的精神我会把相关知识的博文链接也贴出来(其实就是懒不想写那么多哈哈),大家可以自行传送。为了照顾第一次阅读系列博客的小伙伴,本篇会出现一些在之前 系列博客 就讲过的内容,看过的童鞋自行跳过该段即可

国际惯例,先上效果图,本次主要实现了 基本的上下翻页效果 右侧最大翻页距离的限制

在看这篇博客之前,希望大家能先了解一下书籍翻页的实现原理,博客链接我已经贴出来了。通过原理讲解我们知道,整个书籍翻页效果界面分成了三个区域, A 为当前页区域, B 为下一页区域, C 为当前页背面,如图所示

书籍翻页效果的实现就是要以我们 触摸屏幕位置的坐标 为基础绘制出这三个区域,形成模拟翻页的特效。要绘制这三个区域,我们需要通过一组 特定的点 来完成,这些点的坐标需要通过两个已知的点( 触摸点 相对边缘角 )计算得到,下图我将各个特定点的位置和计算公式贴出来,大家对照着原理一起理解(渣画工望体谅 ╮(╯▽╰)╭ ),其中 b 点是由 ae cj 的交点, k 点是由 ah cj 的交点

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

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

界面布局:

在Activity中进行注册

效果如图

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

效果如图

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

效果如图

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

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

效果如图

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

效果如图

翻页可以从右下方翻自然也可以从右上方翻,我们将 f 点设在右上角,由于View上下两部分是呈 镜像 的,所以各标识点的位置也应该是镜像对应的,因为 区域B和C 的绘制与 f 点没有关系,所以我们只需要修改 区域A 的绘制逻辑,新增 getPathAFromTopRight() 方法

效果如图

之前由于测试效果没有对View的大小进行重新测量,在实现触摸翻页之前先把这个结了。重写View的 onMeasure() 方法

我们的需求是,在上半部分翻页时 f 点在右上角,在下半部分翻页时 f 则在右下角,当手指离开屏幕时回到 初始状态 ,根据需求,修改 BookPageView

在Activity中监听View的 onTouch 状态

注意,要设置 android:clickable true ,否则无法监听到 ACTION_MOVE ACTION_UP 状态

效果如图

到这里我们已经实现了基本的翻页效果,但要还原真实的书籍翻页效果,我们还需要设置一些限制条件来完善我们的项目

对于一般的书本来说,最左侧应该是钉起来的,也就是说如果我们从右侧翻页,翻动的距离是 有限制的 ,最下方翻页形成的曲线起点( c 点)的x坐标不能小于0(上方同理),按照这个限定条件,修改我们的 BookPageView

效果如图

至此本篇教程就告一段落了,当然还有许多功能需要继续完善,例如横向翻页、翻页动画、阴影效果等等,这些都会在后面的教程中一一解决。如果大家看了感觉还不错麻烦点个赞,你们的支持是我最大的动力~

Android自定义View初步

有关使用Android如何设计出有个性的界面,按照本人估计,除了遵循google的设计规范,就只能使用自定义View这个最灵活的方式了,这几天找了些资料学习自定义View,但是学习android developer文档中自定义的View比较麻烦,又找了些比较简单的材料,结合自己对CustomView这个实例的理解,开始学习自定义View。

下面实现一个类似时钟/仪表盘的简单界面,通过绘制一个圆来实现,这个圆周围有标的刻度,同时在每五个位置上绘制一个比其他刻度线长的刻度,然后再绘制一个类似的表针。我来一步一步实现这个界面,同时我会介绍如何让View自适应界面,在LinearLayout中添加多个此View的方法(暂时不了解ViewGroup原理,所以解决方式可能比较麻烦)。

首先上图,分别显示在横屏和宽屏下的效果:

技术分享

技术分享

 

 

对于这个自定义View,由于暂时不打算加入监听事件,所以只是使用XML进行静态定义,没有监听事件。

我按照步骤来:

  1. 定义一个自定义View类,继承自View类,同时注意必须使用构造方法,在构造方法中首先调用超类的构造方法:

技术分享

同时注意,我无法添加IDE提示的第四种被重载的方法,就是四个参数的那个构造方法,是因为四个参数版本支持的API是21+。

 

2.下面实现的就是如何定义一些通过XML定义的参数,比如我们在XML布局下使用的id,layout_width,layout_height等等参数是如何定义的?我们就实现这种类型参数的定义。

为此,我定义一个专门用来保存这个名为attr资源的文件,按照标准,命名为attrs.xml

技术分享

,然后在这个文件中定义一个针对我们自定义View的可用参数名的表,我为了这个View定义的是笔触的颜色border_color,还有笔触的宽度border_width。(为什么取这个名字呢,因为这是参考一个名为CircleImageView的时候定义的参数,但是暂时没实现出来,而是只学习了一部分,所以我没再改过)

技术分享

 

因为这个参数要求整体唯一,所以如果你定义的一些attr在你引入的某些库中已经存在,会出现错误。

 

3.定义好了可选参数之后,不着急如何在XML布局中使用,先要知道如何在类中使用他们,我们在构造方法中获取并赋给一些自定义View中变量。按照官方文档的做法,需要在两个参数的构造方法中获取他们:

技术分享

技术分享

这样,我们就可以在XML布局中设置参数值,然后在此处会被获取。

 

4.我这个自定义View做重要的工作,我觉得应当是在onDraw和onSizeChanged方法中,下面介绍这两个重写的方法作用:

  onDraw(Canvas canvas)方法

这个方法是用来绘制View这个Canvas用的重要方法,所有canvas绘图的方法都适用于此,在此介绍一个介绍这个的文章,

地址在此:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2012/1212/703.html

 

  onSizeChanged()

这个方法在界面的尺寸更改的时候会被调用,一般是在屏幕旋转的时候会被调用,有两个新w/h和旧w/h会被传入,这里我用来实现初始化绘图的时候边框(RecF类型,具体见Canvas绘图中的Oval和矩形绘制类)等。

 

5.说完了两个方法的重要性,开始界面绘制的重要工作,因为我们绘制的都是比较简单的线条,我们只需要一个画笔,同时为了方便使用整个View的界面中的一部分,我使用了一个RecF来定义一块矩形区域。绘制圆形使用的radius,每个标度使用Line来绘制,同时表针使用的也是Line。。。这个部分可以自行决定何种算法绘制,这里介绍直接计算的方法,另一种方式在上面介绍的Canvas的文章中介绍过,就是使用旋转进行绘制,下面是我的主要实现

1)首先初始化画笔

自定义一个init方法用来在三个构造方法中都初始化画笔:

技术分享

技术分享

 

2)初始化全局参数,主要是在onSizeChanged()中,这是为了自适应界面,每次界面变化的时候都会被调用来重新计算一些参数:

技术分享

 

3)在onDraw中绘制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(0xff000000);
        mPaint.setColor(0x66555555);
        canvas.drawRoundRect(new RectF(mBounds.centerX()-(float)0.9*width/2, mBounds.centerY() - (float)0.9*height/2, mBounds.centerX() + (float)0.9*width/2, mBounds.centerY() + (float)0.9*height/2), 30, 30, mPaint);
        mPaint.setColor(mBorderColor);
        canvas.drawCircle(mBounds.centerX(),mBounds.centerY(),radius,mPaint);
        float start_x,start_y;
        float end_x,end_y;
        for(int i=0;i<60;++i){
            start_x= radius *(float)Math.cos(Math.PI/180 * i * 6);
            start_y= radius *(float)Math.sin(Math.PI/180 * i * 6);
            if(i%5==0){
                end_x = start_x+largeLength*(float)Math.cos(Math.PI / 180 * i * 6);
                end_y = start_y+largeLength*(float)Math.sin(Math.PI/180 * i * 6);
            }else{
                end_x = start_x+smallLength*(float)Math.cos(Math.PI/180 * i * 6);
                end_y = start_y+smallLength*(float)Math.sin(Math.PI/180 * i * 6);
            }
            start_x+=mBounds.centerX();
            end_x+=mBounds.centerX();
            start_y+=mBounds.centerY();
            end_y+=mBounds.centerY();
            canvas.drawLine(start_x, start_y, end_x, end_y, mPaint);
        }
        canvas.drawCircle(mBounds.centerX(),mBounds.centerY(),20,mPaint);
        canvas.rotate(60,mBounds.centerX(),mBounds.centerY());
        canvas.drawLine(mBounds.centerX(),mBounds.centerY(),mBounds.centerX(),mBounds.centerY()-radius,mPaint);
    }

这样我们就可以在XML布局中使用了,比如这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<LinearLayout
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:orientation="vertical"
 
    <com.example.androidviewtest.view.CustomView
        android:id="@+id/view1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:border_color="#ff1ff41f"
        app:border_width="2dp"/>
 
</LinearLayout>

显示的是:

技术分享

 

 

6.解决多个LinearLayout下使用这个自定义View的方法。

在上面例子中,我们使用的虽然也是使用LinearLayout,但是在其中插入多个这样的自定义View的时候会出现问题:

技术分享

 

我的解决方法是将自定义View用FrameLayout包装起来,代码如下:

 

结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<LinearLayout
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:orientation="vertical"
 
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">
        <com.example.androidviewtest.view.CustomView
            android:id="@+id/view1"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:border_color="#ff1ff41f"
            app:border_width="2dp"/>
    </FrameLayout>
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">
        <com.example.androidviewtest.view.CustomView
            android:id="@+id/view2"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:border_color="#ff2523f4"
            app:border_width="2dp"/>
    </FrameLayout>
</LinearLayout>


技术分享

 

这样我们就解决了这个不显示的问题,据了解我觉得可能是因为LinearLayout的实现中,结合我这个自定义View会有问题,这个问题跟我View中的某些应当定义的重写方法(大胆猜测是onMeasure)有关

 

上述代码运行在模拟器上,横屏和竖屏的结果类似开头的图。

 

整个View的所有代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package com.example.androidviewtest.view;
 
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
 
import com.example.androidviewtest.R;
 
public class CustomView extends View {
 
    private float mBorderWidth;
    private int mBorderColor;
 
    private Paint mPaint;
 
    private RectF mBounds;
    private float width;
    private float height;
    float radius;
    float smallLength;
    float largeLength;
 
    public CustomView(Context context) {
        super(context);
        init();
    }
 
    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
 
        TypedArray typedArray = context.getTheme()
                .obtainStyledAttributes(
                        attrs,
                        R.styleable.CustomView
                        , 0, 0);
 
        try{
            mBorderColor = typedArray.getColor(R.styleable.CustomView_border_color,0xff000000);
            mBorderWidth = typedArray.getDimension(R.styleable.CustomView_border_width,2);
        }finally {
            typedArray.recycle();
        }
 
        init();
    }
 
    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
 
    private void init(){
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mBorderWidth);
        mPaint.setColor(mBorderColor);
    }
 
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
 
        mBounds = new RectF(getLeft(),getTop(),getRight(),getBottom());
 
        width = mBounds.right - mBounds.left;
        height = mBounds.bottom - mBounds.top;
 
        if(width<height){
            radius = width/4;
        }else{
            radius = height/4;
        }
 
        smallLength =10;
        largeLength=20;
    }
 
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(0xff000000);
        mPaint.setColor(0x66555555);
        canvas.drawRoundRect(new RectF(mBounds.centerX()-(float)0.9*width/2, mBounds.centerY() - (float)0.9*height/2, mBounds.centerX() + (float)0.9*width/2, mBounds.centerY() + (float)0.9*height/2), 30, 30, mPaint);
        mPaint.setColor(mBorderColor);
        canvas.drawCircle(mBounds.centerX(),mBounds.centerY(),radius,mPaint);
        float start_x,start_y;
        float end_x,end_y;
        for(int i=0;i<60;++i){
            start_x= radius *(float)Math.cos(Math.PI/180 * i * 6);
            start_y= radius *(float)Math.sin(Math.PI/180 * i * 6);
            if(i%5==0){
                end_x = start_x+largeLength*(float)Math.cos(Math.PI / 180 * i * 6);
                end_y = start_y+largeLength*(float)Math.sin(Math.PI/180 * i * 6);
            }else{
                end_x = start_x+smallLength*(float)Math.cos(Math.PI/180 * i * 6);
                end_y = start_y+smallLength*(float)Math.sin(Math.PI/180 * i * 6);
            }
            start_x+=mBounds.centerX();
            end_x+=mBounds.centerX();
            start_y+=mBounds.centerY();
            end_y+=mBounds.centerY();
            canvas.drawLine(start_x, start_y, end_x, end_y, mPaint);
        }
        canvas.drawCircle(mBounds.centerX(),mBounds.centerY(),20,mPaint);
        canvas.rotate(60,mBounds.centerX(),mBounds.centerY());
        canvas.drawLine(mBounds.centerX(),mBounds.centerY(),mBounds.centerX(),mBounds.centerY()-radius,mPaint);
    }
}

以上是关于Android自定义View——从零开始实现书籍翻页效果(一)的主要内容,如果未能解决你的问题,请参考以下文章

Android自定义View—仿雷达扫描效果

可翻转移到的自定义卡片

可翻转移到的自定义卡片

android自定义View之钟表诞生记

Android自定义View初步

Android进阶之绘制-自定义View完全掌握