首先贴个链接:https://www.jianshu.com/p/d02362fbd9f2
本文是在该博主的基础上优化的,建议先阅读。非常感谢博主。
实现效果: 仿真书籍的翻书效果。
过程: 本来是想在网上找个的控件,能实现仿真书籍翻页就可以了,但是找了很多,要么太冗杂,要么使用效果不流畅,特耗内存。中间也参考了一些博主的文章,不是很理想,上面贴出的链接完整的介绍了编码书籍翻书的基本原理,可以了解一下。
开始: 先上图
上图是最终的效果,整个翻书效果用一个自定义控件实现,上一页,下一页,初始化等。
首页要明白整个翻书的效果是由三个部分组成的。如下
我们把它分为A、B、C 三个区域,翻书的过程其实就是不断改变A、B、C的显示区域。可以比喻为三张矩形的纸张,然后每一张剪切相应的位置,组合在一起,不断改变,最后形成了翻书的效果。
所以翻书效果最主要是如何计算每个区域的显示大小、曲线、阴影等等。需要我们掌握自定义View的相关知识,比如曲线绘制,翻页动画、阴影。
必备知识点:
1 自定义View中曲线的绘制(二阶贝塞尔曲线),绘制流程。
2 相关的数学基本公式,(园,椭圆,曲线方程)
3 自定义View Matrix原理、区域裁剪
在文字开始贴出的链接:《Android自定义View——从零开始实现书籍翻页效果》系列文章中,已经比较清楚的介绍了整体的翻书效果,性能优化,不过并不能完全使用(只介绍了翻页的原理。并没有完成整个翻页的动画)。本文将在其基础上,加上完整的翻页效果。建议先阅读 Android自定义View——从零开始实现书籍翻页效果
上图中说到的A、B、C其实就代表着当前页,当前页的背面,以及下一页。在绘制时,一定要先准备这三个区域的Bitmap,然后在画上去。我们分别了解下。
其中 pageNum 代表当前绘制的是第几页,bitmaps 代表的是整个翻书控件总的页数
第二个方法是画当前页的背景,本来是需要画当前页的背面,但由于翻页中背面 bitmap 旋转反转之后,在绘制截取曲线时存在问题,所以这里用一个纯色的 Bitmap 来当作背景。
第三个方法同第一个方法一样。下面我们来看看 onMeasure()方法
在测量的时候就将三个页面画上去。当作第一次的初始化,当然显示的时候只是显示第一个页面。
然后看看 onTouchEvent() 方法
@Override
public boolean onTouchEvent(MotionEvent event) {
this.event = event;
super.onTouchEvent(event);
if(pageState == PAGE_STAY) {
isSongs = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
float x = event.getX();
float y = event.getY();
// 1 在按下的时候绘制。有一种情况不用绘制 :当前页是最后一页,就不用绘制了
if (x <= viewWidth / 3) {//左
style = STYLE_LEFT;
style_ectype = STYLE_LEFT;
if(pageNum <= 0) { //说明这就是第一页,不能在往上翻页了
// Toast.makeText(getContext(),"已经是第一页了",Toast.LENGTH_SHORT).show();
}else{
setTouchPoint(x, y, style);
}
pageNum -- ;
if(pageNum>=0) {
drawableCurrtPage(pathAContentBitmap, pathAPaint);
drawableNextPage(pathBContentBitmap, pathBPaint);
drawableBackgoundPage(pathCContentBitmap,pathCPaint);
// drawableCurrtPage(pathCContentBitmap, pathCPaint);
}
} else if (x > viewWidth / 3 && y <= viewHeight / 3) {//上
drawableCurrtPage(pathAContentBitmap, pathAPaint);
drawableNextPage(pathBContentBitmap, pathBPaint);
drawableBackgoundPage(pathCContentBitmap,pathCPaint);
// drawableCurrtPage(pathCContentBitmap, pathCPaint);
style = STYLE_TOP_RIGHT;
style_ectype = STYLE_TOP_RIGHT;
pageNum++;
if(pageNum >= bitmaps.size() ){//说明已经是最后一页了
}else{
setTouchPoint(x, y, style);
}
} else if (x > viewWidth * 2 / 3 && y > viewHeight / 3 && y <= viewHeight * 2 / 3) {//右
drawableCurrtPage(pathAContentBitmap, pathAPaint);
drawableNextPage(pathBContentBitmap, pathBPaint);
drawableBackgoundPage(pathCContentBitmap,pathCPaint);
// drawableCurrtPage(pathCContentBitmap, pathCPaint);
style = STYLE_RIGHT;
style_ectype = STYLE_RIGHT;
pageNum++;
if(pageNum >= bitmaps.size() ){//说明已经是最后一页了
}else{
setTouchPoint(x, y, style);
}
/* if(listener!=null){
listener.getFocus();
}*/
} else if (x > viewWidth / 3 && y > viewHeight * 2 / 3) {//下
drawableCurrtPage(pathAContentBitmap, pathAPaint);
drawableNextPage(pathBContentBitmap, pathBPaint);
drawableBackgoundPage(pathCContentBitmap,pathCPaint);
// drawableCurrtPage(pathCContentBitmap, pathCPaint);
style = STYLE_LOWER_RIGHT;
style_ectype = STYLE_LOWER_RIGHT;
pageNum++;
if(pageNum >= bitmaps.size() ){//说明已经是最后一页了
}else{
setTouchPoint(x, y, style);
}
/* if(listener!=null){
listener.getFocus();
}*/
} else if (x > viewWidth / 3 && x < viewWidth * 2 / 3 && y > viewHeight / 3 && y < viewHeight * 2 / 3) {//中
style = STYLE_MIDDLE;
style_ectype = STYLE_MIDDLE;
//在这里呼出菜单
if(listener!=null){
listener.onCilckCentreMenu();
}
}
break;
case MotionEvent.ACTION_MOVE:
if(pageNum < 0){ //说明这就是第一页
}else if(pageNum == bitmaps.size()){//说明这就是最后一页
}else{
setTouchPoint(event.getX(), event.getY(), style);
}
break;
case MotionEvent.ACTION_UP:
isSongs = true;
if(pageNum < 0){ //说明这就是第一页
pageNum = 0;
}else if(pageNum == bitmaps.size()){
pageNum = bitmaps.size() -1;
}else{
if (style == STYLE_LEFT) {
calcPointsXY(testa, f); //用来调整翻页翻过去的贝塞尔曲线的限制
startlPreviousAnim();
} else if(style == STYLE_MIDDLE ){
pageState = PAGE_STAY;
}else{
calcPointsXY(testa, f);
startNextAnim();
}
}
break;
}
}
// postInvalidate();
return true;
}
这里是点击不同的区域然后有不同的操作,见下图
当我们点击某个区域后,就要执行翻页的动画,就需要调用该方法完成整个动作。即上一页下一页的两种动画
public void startlPreviousAnim(){
pageState = PAGE_PREVIOUS;
int dx,dy;
//让a滑动到f点所在位置,留出1像素是为了防止当a和f重叠时出现View闪烁的情况
if(style.equals(STYLE_TOP_RIGHT)){
dx = (int) (viewWidth-1-a.x);
dy = (int) (1-a.y);
}else {
dx = (int) (viewWidth-1-a.x);
dy = (int) (viewHeight-1-a.y);
}
mScroller.startScroll((int) a.x, (int) a.y, dx, dy, 350);
postInvalidate();
}
public void startNextAnim(){
pageState = PAGE_NEXT;
int dx = viewWidth,dy;
//让a滑动到f点所在位置,留出1像素是为了防止当a和f重叠时出现View闪烁的情况
if(style.equals(STYLE_TOP_RIGHT)){
dy = (int) (1-a.y);
if(a.x<= viewWidth/2){
//计算 翻页的 a 点的横坐标
dx = (int) -(viewWidth+a.x/2);
mScroller.startScroll((int) a.x, (int) a.y, dx, dy, 350);
}else if( a.x> viewWidth/2){
//计算 翻页的 a 点的横坐标
dx = (int) -(2*a.x+a.x/5);
mScroller.startScroll((int) a.x, (int) a.y, dx, dy, 350);
}
}else {
dy = (int) (viewHeight-1-a.y);
if(a.x<= viewWidth/2){
dx = (int) -(viewWidth+a.x/2);
mScroller.startScroll((int) a.x, (int) a.y, dx, dy, 350);
}else if( a.x> viewWidth/2){
dx = (int) -(2*a.x+a.x/5);
mScroller.startScroll((int) a.x, (int) a.y, dx, dy, 350);
}
}
postInvalidate();
}
这样 基本就能够完成整个的翻页效果了。
这上面有两个问题没有很好的解决:
1 : 当 C 页面的 Bitmap 不为纯色时,即为图片的 bitmap 在翻页的时候 C 区域的底部和右部会有白边或者黑边出现,主要的原因是,对 C 区域的Bitmap 并没有做曲边处理。相关解决方案可参加这篇文章 Android翻页效果原理实现之模拟扭曲
2 : 点击不同区域执行翻页时,翻页的时间不固定。这是因为我设置好滚动的终点坐标不一样,而代码中的滚动时间确是一样照成的,参见上面的代码,我设置的时间是 350 。而时间中应该根据终点的位置来计算设置时间。有时间我在优化发出来。
通过以上就能基本实现翻书的效果了。但是这其中还有很对优化的地方。后面我会不断完善。
有不明白的,写得不对的,欢迎各位指出讨论,共同进步。