现在的位置: 首页 > 综合 > 正文

oschina-app 的源码分析-主页面滑动切换

2014年09月05日 ⁄ 综合 ⁄ 共 11314字 ⁄ 字号 评论关闭

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


抱歉!评论已关闭.