版權宣告:本文為博主原創文章,未經博主允許不得轉載
系列教程:Android開發之從零開始系列
原始碼: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();
}
}
複製程式碼
效果如圖
實現上下翻頁
相關博文連結
要實現上下翻頁效果我們需從兩個方面入手,一是使用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);
}
}
複製程式碼
效果如圖
至此本篇教程到此結束,如果大家看了感覺還不錯麻煩點個贊,你們的支援是我最大的動力~