版權聲明:本文爲博主原創文章,未經博主容許不得轉載php
系列教程:Android開發之從零開始系列html
源碼:AnliaLee/BookPage,歡迎starjava
你們要是看到有錯誤的地方或者有啥好的建議,歡迎留言評論android
前言:以前講了仿真書籍翻頁效果,效果如圖git
咱們從原理分析、功能實現到性能優化完整地過了一遍,反響不錯,因而有小夥伴私信讓我把 覆蓋翻頁效果也講了,因此這期的主角就是它了 ~github
本篇只着重於思路和實現步驟,裏面用到的一些知識原理不會很是細地拿來說,若是有不清楚的api或方法能夠在網上搜下相應的資料,確定有大神講得很是清楚的,我這就不獻醜了。本着認真負責的精神我會把相關知識的博文連接也貼出來(其實就是懶不想寫那麼多哈哈),你們能夠自行傳送。爲了照顧第一次閱讀系列博客的小夥伴,本篇可能會出現一些在以前系列博客就講過的內容,看過的童鞋自行跳過該段便可canvas
國際慣例,先上效果圖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();
}
}
複製代碼
效果如圖
相關博文連接
要實現上下翻頁效果咱們需從兩個方面入手,一是使用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);
}
}
複製代碼
效果如圖
至此本篇教程到此結束,若是你們看了感受還不錯麻煩點個贊,大家的支持是我最大的動力~