以前的項目中也經常用到頁面活動切換,但都是用現成項目庫viewpaper來實現的,使用起來比較簡單,綁定數據,重寫下適配器,有必要保存下數據狀態避免數據頻繁刷新,如果對內存使用要求不高可以設置多個緩存頁面:setOffscreenPageLimit(2),oschina裏面是通過一個工具類ScrollLayout來實現,跟viewpaper一樣都是重寫viewgroup來實現,下面我們通過分析ScrollLayout的實現原理,來學習下頁面滑動原理。
首先先看下ScrollLayout類的實現代碼
/**
* 左右滑動切換屏幕控件
* @author Yao.GUET date: 2011-05-04
* @modify liux (http://my.oschina.net/liux)
*/
public class ScrollLayout extends ViewGroup {
private static final String TAG = "ScrollLayout";
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
private int mCurScreen;
private int mDefaultScreen = 0;
private static final int TOUCH_STATE_REST = 0;
private static final int TOUCH_STATE_SCROLLING = 1;
private static final int SNAP_VELOCITY = 600;
private int mTouchState = TOUCH_STATE_REST;
private int mTouchSlop;
private float mLastMotionX;
private float mLastMotionY;
private OnViewChangeListener mOnViewChangeListener;
/**
* 設置是否可左右滑動
* @author liux
*/
private boolean isScroll = true;
public void setIsScroll(boolean b) {
this.isScroll = b;
}
public ScrollLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScrollLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mScroller = new Scroller(context);
mCurScreen = mDefaultScreen;
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE) {
final int childWidth = childView.getMeasuredWidth();
childView.layout(childLeft, 0, childLeft + childWidth,
childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//Log.e(TAG, "onMeasure");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
//佈局文件裏面是否明確指定該控件的寬高(100dp這樣的值)/一個MeasureSpec由大小和模式組成。
if (widthMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException(
"ScrollLayout only canmCurScreen run at EXACTLY mode!");
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException(
"ScrollLayout only can run at EXACTLY mode!");
}
// The children are given the same width and height as the scrollLayout
final int count = getChildCount();
for (int i = 0; i < count; i++) {
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
}
// Log.e(TAG, "moving to screen "+mCurScreen);
scrollTo(mCurScreen * width, 0);
}
/**
* According to the position of current layout scroll to the destination
* page.
*/
public void snapToDestination() {
final int screenWidth = getWidth();
final int destScreen = (getScrollX() + screenWidth / 2) / screenWidth;
snapToScreen(destScreen);
}
public void snapToScreen(int whichScreen) {
//是否可滑動
if(!isScroll) {
this.setToScreen(whichScreen);
return;
}
scrollToScreen(whichScreen);
}
public void scrollToScreen(int whichScreen) {
// get the valid layout page
whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
if (getScrollX() != (whichScreen * getWidth())) {
final int delta = whichScreen * getWidth() - getScrollX();
mScroller.startScroll(getScrollX(), 0, delta, 0,
Math.abs(delta) * 1);//持續滾動時間 以毫秒爲單位
mCurScreen = whichScreen;
invalidate(); // Redraw the layout
if (mOnViewChangeListener != null)
{
mOnViewChangeListener.OnViewChange(mCurScreen);
}
}
}
public void setToScreen(int whichScreen) {
whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
mCurScreen = whichScreen;
scrollTo(whichScreen * getWidth(), 0);
if (mOnViewChangeListener != null)
{
mOnViewChangeListener.OnViewChange(mCurScreen);
}
}
public int getCurScreen() {
return mCurScreen;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//是否可滑動
if(!isScroll) {
return false;
}
//獲得VelocityTracker類的一個實例對象
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
final int action = event.getAction();
final float x = event.getX();
final float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
//Log.e(TAG, "event down!");
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mLastMotionX = x;
//---------------New Code----------------------
mLastMotionY = y;
//---------------------------------------------
break;
case MotionEvent.ACTION_MOVE:
int deltaX = (int) (mLastMotionX - x);
//---------------New Code----------------------
int deltaY = (int) (mLastMotionY - y);
if(Math.abs(deltaX) < 200 && Math.abs(deltaY) > 10)
break;
mLastMotionY = y;
//-------------------------------------
mLastMotionX = x;
scrollBy(deltaX, 0);
break;
case MotionEvent.ACTION_UP:
//Log.e(TAG, "event : up");
// if (mTouchState == TOUCH_STATE_SCROLLING) {
//判斷當ev事件是MotionEvent.ACTION_UP時:計算速率
final VelocityTracker velocityTracker = mVelocityTracker;
//設置units的值爲1000,意思爲一秒時間內運動了多少個像素
velocityTracker.computeCurrentVelocity(1000);
int velocityX = (int) velocityTracker.getXVelocity();
if (velocityX > SNAP_VELOCITY && mCurScreen > 0) {
// Fling enough to move left
snapToScreen(mCurScreen - 1);
} else if (velocityX < -SNAP_VELOCITY
&& mCurScreen < getChildCount() - 1) {
// Fling enough to move right
snapToScreen(mCurScreen + 1);
} else {
snapToDestination();
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
// }
mTouchState = TOUCH_STATE_REST;
break;
case MotionEvent.ACTION_CANCEL:
mTouchState = TOUCH_STATE_REST;
break;
}
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//Log.e(TAG, "onInterceptTouchEvent-slop:" + mTouchSlop);
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE)
&& (mTouchState != TOUCH_STATE_REST)) {
return true;
}
final float x = ev.getX();
final float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_MOVE:
final int xDiff = (int) Math.abs(mLastMotionX - x);
if (xDiff > mTouchSlop) {
mTouchState = TOUCH_STATE_SCROLLING;
}
break;
case MotionEvent.ACTION_DOWN:
mLastMotionX = x;
mLastMotionY = y;
mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST
: TOUCH_STATE_SCROLLING;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mTouchState = TOUCH_STATE_REST;
break;
}
return mTouchState != TOUCH_STATE_REST;
}
/**
* 設置屏幕切換監聽器
* @param listener
*/
public void SetOnViewChangeListener(OnViewChangeListener listener)
{
mOnViewChangeListener = listener;
}
/**
* 屏幕切換監聽器
* @author liux
*/
public interface OnViewChangeListener {
public void OnViewChange(int view);
}
整個划動過程是,初始化空間後,根據手勢滑動判斷是否滑動翻頁,翻頁後回調監聽到UI做相應處理,比如刷新頁面,划動頁面結束。
分析幾個主要的知識點:
1.自定義iew初始化,onMeasure是計算view的寬和高,onLayout是確定佈局和位置的,着重說下onMeasure方法中的MeasureSpec。
MeasureSpec封裝了父佈局傳遞給子佈局的佈局要求,每個MeasureSpec代表了一組寬度和高度的要求。一個MeasureSpec由大小和模式組成。
它有三種模式:
UNSPECIFIED(未指定), 父元素不對自元素施加任何束縛,子元素可以得到任意想要的大小;
EXACTLY(完全),父元素決定自元素的確切大小,子元素將被限定在給定的邊界裏而忽略它本身大小;
AT_MOST(至多),子元素至多達到指定大小的值。
2.根據收拾滑動,判斷翻頁。
滑動過程,肯定需要有狀態控制,避免手勢衝突:
private int mTouchState = TOUCH_STATE_REST;
划動的邏輯比較簡單,
一,MotionEvent.ACTION_DOWN:記錄點擊位置
二,MotionEvent.ACTION_MOVE:滑動相應距離,scrollBy(deltaX, 0);
三,鬆手時,首先判斷划動的速率和划動方向來翻頁,當不滿足速率時候,再判斷滑動的位置是否需要完成翻頁。
在這裏我們分析下划動的速率的判斷,即VelocityTracker速率跟蹤器。
當你需要跟蹤觸摸屏事件的速度的時候,使用obtain()方法來獲得VelocityTracker類的一個實例對象
在onTouchEvent回調函數中,使用addMovement(MotionEvent)函數將當前的移動事件傳遞給VelocityTracker對象
使用computeCurrentVelocity (int units)函數來計算當前的速度,使用 getXVelocity ()、 getYVelocity ()函數來獲得當前的速度
3,完成翻頁並回調監聽:
public void scrollToScreen(int whichScreen) {
// get the valid layout page
whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
if (getScrollX() != (whichScreen * getWidth())) {
final int delta = whichScreen * getWidth() - getScrollX();
mScroller.startScroll(getScrollX(), 0, delta, 0,
Math.abs(delta) * 1);//持續滾動時間 以毫秒爲單位
mCurScreen = whichScreen;
invalidate(); // Redraw the layout
if (mOnViewChangeListener != null)
{
mOnViewChangeListener.OnViewChange(mCurScreen);
}
}
}
首先將划動頁數規範在正常範圍內,再計算出需要划動的距離,完成滑動
mScroller.startScroll(getScrollX(), 0, delta, 0,Math.abs(delta) * 1);//持續滾動時間 以毫秒爲單位
startScroll的解析:
public voidstartScroll (int startX, int startY, int dx, int dy, int duration)
以提供的起始點和將要滑動的距離開始滾動。
參數
startX 水平方向滾動的偏移值,以像素爲單位。正值表明滾動將向左滾動
startY 垂直方向滾動的偏移值,以像素爲單位。正值表明滾動將向上滾動
dx 水平方向滑動的距離,正值會使滾動向左滾動
dy 垂直方向滑動的距離,正值會使滾動向上滾動
duration 滾動持續時間,以毫秒計。
滑動頁面的自定義過程完成,看下UI主界面是怎麼使用的,首先在佈局文件裏定義ScrollLayout
<net.oschina.app.widget.ScrollLayout
android:id="@+id/main_scrolllayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1">
<include layout="@layout/frame_news" />
<include layout="@layout/frame_question" />
<include layout="@layout/frame_tweet" />
<include layout="@layout/frame_active" />
</net.oschina.app.widget.ScrollLayout>
主程序中初始化,和RadioButton配合使用實現滑動頁面切換按鈕,點擊按鈕切換頁面:
/**
* 初始化水平滾動翻頁
*/
private void initPageScroll() {
mScrollLayout = (ScrollLayout) findViewById(R.id.main_scrolllayout);
LinearLayout linearLayout = (LinearLayout) findViewById(R.id.main_linearlayout_footer);
mHeadTitles = getResources().getStringArray(R.array.head_titles);
mViewCount = mScrollLayout.getChildCount();
mButtons = new RadioButton[mViewCount];
for (int i = 0; i < mViewCount; i++) {
mButtons[i] = (RadioButton) linearLayout.getChildAt(i * 2);
mButtons[i].setTag(i);
mButtons[i].setChecked(false);
mButtons[i].setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
int pos = (Integer) (v.getTag());
// 點擊當前項刷新
if (mCurSel == pos) {
switch (pos) {
case 0:// 資訊+博客
if (lvNews.getVisibility() == View.VISIBLE)
lvNews.clickRefresh();
else
lvBlog.clickRefresh();
break;
case 1:// 問答
lvQuestion.clickRefresh();
break;
case 2:// 動彈
lvTweet.clickRefresh();
break;
case 3:// 動態+留言
if (lvActive.getVisibility() == View.VISIBLE)
lvActive.clickRefresh();
else
lvMsg.clickRefresh();
break;
}
}
mScrollLayout.snapToScreen(pos);
}
});
}
// 設置第一顯示屏
mCurSel = 0;
mButtons[mCurSel].setChecked(true);
mScrollLayout
.SetOnViewChangeListener(new ScrollLayout.OnViewChangeListener() {
public void OnViewChange(int viewIndex) {
// 切換列表視圖-如果列表數據爲空:加載數據
switch (viewIndex) {
case 0:// 資訊
if (lvNews.getVisibility() == View.VISIBLE) {
if (lvNewsData.isEmpty()) {
loadLvNewsData(curNewsCatalog, 0,
lvNewsHandler,
UIHelper.LISTVIEW_ACTION_INIT);
}
} else {
if (lvBlogData.isEmpty()) {
loadLvBlogData(curNewsCatalog, 0,
lvBlogHandler,
UIHelper.LISTVIEW_ACTION_INIT);
}
}
break;
case 1:// 問答
if (lvQuestionData.isEmpty()) {
loadLvQuestionData(curQuestionCatalog, 0,
lvQuestionHandler,
UIHelper.LISTVIEW_ACTION_INIT);
}
break;
case 2:// 動彈
if (lvTweetData.isEmpty()) {
loadLvTweetData(curTweetCatalog, 0,
lvTweetHandler,
UIHelper.LISTVIEW_ACTION_INIT);
}
break;
case 3:// 動態
// 判斷登錄
if (!appContext.isLogin()) {
if (lvActive.getVisibility() == View.VISIBLE
&& lvActiveData.isEmpty()) {
lvActive_foot_more
.setText(R.string.load_empty);
lvActive_foot_progress
.setVisibility(View.GONE);
} else if (lvMsg.getVisibility() == View.VISIBLE
&& lvMsgData.isEmpty()) {
lvMsg_foot_more
.setText(R.string.load_empty);
lvMsg_foot_progress
.setVisibility(View.GONE);
}
UIHelper.showLoginDialog(Main.this);
break;
}
// 處理通知信息
if (bv_atme.isShown())
frameActiveBtnOnClick(framebtn_Active_atme,
ActiveList.CATALOG_ATME,
UIHelper.LISTVIEW_ACTION_REFRESH);
else if (bv_review.isShown())
frameActiveBtnOnClick(framebtn_Active_comment,
ActiveList.CATALOG_COMMENT,
UIHelper.LISTVIEW_ACTION_REFRESH);
else if (bv_message.isShown())
frameActiveBtnOnClick(framebtn_Active_message,
0, UIHelper.LISTVIEW_ACTION_REFRESH);
else if (lvActive.getVisibility() == View.VISIBLE
&& lvActiveData.isEmpty())
loadLvActiveData(curActiveCatalog, 0,
lvActiveHandler,
UIHelper.LISTVIEW_ACTION_INIT);
else if (lvMsg.getVisibility() == View.VISIBLE
&& lvMsgData.isEmpty())
loadLvMsgData(0, lvMsgHandler,
UIHelper.LISTVIEW_ACTION_INIT);
break;
}
setCurPoint(viewIndex);
}
});
}
ScrollLayout.OnViewChangeListener在回調監聽裏,對每一頁的數據做了緩存,這樣避免了數據的頻繁刷新,但這樣也必須設計刷新方式,下拉刷新或者刷新按鈕。到此滑動頁面切換完成,總體感覺體驗還不錯。
oschina-app完整源碼下載:http://download.csdn.net/detail/xiangxue336/7023661