学习最好的办法就是从实际出发,所以我选择了ScrollLayout(左右滑动切换屏幕控件)来作为例子,讲述一下我对ViewGroup的一些机制的个人理解。
首先先贴一下ScrollLayout的代码:
1package cn.edu.scau.hci.widget; 2 3import android.content.Context; 4import android.util.AttributeSet; 5import android.view.MotionEvent; 6import android.view.VelocityTracker; 7import android.view.View; 8import android.view.ViewConfiguration; 9import android.view.ViewGroup; 10import android.widget.Scroller; 11 12/** 13 * 左右滑动切换屏幕控件 14 * 15 * @author Yao.GUET date: 2011-05-04 16 * @modify liux (http://my.oschina.net/liux) 17 */ 18public class ScrollLayout extends ViewGroup { 19 private Scroller mScroller; 20 private VelocityTracker mVelocityTracker; 21 private int mCurScreen; 22 private int mDefaultScreen = 0; 23 private static final int TOUCH_STATE_REST = 0; 24 private static final int TOUCH_STATE_SCROLLING = 1; 25 private static final int SNAP_VELOCITY = 600; 26 private int mTouchState = TOUCH_STATE_REST; 27 private int mTouchSlop; 28 private float mLastMotionX; 29 private float mLastMotionY; 30 private OnViewChangeListener mOnViewChangeListener; 31 32 /** 33 * 设置是否可左右滑动 34 * 35 * @author liux 36 */ 37 private boolean isScroll = true; 38 39 public void setIsScroll(boolean b) { 40 this.isScroll = b; 41 } 42 43 public ScrollLayout(Context context, AttributeSet attrs) { 44 this(context, attrs, 0); 45 } 46 47 public ScrollLayout(Context context, AttributeSet attrs, int defStyle) { 48 super(context, attrs, defStyle); 49 mScroller = new Scroller(context); 50 mCurScreen = mDefaultScreen; 51 mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 52 } 53 54 @Override 55 protected void onLayout(boolean changed, int l, int t, int r, int b) { 56 int childLeft = 0; 57 final int childCount = getChildCount(); 58 for (int i = 0; i < childCount; i++) { 59 final View childView = getChildAt(i); 60 if (childView.getVisibility() != View.GONE) { 61 final int childWidth = childView.getMeasuredWidth(); 62 childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight()); 63 childLeft += childWidth; 64 } 65 } 66 } 67 68 @Override 69 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 70 // Log.e(TAG, "onMeasure"); 71 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 72 final int width = MeasureSpec.getSize(widthMeasureSpec); 73 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 74 if (widthMode != MeasureSpec.EXACTLY) { 75 throw new IllegalStateException("ScrollLayout only canmCurScreen run at EXACTLY mode!"); 76 } 77 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 78 if (heightMode != MeasureSpec.EXACTLY) { 79 throw new IllegalStateException("ScrollLayout only can run at EXACTLY mode!"); 80 } 81 82 // The children are given the same width and height as the scrollLayout 83 final int count = getChildCount(); 84 for (int i = 0; i < count; i++) { 85 getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec); 86 } 87 // Log.e(TAG, "moving to screen "+mCurScreen); 88 scrollTo(mCurScreen * width, 0); 89 } 90 91 /** 92 * According to the position of current layout scroll to the destination page. 93 */ 94 public void snapToDestination() { 95 final int screenWidth = getWidth(); 96 final int destScreen = (getScrollX() + screenWidth / 2) / screenWidth; 97 snapToScreen(destScreen); 98 } 99 100 public void snapToScreen(int whichScreen) { 101 // 是否可滑动 102 if (!isScroll) { 103 this.setToScreen(whichScreen); 104 return; 105 } 106 107 scrollToScreen(whichScreen); 108 } 109 110 public void scrollToScreen(int whichScreen) { 111 // get the valid layout page 112 whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1)); 113 if (getScrollX() != (whichScreen * getWidth())) { 114 final int delta = whichScreen * getWidth() - getScrollX(); 115 mScroller.startScroll(getScrollX(), 0, delta, 0, Math.abs(delta) * 1);// 持续滚动时间 以毫秒为单位 116 mCurScreen = whichScreen; 117 118 invalidate(); // Redraw the layout 119 120 if (mOnViewChangeListener != null) { 121 mOnViewChangeListener.OnViewChange(mCurScreen); 122 } 123 } 124 125 } 126 127 public void setToScreen(int whichScreen) { 128 whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1)); 129 mCurScreen = whichScreen; 130 scrollTo(whichScreen * getWidth(), 0); 131 132 if (mOnViewChangeListener != null) { 133 mOnViewChangeListener.OnViewChange(mCurScreen); 134 } 135 } 136 137 public int getCurScreen() { 138 return mCurScreen; 139 } 140 141 @Override 142 public void computeScroll() { 143 if (mScroller.computeScrollOffset()) { 144 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 145 postInvalidate(); 146 } 147 } 148 149 @Override 150 public boolean onTouchEvent(MotionEvent event) { 151 // 是否可滑动 152 if (!isScroll) { 153 return false; 154 } 155 156 if (mVelocityTracker == null) { 157 mVelocityTracker = VelocityTracker.obtain(); 158 } 159 mVelocityTracker.addMovement(event); 160 final int action = event.getAction(); 161 final float x = event.getX(); 162 final float y = event.getY(); 163 switch (action) { 164 case MotionEvent.ACTION_DOWN: 165 // Log.e(TAG, "event down!"); 166 if (!mScroller.isFinished()) { 167 mScroller.abortAnimation(); 168 } 169 mLastMotionX = x; 170 171 // ---------------New Code---------------------- 172 mLastMotionY = y; 173 // --------------------------------------------- 174 175 break; 176 case MotionEvent.ACTION_MOVE: 177 int deltaX = (int) (mLastMotionX - x); 178 179 // ---------------New Code---------------------- 180 int deltaY = (int) (mLastMotionY - y); 181 if (Math.abs(deltaX) < 200 && Math.abs(deltaY) > 10) break; 182 mLastMotionY = y; 183 // ------------------------------------- 184 185 mLastMotionX = x; 186 scrollBy(deltaX, 0); 187 break; 188 case MotionEvent.ACTION_UP: 189 // Log.e(TAG, "event : up"); 190 // if (mTouchState == TOUCH_STATE_SCROLLING) { 191 final VelocityTracker velocityTracker = mVelocityTracker; 192 velocityTracker.computeCurrentVelocity(1000); 193 int velocityX = (int) velocityTracker.getXVelocity(); 194 // Log.e(TAG, "velocityX:" + velocityX); 195 if (velocityX > SNAP_VELOCITY && mCurScreen > 0) { 196 // Fling enough to move left 197 // Log.e(TAG, "snap left"); 198 snapToScreen(mCurScreen - 1); 199 } else if (velocityX < -SNAP_VELOCITY && mCurScreen < getChildCount() - 1) { 200 // Fling enough to move right 201 // Log.e(TAG, "snap right"); 202 snapToScreen(mCurScreen + 1); 203 } else { 204 snapToDestination(); 205 } 206 if (mVelocityTracker != null) { 207 mVelocityTracker.recycle(); 208 mVelocityTracker = null; 209 } 210 // } 211 mTouchState = TOUCH_STATE_REST; 212 break; 213 case MotionEvent.ACTION_CANCEL: 214 mTouchState = TOUCH_STATE_REST; 215 break; 216 } 217 return true; 218 } 219 220 @Override 221 public boolean onInterceptTouchEvent(MotionEvent ev) { 222 // Log.e(TAG, "onInterceptTouchEvent-slop:" + mTouchSlop); 223 final int action = ev.getAction(); 224 if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) { 225 return true; 226 } 227 final float x = ev.getX(); 228 final float y = ev.getY(); 229 switch (action) { 230 case MotionEvent.ACTION_MOVE: 231 final int xDiff = (int) Math.abs(mLastMotionX - x); 232 if (xDiff > mTouchSlop) { 233 mTouchState = TOUCH_STATE_SCROLLING; 234 } 235 break; 236 case MotionEvent.ACTION_DOWN: 237 mLastMotionX = x; 238 mLastMotionY = y; 239 mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING; 240 break; 241 case MotionEvent.ACTION_CANCEL: 242 case MotionEvent.ACTION_UP: 243 mTouchState = TOUCH_STATE_REST; 244 break; 245 } 246 return mTouchState != TOUCH_STATE_REST; 247 } 248 249 /** 250 * 设置屏幕切换监听器 251 * 252 * @param listener 253 */ 254 public void SetOnViewChangeListener(OnViewChangeListener listener) { 255 mOnViewChangeListener = listener; 256 } 257 258 /** 259 * 屏幕切换监听器 260 * 261 * @author liux 262 */ 263 public interface OnViewChangeListener { 264 public void OnViewChange(int view); 265 } 266}
下面我就根据我的理解讲述一下整个滑动的过程
首先是整个组建的初始化,调用构造函数(这个就不详细说了)
调用onMeasure(int widthMeasureSpec, int heightMeasureSpec)函数,循环计算没一个子View的宽高。
调用onLayout(boolean changed, int l, int t, int r, int b)设置每一个子View的布局
调用onDraw方法开始画图。在调用onDraw方法时候会调用computeScroll()方法,假如正在滑动的时候就调用使ViewscrollTo方法滑动,然后调用postInvalidate更新界面。
用户滑动的时候,会先到用onInterceptTouchEvent(MotionEvent ev),第一次ACTION_DOWN的时候,onInterceptTouchEvent方法会返回false,表示ViewGroup不拦截这个之后的手势(即这一系列的手势会传到其子View),然后会触发ScrollLayout的onTouchEvent(MotionEvent event)方法(子View的onTouchEvent均返回false的情况下)。
onTouchEvent(MotionEvent event)方法中
ACTION_DOWN事件:
记下事件的坐标。
ACTION_MOVE事件:
计算滑动的距离,当水平华东大于200,垂直滑动大于10时候组建滑动。
ACTION_UP事件:
使用VelocityTracker计算滑动的速度,当速度大于600 千像素/微秒 时候,根据速度方向滑动。然后注销VelocityTracker。当速度达不到的时候,调用snapToDestination()方法判断应该返回到那一个页面。
参考链接
onMeasure和onLayout:http://blog.csdn.net/czh0766/article/details/5846460
Scroller:http://www.cnblogs.com/over140/archive/2010/12/16/1907528.html,http://ipjmc.iteye.com/blog/1615828
onInterceptTouchEvent和onTouchEvent:http://blog.csdn.net/ddna/article/details/5473293