Android自定义View——从零开始实现覆盖翻页效果
来源:互联网 发布:log4j.xml输出完整sql 编辑:程序博客网 时间:2024/06/05 02:12
版权声明:本文为博主原创文章,未经博主允许不得转载。
系列教程:Android开发之从零开始系列
源码:github.com/AnliaLee/BookPage,欢迎star大家要是看到有错误的地方或者有啥好的建议,欢迎留言评论
前言:之前讲了仿真书籍翻页效果,效果如图
我们从原理分析、功能实现到性能优化完整地过了一遍,反响不错,于是有小伙伴私信让我把 覆盖翻页效果也讲了,所以这期的主角就是它了 ~
本篇只着重于思路和实现步骤,里面用到的一些知识原理不会非常细地拿来讲,如果有不清楚的api或方法可以在网上搜下相应的资料,肯定有大神讲得非常清楚的,我这就不献丑了。本着认真负责的精神我会把相关知识的博文链接也贴出来(其实就是懒不想写那么多哈哈),大家可以自行传送。为了照顾第一次阅读系列博客的小伙伴,本篇可能会出现一些在之前系列博客就讲过的内容,看过的童鞋自行跳过该段即可
国际惯例,先上效果图
目录
- 创建页面内容工厂类
- 使用工厂类获取页面内容并绘制
- 实现页面滑动效果
- 实现上下翻页
- 绘制页面阴影
创建页面内容工厂类
在Android自定义View——从零开始实现书籍翻页效果(三)一文中提到了向View填充内容实际上就是将所有页面元素绘制到一个bitmap上,然后再将这个bitmap绘制到View中。我们把绘制页面内容bitmap的过程封装起来,方便用户调用,创建PageFactory抽象类,在内部实现绘制页面内容的抽象方法
public abstract class PageFactory { public boolean hasData = false;//是否含有数据 public int pageTotal = 0;//页面总数 public PageFactory(){} /** * 绘制上一页bitmap * @param bitmap * @param pageNum */ public abstract void drawPreviousBitmap(Bitmap bitmap, int pageNum); /** * 绘制当前页bitmap * @param bitmap * @param pageNum */ public abstract void drawCurrentBitmap(Bitmap bitmap, int pageNum); /** * 绘制下一页bitmap * @param bitmap * @param pageNum */ public abstract void drawNextBitmap(Bitmap bitmap, int pageNum); /** * 通过索引在集合中获取相应内容 * @param index * @return */ public abstract Bitmap getBitmapByIndex(int index);}
我们以纯图像内容的绘制为例,创建PicturesPageFactory继承PageFactory,除了实现内容绘制的具体逻辑以外,设置多种初始化方法,方便用户使用不同路径下的图像集合
public class PicturesPageFactory extends PageFactory { private Context context; public int style;//集合类型 public final static int STYLE_IDS = 1;//drawable目录图片集合类型 public final static int STYLE_URIS = 2;//手机本地目录图片集合类型 private int[] picturesIds; /** * 初始化drawable目录下的图片id集合 * @param context * @param pictureIds */ public PicturesPageFactory(Context context, int[] pictureIds){ this.context = context; this.picturesIds = pictureIds; this.style = STYLE_IDS; if (pictureIds.length > 0){ hasData = true; pageTotal = pictureIds.length; } } private String[] picturesUris; /** * 初始化本地目录下的图片uri集合 * @param context * @param picturesUris */ public PicturesPageFactory(Context context, String[] picturesUris){ this.context = context; this.picturesUris = picturesUris; this.style = STYLE_URIS; if (picturesUris.length > 0){ hasData = true; pageTotal = picturesUris.length; } } @Override public void drawPreviousBitmap(Bitmap bitmap, int pageNum) { Canvas canvas = new Canvas(bitmap); canvas.drawBitmap(getBitmapByIndex(pageNum-2),0,0,null); } @Override public void drawCurrentBitmap(Bitmap bitmap, int pageNum) { Canvas canvas = new Canvas(bitmap); canvas.drawBitmap(getBitmapByIndex(pageNum-1),0,0,null); } @Override public void drawNextBitmap(Bitmap bitmap, int pageNum) { Canvas canvas = new Canvas(bitmap); canvas.drawBitmap(getBitmapByIndex(pageNum),0,0,null); } @Override public Bitmap getBitmapByIndex(int index) { if(hasData){ switch (style){ case STYLE_IDS: return getBitmapFromIds(index); case STYLE_URIS: return getBitmapFromUris(index); default: return null; } }else { return null; } } /** * 从id集合获取bitmap * @param index * @return */ private Bitmap getBitmapFromIds(int index){ return BitmapUtils.drawableToBitmap( context.getResources().getDrawable(picturesIds[index]), ScreenUtils.getScreenWidth(context), ScreenUtils.getScreenHeight(context) ); } /** * 从uri集合获取bitmap * @param index * @return */ private Bitmap getBitmapFromUris(int index){ return null;//这个有空再写啦,大家可自行补充完整 }}
基本架构就是这样(BitmapUtils和ScreenUtils两个工具类大家自己去看下源码吧,就不在这展开说了~),至于小说文本类的解析比较复杂,以后可能会出一个番外篇专门讲这个。下面我们开始介绍如何在自定义View中使用这个工厂类
使用工厂类获取页面内容并绘制
创建CoverPageView,提供一个对外的接口用以设置工厂类
public class CoverPageView extends View { private int defaultWidth;//默认宽度 private int defaultHeight;//默认高度 private int viewWidth; private int viewHeight; private int pageNum;//当前页数 private PageFactory pageFactory; private Bitmap currentPage;//当前页bitmap public CoverPageView(Context context) { super(context); init(context); } public CoverPageView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context); } private void init(Context context){ defaultWidth = 600; defaultHeight = 1000; pageNum = 1; } /** * 设置工厂类 * @param factory */ public void setPageFactory(final PageFactory factory){ //保证View已经完成了测量工作,各页bitmap已初始化 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { getViewTreeObserver().removeOnPreDrawListener(this); if(factory.hasData){ pageFactory = factory; pageFactory.drawCurrentBitmap(currentPage,pageNum); postInvalidate(); } return true; } }); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int height = ViewUtils.measureSize(defaultHeight, heightMeasureSpec); int width = ViewUtils.measureSize(defaultWidth, widthMeasureSpec); setMeasuredDimension(width, height); viewWidth = width; viewHeight = height; currentPage = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.RGB_565); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if(pageFactory !=null){ drawCurrentPage(canvas); } } /** * 绘制当前页 * @param canvas */ private void drawCurrentPage(Canvas canvas){ canvas.drawBitmap(currentPage, 0, 0,null); }}
在Activity中进行初始化,这里我用了drawable目录下的一些图片作为页面内容
int[] pIds = new int[]{R.drawable.test1,R.drawable.test2,R.drawable.test3};coverPageView = (CoverPageView) findViewById(R.id.view_cover_page);coverPageView.setPageFactory(new PicturesPageFactory(this,pIds));
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:splitMotionEvents="false"> <com.anlia.pageturn.view.CoverPageView android:id="@+id/view_cover_page" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="10dp"/></RelativeLayout>
CoverPageView设置了工厂类对象后便会绘制出当前页内容,效果如图
实现页面滑动效果
页面滑动效果的原理其实很简单,之前我们调用了canvas.drawBitmap方法将当前页内容绘制到View中,要实现页面滑动,只需要设置drawBitmap方法中的left值(bitmap的左边界值)即可。也就是说,我们可以通过记录手指在X轴上的滑动距离,计算出left值,从而改变当前页内容bitmap的起始位置,实现滑动效果,如图
修改CoverPageView,监听触摸事件
public class CoverPageView extends View { //省略部分代码... private float xDown;//记录初始触摸的x坐标 private float scrollPageLeft;//滑动页左边界 private MyPoint touchPoint;//触摸点 private Bitmap nextPage;//下一页bitmap private int touchStyle;//触摸类型 public static final int TOUCH_MIDDLE = 0;//点击中间区域 public static final int TOUCH_LEFT = 1;//点击左边区域 public static final int TOUCH_RIGHT = 2;//点击右边区域 private void init(Context context){ //省略部分代码... scrollPageLeft = 0; touchStyle = TOUCH_RIGHT; touchPoint = new MyPoint(-1,-1); } /** * 设置工厂类 * @param factory */ public void setPageFactory(final PageFactory factory){ 记得使用pageFactory.drawNextBitmap(nextPage,pageNum)绘制下一页的内容,不然滑动当前页时会出现背景空白没有内容 } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if(pageFactory !=null){ if(touchPoint.x ==-1 && touchPoint.y ==-1){ drawCurrentPage(canvas); }else{ drawNextPage(canvas); drawCurrentPage(canvas); } } } /** * 绘制当前页 * @param canvas */ private void drawCurrentPage(Canvas canvas){ canvas.drawBitmap(currentPage, scrollPageLeft, 0,null);//修改left值 } /** * 绘制下一页 * @param canvas */ private void drawNextPage(Canvas canvas){ canvas.drawBitmap(nextPage, 0, 0, null); } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); float x = event.getX(); float y = event.getY(); switch (event.getAction()){ case MotionEvent.ACTION_DOWN: xDown = x; if(x<=viewWidth/3){//左 touchStyle = TOUCH_LEFT; }else if(x>viewWidth*2/3){//右 touchStyle = TOUCH_RIGHT; }else if(x>viewWidth/3 && x<viewWidth*2/3){//中 touchStyle = TOUCH_MIDDLE; } break; case MotionEvent.ACTION_MOVE: scrollPage(x,y); break; case MotionEvent.ACTION_UP: break; } return true; } /** * 计算滑动页面左边界位置,实现滑动当前页效果 * @param x * @param y */ private void scrollPage(float x, float y){ touchPoint.x = x; touchPoint.y = y; if(touchStyle == TOUCH_RIGHT){ scrollPageLeft = touchPoint.x - xDown; }else if(touchStyle == TOUCH_LEFT){ scrollPageLeft =touchPoint.x - xDown - viewWidth; } if(scrollPageLeft > 0){ scrollPageLeft = 0; } postInvalidate(); }}
效果如图
实现上下翻页
相关博文链接
android scroller类的使用
Android学习之 Scroller的介绍与使用
Android Scroller完全解析,关于Scroller你所需知道的一切
Android – Interpolator
android动画 之Interpolator类
要实现上下翻页效果我们需从两个方面入手,一是使用scroller和Interpolator插值器方面的知识完成自动翻页的效果;二是在恰当的时机更新上页、当前页、下页的内容,使得整个翻页衔接更为流畅
先说第一点,自动翻到上页和下页区别在于页面滑动的方向不同,我们以滑动页的右边界(因为左边界在View的范围之外,所以选取右边界作为参考,方便大家理解)的位置变化为例,翻到上页时上一页的内容右边界从左向右滑动,逐渐覆盖当前页内容,而翻到下页时,则是当前页内容右边界从右向左滑动,逐渐显示出下页内容,具体计算的方法如下
/** * 自动完成翻到下一页操作 */private void autoScrollToNextPage(){ pageState = PAGE_NEXT; int dx,dy; dx = (int) -(viewWidth+scrollPageLeft); dy = (int) (touchPoint.y); int time =(int) ((1+scrollPageLeft/viewWidth) * scrollTime);//按已滑动的距离占比计算实际的动画时间 mScroller.startScroll((int) (viewWidth+scrollPageLeft), (int) touchPoint.y, dx, dy, time);}/** * 自动完成返回上一页操作 */private void autoScrollToPreviousPage(){ pageState = PAGE_PREVIOUS; int dx,dy; dx = (int) -scrollPageLeft; dy = (int) (touchPoint.y); int time =(int) (-scrollPageLeft/viewWidth * scrollTime); mScroller.startScroll((int) (viewWidth+scrollPageLeft), (int) touchPoint.y, dx, dy, time);}
第二点,关于更新页面内容的时机。前文我们提到更新页面内容需要调用pageFactory.drawXxxBitmap方法重新绘制页面内容,内容数据太大时,绘制速度就会变慢,如果在View的onDraw方法内执行此操作,就会造成卡顿。因此,我们需要在onDraw之前绘制好内容bitmap。View何时重绘和触摸操作有关,所以在监听到ACTION_DOWN时就应该要开始更新内容了。举个例子,如果当前页数为2,执行翻到下页的操作,既然要提前更新页面内容,那么当手指落下的区域为右区域(touchStyle == TOUCH_RIGHT)时,第2页的内容就要绘制到previousPage(上页)中,第3页的内容绘制到currentPage(当前页)中,具体代码实现如下
pageNum++;pageFactory.drawPreviousBitmap(previousPage,pageNum);pageFactory.drawCurrentBitmap(currentPage,pageNum);pageNum--;
最后在View的computeScroll()方法中判断滑动页的位置,如果滑动页到了指定的位置(离开View),执行页数增加的操作。具体代码如下(文字分析理解不清楚的可以对照着代码一步步看)
public class CoverPageView extends View { //省略部分代码... private int scrollTime;//滑动动画时间 private Scroller mScroller; private int pageState;//翻页状态,用于限制翻页动画结束前的触摸操作 public static final int PAGE_STAY = 0;//处于静止状态 public static final int PAGE_NEXT = 1;//翻至下一页 public static final int PAGE_PREVIOUS = 2;//翻至上一页 private void init(Context context){ //省略部分代码... pageState = PAGE_STAY; mScroller = new Scroller(context,new LinearInterpolator()); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if(pageFactory !=null){ if(touchPoint.x ==-1 && touchPoint.y ==-1){ drawCurrentPage(canvas); pageState = PAGE_STAY; }else{ if(touchStyle == TOUCH_RIGHT){ drawCurrentPage(canvas); drawPreviousPage(canvas); }else { drawNextPage(canvas); drawCurrentPage(canvas); } } } } /** * 绘制上一页 * @param canvas */ private void drawPreviousPage(Canvas canvas){ canvas.drawBitmap(previousPage, scrollPageLeft, 0,null); } /** * 绘制当前页 * @param canvas */ private void drawCurrentPage(Canvas canvas){ //注意上下翻页时的滑动页的内容不一样 if(touchStyle == TOUCH_RIGHT){ canvas.drawBitmap(currentPage, 0, 0,null); }else if(touchStyle == TOUCH_LEFT){ canvas.drawBitmap(currentPage, scrollPageLeft, 0,null); } } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); float x = event.getX(); float y = event.getY(); if(pageState == PAGE_STAY){ switch (event.getAction()){ case MotionEvent.ACTION_DOWN: xDown = x; if(x<=viewWidth/3){//左 touchStyle = TOUCH_LEFT; if(pageNum>1){ pageNum--; pageFactory.drawCurrentBitmap(currentPage,pageNum); pageFactory.drawNextBitmap(nextPage,pageNum); pageNum++; } }else if(x>viewWidth*2/3){//右 touchStyle = TOUCH_RIGHT; if(pageNum<pageFactory.pageTotal){ pageNum++; pageFactory.drawPreviousBitmap(previousPage,pageNum); pageFactory.drawCurrentBitmap(currentPage,pageNum); pageNum--; } }else if(x>viewWidth/3 && x<viewWidth*2/3){//中 touchStyle = TOUCH_MIDDLE; } break; case MotionEvent.ACTION_MOVE: if(touchStyle == TOUCH_LEFT){ if(pageNum>1){ scrollPage(x,y); } }else if(touchStyle == TOUCH_RIGHT){ if(pageNum<pageFactory.pageTotal){ scrollPage(x,y); } } break; case MotionEvent.ACTION_UP: autoScroll(); break; } } return true; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { float x = mScroller.getCurrX(); float y = mScroller.getCurrY(); scrollPageLeft = 0 - (viewWidth - x); if (mScroller.getFinalX() == x && mScroller.getFinalY() == y){//滑动页到达指定位置 if(touchStyle == TOUCH_RIGHT){ pageNum++; }else if(touchStyle == TOUCH_LEFT){ pageNum--; } resetView(); } postInvalidate(); } } /** * 计算滑动页面左边界位置,实现滑动当前页效果 * @param x * @param y */ private void scrollPage(float x, float y){ touchPoint.x = x; touchPoint.y = y; if(touchStyle == TOUCH_RIGHT){ scrollPageLeft = touchPoint.x - xDown; }else if(touchStyle == TOUCH_LEFT){ scrollPageLeft =touchPoint.x - xDown - viewWidth; } if(scrollPageLeft > 0){ scrollPageLeft = 0; } postInvalidate(); } /** * 自动完成滑动操作 */ private void autoScroll(){ switch (touchStyle){ case TOUCH_LEFT: if(pageNum>1){ autoScrollToPreviousPage(); } break; case TOUCH_RIGHT: if(pageNum<pageFactory.pageTotal){ autoScrollToNextPage(); } break; } } /** * 自动完成翻到下一页操作 */ private void autoScrollToNextPage(){ pageState = PAGE_NEXT; int dx,dy; dx = (int) -(viewWidth+scrollPageLeft); dy = (int) (touchPoint.y); int time =(int) ((1+scrollPageLeft/viewWidth) * scrollTime); mScroller.startScroll((int) (viewWidth+scrollPageLeft), (int) touchPoint.y, dx, dy, time); } /** * 自动完成返回上一页操作 */ private void autoScrollToPreviousPage(){ pageState = PAGE_PREVIOUS; int dx,dy; dx = (int) -scrollPageLeft; dy = (int) (touchPoint.y); int time =(int) (-scrollPageLeft/viewWidth * scrollTime); mScroller.startScroll((int) (viewWidth+scrollPageLeft), (int) touchPoint.y, dx, dy, time); } /** * 重置操作 */ private void resetView(){ scrollPageLeft = 0; touchPoint.x = -1; touchPoint.y = -1; }}
效果如图
绘制页面阴影
在Android自定义View——从零开始实现书籍翻页效果(四)一文中我们详细介绍了如何绘制页面的阴影,主要用到了GradientDrawable方面的知识。这里的阴影绘制比仿真翻页的要简单许多,我们不需要考虑如何截取和旋转阴影区域,只需要绘制到滑动页右边界处就行,代码如下
public class CoverPageView extends View { //省略部分代码... private GradientDrawable shadowDrawable; private void init(Context context){ //省略部分代码... int[] mBackShadowColors = new int[] { 0x66000000,0x00000000}; shadowDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors); shadowDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if(pageFactory !=null){ if(touchPoint.x ==-1 && touchPoint.y ==-1){ drawCurrentPage(canvas); pageState = PAGE_STAY; }else{ if(touchStyle == TOUCH_RIGHT){ drawCurrentPage(canvas); drawPreviousPage(canvas); drawShadow(canvas); }else { drawNextPage(canvas); drawCurrentPage(canvas); drawShadow(canvas); } } } } /** * 绘制阴影 * @param canvas */ private void drawShadow(Canvas canvas){ int left = (int)(viewWidth + scrollPageLeft); shadowDrawable.setBounds(left, 0, left + 30 , viewHeight); shadowDrawable.draw(canvas); }}
效果如图
至此本篇教程到此结束,如果大家看了感觉还不错麻烦点个赞,你们的支持是我最大的动力~
- Android自定义View——从零开始实现覆盖翻页效果
- Android自定义View——从零开始实现书籍翻页效果(一)
- Android自定义View——从零开始实现书籍翻页效果(二)
- Android自定义View——从零开始实现书籍翻页效果(三)
- Android自定义View——从零开始实现书籍翻页效果(四)
- Android自定义View——从零开始实现书籍翻页效果(性能优化篇)
- Android自定义View——从零开始实现雪花飘落效果
- Android自定义View——从零开始实现可暂停的旋转动画效果
- 自定义View很简单 - Android翻页效果原理实现之翻页的尝试
- Android自定义View——从零开始实现圆形进度条
- 自定义View很简单 - Android翻页效果原理实现之曲线的实现
- 自定义View很简单 - Android翻页效果原理实现之引入折线
- 自定义View很简单 - Android翻页效果原理实现之模拟扭曲
- Android 自定义View——蒙版擦除效果实现
- 自定义Android View组件——实现雷达图效果
- Android 自定义View——蒙版擦除效果实现
- Android自定义View——实现屏幕触摸校验效果
- Android自定义View——实现时钟效果
- tictactoe的wp
- 《小强升职记》
- Lua 笔记
- 优化数据库对象
- java10课语法糖
- Android自定义View——从零开始实现覆盖翻页效果
- go语言中yaml配置文件的使用
- 圈圈套圈圈(4)(待续)
- 内存管理(二)
- HTTP报文的语法
- oracle 分析函数
- JAVA POI 读取2013 EXCEL时 XSSFWorkbook(fis);出错
- 三种方法让你的Service不被“一键加速”和系统杀掉
- 卷积神经网络CNN中的各种意义