以前的项目中也经常用到页面活动切换,但都是用现成项目库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