Android自定義控件實戰——仿多看閱讀平移翻頁

    轉載請聲明出處http://blog.csdn.net/zhongkejingwang/article/details/38728119

之前自己做的一個APP需要用到翻頁閱讀,網上看過立體翻頁效果,不過bug太多了還不兼容。看了一下多看閱讀翻頁是採用平移翻頁的,於是就仿寫了一個平移翻頁的控件。效果如下:


在翻頁時頁面右邊緣繪製了陰影,效果還不錯。要實現這種平移翻頁控件並不難,只需要定義一個佈局管理頁面就可以了。具體實現上有以下難點:

    1、循環翻頁,頁面的重複利用。

    2、在翻頁時過濾掉多點觸碰。

    3、採用setAdapter的方式設置頁面佈局和數據。

下面就來一一解決這幾個難點。首先看循環翻頁問題,怎麼樣能採用較少的頁面實現這種翻頁呢?由於屏幕上每次只能顯示一張完整的頁面,翻過去的頁面也看不到,所以可以把翻過去的頁面拿來重複利用,不必每次都new一個頁面,所以,我只用了三張頁面實現循環翻頁。要想重複利用頁面,首先要知道頁面在佈局中序號和對應的層次關係,比如一個父控件的子view的序號越大就位於越上層。循環利用頁面的原理圖如下:

向右翻頁時狀態圖是這樣的,只用了0、1、2三張頁面,頁面序號爲2的位於最上層,我把它隱藏在左邊,所以看到的只有頁面1,頁面0在1下面擋着也看不到,向右翻頁時,頁面2被滑到屏幕中,這時候把頁面0的內容替換成頁面2的前一頁內容,把它放到之前頁面2的位置,這時,狀態又回到了初始狀態,又可以繼續向右翻頁了!


向左翻頁時是這樣的,初始狀態還是一樣,當頁面1被往左翻過時,看到的是頁面0,這時候頁面0下面已經沒有頁面了,而頁面2已經用不到了,這時候把頁面2放到頁面0下面,這時候狀態又回到了初始狀態,就可以繼續往左翻頁了。


類似於這種循環效果的實現我一直用的解決方案都是將選中的置於最中間,比如原理圖中的頁面1,每次翻頁完成後可見的都是頁面1。在滾動選擇器PickerView中也是同樣的方案。這就解決了頁面的重複利用問題了。

解決難點2 翻頁時過濾多點觸碰這個問題在仿淘寶商品瀏覽界面中已經解決過了,就是用一個控制變量mEvents過濾掉pointer down或up後到來的第一個move事件。

解決難點3 採用adapter方式設置頁面的佈局和數據。這個在Android的AdapterView裏用到的,但是我沒有看它的adapter機制,太複雜了,我就搞了個簡單的adapter,如下:

PageAdapter.java:

package com.jingchen.pagerdemo;

import android.view.View;

public abstract class PageAdapter
{
	/**
	 * @return 頁面view
	 */
	public abstract View getView();

	public abstract int getCount();

	/**
	 * 將內容添加到view中
	 * 
	 * @param view
	 *            包含內容的view
	 * @param position
	 *            第position頁
	 */
	public abstract void addContent(View view, int position);
}
這是一個抽象類,getView()用於返回頁面的佈局,getCount()返回數據總共需要多少頁,addContent(View view, int position)這個是每翻過一頁後將會被調用來請求頁面數據的,參數view就是頁面,position是表明第幾頁。待會兒會在自定義佈局中定義setAdapter方法設置設配器。

    OK,難點都解決了,自定義一個佈局叫ScanView繼承自RelativeLayout:

ScanView.java:

package com.jingchen.pagerdemo;

import java.util.Timer;
import java.util.TimerTask;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.RectF;
import android.graphics.Shader.TileMode;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.widget.RelativeLayout;

/**
 * @author chenjing
 *
 */
public class ScanView extends RelativeLayout
{
	public static final String TAG = "ScanView";
	private boolean isInit = true;
	// 滑動的時候存在兩頁可滑動,要判斷是哪一頁在滑動
	private boolean isPreMoving = true, isCurrMoving = true;
	// 當前是第幾頁
	private int index;
	private float lastX;
	// 前一頁,當前頁,下一頁的左邊位置
	private int prePageLeft = 0, currPageLeft = 0, nextPageLeft = 0;
	// 三張頁面
	private View prePage, currPage, nextPage;
	// 頁面狀態
	private static final int STATE_MOVE = 0;
	private static final int STATE_STOP = 1;
	// 滑動的頁面,只有前一頁和當前頁可滑
	private static final int PRE = 2;
	private static final int CURR = 3;
	private int state = STATE_STOP;
	// 正在滑動的頁面右邊位置,用於繪製陰影
	private float right;
	// 手指滑動的距離
	private float moveLenght;
	// 頁面寬高
	private int mWidth, mHeight;
	// 獲取滑動速度
	private VelocityTracker vt;
	// 防止抖動
	private float speed_shake = 20;
	// 當前滑動速度
	private float speed;
	private Timer timer;
	private MyTimerTask mTask;
	// 滑動動畫的移動速度
	public static final int MOVE_SPEED = 10;
	// 頁面適配器
	private PageAdapter adapter;
	/**
	 * 過濾多點觸碰的控制變量
	 */
	private int mEvents;

	public void setAdapter(ScanViewAdapter adapter)
	{
		removeAllViews();
		this.adapter = adapter;
		prePage = adapter.getView();
		addView(prePage, 0, new LayoutParams(LayoutParams.MATCH_PARENT,
				LayoutParams.MATCH_PARENT));
		adapter.addContent(prePage, index - 1);

		currPage = adapter.getView();
		addView(currPage, 0, new LayoutParams(LayoutParams.MATCH_PARENT,
				LayoutParams.MATCH_PARENT));
		adapter.addContent(currPage, index);

		nextPage = adapter.getView();
		addView(nextPage, 0, new LayoutParams(LayoutParams.MATCH_PARENT,
				LayoutParams.MATCH_PARENT));
		adapter.addContent(nextPage, index + 1);

	}

	/**
	 * 向左滑。注意可以滑動的頁面只有當前頁和前一頁
	 * 
	 * @param which
	 */
	private void moveLeft(int which)
	{
		switch (which)
		{
		case PRE:
			prePageLeft -= MOVE_SPEED;
			if (prePageLeft < -mWidth)
				prePageLeft = -mWidth;
			right = mWidth + prePageLeft;
			break;
		case CURR:
			currPageLeft -= MOVE_SPEED;
			if (currPageLeft < -mWidth)
				currPageLeft = -mWidth;
			right = mWidth + currPageLeft;
			break;
		}
	}

	/**
	 * 向右滑。注意可以滑動的頁面只有當前頁和前一頁
	 * 
	 * @param which
	 */
	private void moveRight(int which)
	{
		switch (which)
		{
		case PRE:
			prePageLeft += MOVE_SPEED;
			if (prePageLeft > 0)
				prePageLeft = 0;
			right = mWidth + prePageLeft;
			break;
		case CURR:
			currPageLeft += MOVE_SPEED;
			if (currPageLeft > 0)
				currPageLeft = 0;
			right = mWidth + currPageLeft;
			break;
		}
	}

	/**
	 * 當往回翻過一頁時添加前一頁在最左邊
	 */
	private void addPrePage()
	{
		removeView(nextPage);
		addView(nextPage, -1, new LayoutParams(LayoutParams.MATCH_PARENT,
				LayoutParams.MATCH_PARENT));
		// 從適配器獲取前一頁內容
		adapter.addContent(nextPage, index - 1);
		// 交換順序
		View temp = nextPage;
		nextPage = currPage;
		currPage = prePage;
		prePage = temp;
		prePageLeft = -mWidth;
	}

	/**
	 * 當往前翻過一頁時,添加一頁在最底下
	 */
	private void addNextPage()
	{
		removeView(prePage);
		addView(prePage, 0, new LayoutParams(LayoutParams.MATCH_PARENT,
				LayoutParams.MATCH_PARENT));
		// 從適配器獲取後一頁內容
		adapter.addContent(prePage, index + 1);
		// 交換順序
		View temp = currPage;
		currPage = nextPage;
		nextPage = prePage;
		prePage = temp;
		currPageLeft = 0;
	}

	Handler updateHandler = new Handler()
	{

		@Override
		public void handleMessage(Message msg)
		{
			if (state != STATE_MOVE)
				return;
			// 移動頁面
			// 翻回,先判斷當前哪一頁處於未返回狀態
			if (prePageLeft > -mWidth && speed <= 0)
			{
				// 前一頁處於未返回狀態
				moveLeft(PRE);
			} else if (currPageLeft < 0 && speed >= 0)
			{
				// 當前頁處於未返回狀態
				moveRight(CURR);
			} else if (speed < 0 && index < adapter.getCount())
			{
				// 向左翻,翻動的是當前頁
				moveLeft(CURR);
				if (currPageLeft == (-mWidth))
				{
					index++;
					// 翻過一頁,在底下添加一頁,把最上層頁面移除
					addNextPage();
				}
			} else if (speed > 0 && index > 1)
			{
				// 向右翻,翻動的是前一頁
				moveRight(PRE);
				if (prePageLeft == 0)
				{
					index--;
					// 翻回一頁,添加一頁在最上層,隱藏在最左邊
					addPrePage();
				}
			}
			if (right == 0 || right == mWidth)
			{
				releaseMoving();
				state = STATE_STOP;
				quitMove();
			}
			ScanView.this.requestLayout();
		}

	};

	public ScanView(Context context, AttributeSet attrs, int defStyle)
	{
		super(context, attrs, defStyle);
		init();
	}

	public ScanView(Context context)
	{
		super(context);
		init();
	}

	public ScanView(Context context, AttributeSet attrs)
	{
		super(context, attrs);
		init();
	}

	/**
	 * 退出動畫翻頁
	 */
	public void quitMove()
	{
		if (mTask != null)
		{
			mTask.cancel();
			mTask = null;
		}
	}

	private void init()
	{
		index = 1;
		timer = new Timer();
		mTask = new MyTimerTask(updateHandler);
	}

	/**
	 * 釋放動作,不限制手滑動方向
	 */
	private void releaseMoving()
	{
		isPreMoving = true;
		isCurrMoving = true;
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent event)
	{
		if (adapter != null)
			switch (event.getActionMasked())
			{
			case MotionEvent.ACTION_DOWN:
				lastX = event.getX();
				try
				{
					if (vt == null)
					{
						vt = VelocityTracker.obtain();
					} else
					{
						vt.clear();
					}
				} catch (Exception e)
				{
					e.printStackTrace();
				}
				vt.addMovement(event);
				mEvents = 0;
				break;
			case MotionEvent.ACTION_POINTER_DOWN:
			case MotionEvent.ACTION_POINTER_UP:
				mEvents = -1;
				break;
			case MotionEvent.ACTION_MOVE:
				// 取消動畫
				quitMove();
				Log.d("index", "mEvents = " + mEvents + ", isPreMoving = "
						+ isPreMoving + ", isCurrMoving = " + isCurrMoving);
				vt.addMovement(event);
				vt.computeCurrentVelocity(500);
				speed = vt.getXVelocity();
				moveLenght = event.getX() - lastX;
				if ((moveLenght > 0 || !isCurrMoving) && isPreMoving
						&& mEvents == 0)
				{
					isPreMoving = true;
					isCurrMoving = false;
					if (index == 1)
					{
						// 第一頁不能再往右翻,跳轉到前一個activity
						state = STATE_MOVE;
						releaseMoving();
					} else
					{
						// 非第一頁
						prePageLeft += (int) moveLenght;
						// 防止滑過邊界
						if (prePageLeft > 0)
							prePageLeft = 0;
						else if (prePageLeft < -mWidth)
						{
							// 邊界判斷,釋放動作,防止來回滑動導致滑動前一頁時當前頁無法滑動
							prePageLeft = -mWidth;
							releaseMoving();
						}
						right = mWidth + prePageLeft;
						state = STATE_MOVE;
					}
				} else if ((moveLenght < 0 || !isPreMoving) && isCurrMoving
						&& mEvents == 0)
				{
					isPreMoving = false;
					isCurrMoving = true;
					if (index == adapter.getCount())
					{
						// 最後一頁不能再往左翻
						state = STATE_STOP;
						releaseMoving();
					} else
					{
						currPageLeft += (int) moveLenght;
						// 防止滑過邊界
						if (currPageLeft < -mWidth)
							currPageLeft = -mWidth;
						else if (currPageLeft > 0)
						{
							// 邊界判斷,釋放動作,防止來回滑動導致滑動當前頁是前一頁無法滑動
							currPageLeft = 0;
							releaseMoving();
						}
						right = mWidth + currPageLeft;
						state = STATE_MOVE;
					}

				} else
					mEvents = 0;
				lastX = event.getX();
				requestLayout();
				break;
			case MotionEvent.ACTION_UP:
				if (Math.abs(speed) < speed_shake)
					speed = 0;
				quitMove();
				mTask = new MyTimerTask(updateHandler);
				timer.schedule(mTask, 0, 5);
				try
				{
					vt.clear();
					vt.recycle();
				} catch (Exception e)
				{
					e.printStackTrace();
				}
				break;
			default:
				break;
			}
		super.dispatchTouchEvent(event);
		return true;
	}

	/*
	 * (非 Javadoc) 在這裏繪製翻頁陰影效果
	 * 
	 * @see android.view.ViewGroup#dispatchDraw(android.graphics.Canvas)
	 */
	@Override
	protected void dispatchDraw(Canvas canvas)
	{
		super.dispatchDraw(canvas);
		if (right == 0 || right == mWidth)
			return;
		RectF rectF = new RectF(right, 0, mWidth, mHeight);
		Paint paint = new Paint();
		paint.setAntiAlias(true);
		LinearGradient linearGradient = new LinearGradient(right, 0,
				right + 36, 0, 0xffbbbbbb, 0x00bbbbbb, TileMode.CLAMP);
		paint.setShader(linearGradient);
		paint.setStyle(Style.FILL);
		canvas.drawRect(rectF, paint);
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
	{
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		mWidth = getMeasuredWidth();
		mHeight = getMeasuredHeight();
		if (isInit)
		{
			// 初始狀態,一頁放在左邊隱藏起來,兩頁疊在一塊
			prePageLeft = -mWidth;
			currPageLeft = 0;
			nextPageLeft = 0;
			isInit = false;
		}
	}

	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b)
	{
		if (adapter == null)
			return;
		prePage.layout(prePageLeft, 0,
				prePageLeft + prePage.getMeasuredWidth(),
				prePage.getMeasuredHeight());
		currPage.layout(currPageLeft, 0,
				currPageLeft + currPage.getMeasuredWidth(),
				currPage.getMeasuredHeight());
		nextPage.layout(nextPageLeft, 0,
				nextPageLeft + nextPage.getMeasuredWidth(),
				nextPage.getMeasuredHeight());
		invalidate();
	}

	class MyTimerTask extends TimerTask
	{
		Handler handler;

		public MyTimerTask(Handler handler)
		{
			this.handler = handler;
		}

		@Override
		public void run()
		{
			handler.sendMessage(handler.obtainMessage());
		}

	}
}
代碼中的註釋寫的非常多,原理理解了看代碼就容易看懂了。寫完這個佈局後再寫一個ScanViewAdapter繼承PageAdapter:

package com.jingchen.pagerdemo;

import java.util.List;

import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Typeface;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;

public class ScanViewAdapter extends PageAdapter
{
	Context context;
	List<String> items;
	AssetManager am;

	public ScanViewAdapter(Context context, List<String> items)
	{
		this.context = context;
		this.items = items;
		am = context.getAssets();
	}

	public void addContent(View view, int position)
	{
		TextView content = (TextView) view.findViewById(R.id.content);
		TextView tv = (TextView) view.findViewById(R.id.index);
		if ((position - 1) < 0 || (position - 1) >= getCount())
			return;
		content.setText("    雙峯疊障,過天風海雨,無邊空碧。月姊年年應好在,玉闕瓊宮愁寂。誰喚癡雲,一杯未盡,夜氣寒無色。碧城凝望,高樓縹緲西北。\n\n    腸斷桂冷蟾孤,佳期如夢,又把闌干拍。霧鬢風虔相借問,浮世幾回今夕。圓缺睛明,古今同恨,我更長爲客。蟬娟明夜,尊前誰念南陌。");
		tv.setText(items.get(position - 1));
	}

	public int getCount()
	{
		return items.size();
	}

	public View getView()
	{
		View view = LayoutInflater.from(context).inflate(R.layout.page_layout,
				null);
		return view;
	}
}
這裏只是我的demo裏寫的Adapter,也可以寫成帶更多內容的Adapter。addContent裏帶的參數view就是getView裏面返回的view,這樣就可以根據inflate的佈局設置內容了,getView返回的佈局page_layout.xml如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/cover" >

    <TextView
        android:id="@+id/content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="60dp"
        android:padding="10dp"
        android:textColor="#000000"
        android:textSize="22sp" />

    <TextView
        android:id="@+id/index"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="60dp"
        android:textColor="#000000"
        android:textSize="30sp" />

</RelativeLayout>
只包含了兩個TextView,所以在adapter中可以根據id查找到這兩個TextView再給它設置內容。

OK了,MainActivity的佈局如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.jingchen.pagerdemo.ScanView
        android:id="@+id/scanview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>
很簡單,只包含了ScanView。

MainActivity的代碼:

package com.jingchen.pagerdemo;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;

public class MainActivity extends Activity
{
	ScanView scanview;
	ScanViewAdapter adapter;

	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		scanview = (ScanView) findViewById(R.id.scanview);
		List<String> items = new ArrayList<String>();
		for (int i = 0; i < 8; i++)
			items.add("第 " + (i + 1) + " 頁");
		adapter = new ScanViewAdapter(this, items);
		scanview.setAdapter(adapter);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu)
	{
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

}
給ScanView設置Adapter就可以了。

好啦,仿多看的平移翻頁就完成了~

源碼下載






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章