Android 多屏滑動

jalen_ma發表於2014-01-10

最近寫應用,多次用到裡多屏滑動,參考了下一些資料,寫裡一個類,可以通用。目前用起來沒什麼問題,先存下來。

package com.android.systemui.recent.gk;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import android.widget.Scroller;
import android.widget.TextView;

public class ScreenView extends ViewGroup {
	
	protected int mVisibleRange = 1;
	private static final int MAX_VISIBLE_RANGE = 4;
	private static final int TOUCH_STATE_RESET = 0;
	private static final int TOUCH_STATE_SCROLLING = 1;
	private static final int INVALIDATE_SCREEN = -1;
	private int mTouchState = TOUCH_STATE_RESET;
	private float mTouchDownX;
	private float mTouchDownY;
	private float mLastMotionX;
	private float mTouchSlop;
	private int mCurrentScreen;
	private int mNextScreen;
	private Scroller mScroller;
	private VelocityTracker mVelocityTracker;
	
	private static final int OVER_SCROLL_BOUNDS = 0;
	private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.3f;
	private static final float RETURN_TO_ORIGINAL_SCREEN_THRESHOLD = 0.33f;
	private static final int MIN_LENGTH_FOR_FLING = 25;
	private static final int FLING_THRESHOLD_VELOCITY = 200;
	private static final int MIN_SNAP_VELOCITY_BASE = 200;
	private static final int SCREEN_SNAP_DURATION = 200;
	
	private OvershootInterpolator mScrollInterpolator;
	private int mFlingThresholdVelocity;
	private int mMinSnapVelocity;
	private int mMaximumVelocity;
	private final float mBaseLineFlingVelocity = 2500.0f;
	private final float mFlingVelocityInfluence = 0.4f;
	
	private boolean mTouchMoveHorizion = false;
	private double mTouchMoveOrientationSlop;
	
	private int mScreenCount = 0;
	private int mScreenWidth;
	private int mMaxChildWidth;
	
	private static final float mOverScrollRatio = 0.3f;
	private static final String TAG = "ScreenView jalen";
	private int mOverScrollBounds = 0;
	

	public ScreenView(Context context) {
	    super(context);
	    init(context);
	}
	
	public ScreenView(Context context, AttributeSet attrs) {
	    super(context, attrs);
	    init(context);
	}
	
	public ScreenView(Context context, AttributeSet attrs, int defStyle) {
	    super(context, attrs, defStyle);
	    init(context);
	}
	
	@Override
	protected void onFinishInflate() {
		// TODO Auto-generated method stub
		super.onFinishInflate();
	}
	
	private void init(Context context){
		ViewConfiguration config = ViewConfiguration.get(context);
		mTouchSlop = config.getScaledTouchSlop();
		mMaximumVelocity = config.getScaledMaximumFlingVelocity();
		mMinSnapVelocity = MIN_SNAP_VELOCITY_BASE * 2;
		mFlingThresholdVelocity = FLING_THRESHOLD_VELOCITY * 2;
		mScrollInterpolator = new OvershootInterpolator();
		mScroller = new Scroller(context, mScrollInterpolator);
		mTouchMoveOrientationSlop = getTouchMoveOrientationSlop();
	}
	
	private double getTouchMoveOrientationSlop(){
		return Math.tan(45 * Math.PI / 180);
	}
	
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		// TODO Auto-generated method stub
		int childCount = getChildCount();
		int childGap;
		
		if(childCount < mVisibleRange){
			childGap = (mScreenWidth - childCount * mMaxChildWidth) / (childCount + 1);
		}else{
			childGap = (mScreenWidth - mVisibleRange * mMaxChildWidth) / (mVisibleRange + 1);
		}
		
		int left = getPaddingLeft() + childGap;
		int top = getPaddingTop();
		boolean isNewScreen;
		
		for(int i=0; i<childCount; i++){
			View v = getChildAt(i);
			if(v.getVisibility() == GONE) continue;
			
			v.layout(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight());
			isNewScreen = ((i + 1) % mVisibleRange == 0);
			if(isNewScreen){
				int screenIndex = (i + mVisibleRange - 1) / mVisibleRange;
				left = screenIndex * getWidth() + getPaddingLeft() + childGap;
			}else{
				left += v.getMeasuredWidth() + childGap;
			}
		}
		
		Log.i(TAG,"onLayout childCount="+childCount + ", childGap="+childGap);
	}
	
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// TODO Auto-generated method stub
		int childCount = getChildCount();
		
		for(int i=0; i<childCount; i++){
			View v = getChildAt(i);
			measureChild(v, widthMeasureSpec, heightMeasureSpec);
			mMaxChildWidth = Math.max(mMaxChildWidth, v.getMeasuredWidth());
			mScreenWidth = View.MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
			mVisibleRange = Math.min(MAX_VISIBLE_RANGE, Math.max(1, mScreenWidth / mMaxChildWidth));
		}
		Log.i(TAG,"onMeasure childCount="+childCount + ", mMaxChildWidth="+mMaxChildWidth
				+", mScreenWidth="+mScreenWidth
				+", mVisibleRange="+mVisibleRange);
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	}
	
	@Override
	public void addView(View child, int index, LayoutParams params) {
		// TODO Auto-generated method stub
		super.addView(child, index, params);
		mScreenCount += 1;
	}
	
	@Override
	public void removeAllViews() {
		// TODO Auto-generated method stub
		super.removeAllViews();
		mScreenCount = 0;
	}
	
	@Override
	public void removeViewsInLayout(int start, int count) {
		// TODO Auto-generated method stub
		super.removeViewsInLayout(start, count);
		mScreenCount -= 1;
	}
	
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		// TODO Auto-generated method stub
		aquireVelocityTrackerAndAddMotion(ev);
		switch(ev.getActionMasked()){
		case MotionEvent.ACTION_DOWN:
			mLastMotionX = mTouchDownX = ev.getX();
			mTouchDownY = ev.getY();
			mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESET : TOUCH_STATE_SCROLLING;
			mTouchMoveHorizion = (mTouchState == TOUCH_STATE_SCROLLING);
			break;
		case MotionEvent.ACTION_MOVE:
			if(mTouchState != TOUCH_STATE_SCROLLING){
				float xDiff = Math.abs(ev.getX() - mTouchDownX);
				float yDiff = Math.abs(ev.getY() - mTouchDownY);
				
				if(xDiff > mTouchSlop){
					if(yDiff == 0 || (xDiff / yDiff > mTouchMoveOrientationSlop)){
						mTouchMoveHorizion = true; 
					}
					
					if(mTouchMoveHorizion){
						mTouchState = TOUCH_STATE_SCROLLING;
					}
				}
			}
			break;
		case MotionEvent.ACTION_UP:
		case MotionEvent.ACTION_CANCEL:
			releaseVelocityTracker();
			mTouchState = TOUCH_STATE_RESET;
			break;
		}
		
		Log.i(TAG, " onInterceptTouchEvent "+ev.getActionMasked()+" return="+(mTouchState == TOUCH_STATE_SCROLLING));
		return  mTouchState == TOUCH_STATE_SCROLLING;
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		// TODO Auto-generated method stub
		aquireVelocityTrackerAndAddMotion(event);
		
		switch(event.getActionMasked()){
		case MotionEvent.ACTION_DOWN:
			mLastMotionX = mTouchDownX = event.getX();
			mTouchDownY = event.getY();
			mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESET : TOUCH_STATE_SCROLLING;
			mTouchMoveHorizion = (mTouchState == TOUCH_STATE_SCROLLING);
			break;
		case MotionEvent.ACTION_MOVE:
			if(mTouchState == TOUCH_STATE_SCROLLING){
				float deltaX = mLastMotionX - event.getX();
				mLastMotionX = event.getX();
				float nextScrollX = getScrollX() + deltaX;
				if(nextScrollX < (getScreenCount() - 1) * getWidth() + OVER_SCROLL_BOUNDS
						&& nextScrollX > -OVER_SCROLL_BOUNDS){
					scrollBy((int)deltaX, 0);
				}
			}else{
				float xDiff = Math.abs(event.getX() - mTouchDownX);
				float yDiff = Math.abs(event.getY() - mTouchDownY);
				
				if(xDiff > mTouchSlop){
					if(yDiff == 0 || (xDiff / yDiff > mTouchMoveOrientationSlop)){
						mTouchMoveHorizion = true; 
					}
					
					if(mTouchMoveHorizion){
						mTouchState = TOUCH_STATE_SCROLLING;
					}
				}
			}
			break;
		case MotionEvent.ACTION_UP:
		case MotionEvent.ACTION_CANCEL:
			if(mTouchState == TOUCH_STATE_SCROLLING){
				final int screenWidth = getWidth();

				mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                final int velocityX = (int) mVelocityTracker.getXVelocity();
                
                int delataX = (int)(event.getX() - mTouchDownX);
                boolean isSignificationMove = Math.abs(delataX) > screenWidth * SIGNIFICANT_MOVE_THRESHOLD;
                
                boolean isFling = (Math.abs(velocityX) > mFlingThresholdVelocity)
                		&& (Math.abs(delataX) > MIN_LENGTH_FOR_FLING);
                 
                boolean returnToOriginalScreen = (Math.signum(velocityX) != Math.signum(delataX))
                		&& (Math.abs(delataX) > screenWidth * RETURN_TO_ORIGINAL_SCREEN_THRESHOLD)
                		&& isFling;
                
                int finalScreen;
                if(((isSignificationMove && delataX > 0 && !isFling) || (isFling && velocityX > 0)) 
                		&& mCurrentScreen > 0){
                	finalScreen = returnToOriginalScreen ? mCurrentScreen : mCurrentScreen - 1;
                	snapToScreen(finalScreen, velocityX);
                }else if(((isSignificationMove && delataX < 0 && ! isFling) || (isFling && velocityX < 0))
                		&& mCurrentScreen < getScreenCount() - 1){
                	finalScreen = returnToOriginalScreen ? mCurrentScreen : mCurrentScreen + 1;
                	snapToScreen(finalScreen, velocityX);
                }else{
                	finalScreen = (getScrollX() + screenWidth / 2) / screenWidth;
                	snapToScreen(finalScreen, velocityX);
                }
                
				mTouchState = TOUCH_STATE_RESET;
			}
			releaseVelocityTracker();
			break;
		}
		return true;
	}
	
	@Override
	public void computeScroll() {
		// TODO Auto-generated method stub
		//super.computeScroll();
		if(mScroller.computeScrollOffset()){
			scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
			postInvalidate();
			
		}else if(mNextScreen != INVALIDATE_SCREEN){
			mCurrentScreen = Math.max(0, Math.min(mNextScreen, getScreenCount() - 1));
			scrollTo(mCurrentScreen * getWidth(), 0);
			mNextScreen = INVALIDATE_SCREEN;
		}
	}
	
	private void snapToScreen(int whichScreen, int velocity){
		whichScreen = Math.max(0, Math.min(whichScreen, getScreenCount() - 1));
		int screenWidth = getWidth();
		final int screenDelta = Math.abs(mCurrentScreen - whichScreen);
		final int newX = whichScreen * screenWidth;
		final int deltaX = newX - getScrollX();
		int duration = 0;
		
		if(!mScroller.isFinished()){
			mScroller.abortAnimation();
		}
		
		velocity = Math.max(mMinSnapVelocity, Math.abs(velocity));
		int tmpDuration = Math.abs(deltaX) * SCREEN_SNAP_DURATION / screenWidth;
		
		if(velocity > 0){
			tmpDuration += tmpDuration / (velocity / mBaseLineFlingVelocity) * mFlingVelocityInfluence;
		}
		duration = Math.max(SCREEN_SNAP_DURATION, tmpDuration);
		if(screenDelta <= 1) duration = Math.min(duration, SCREEN_SNAP_DURATION * 2);
		
		boolean changeScreen = whichScreen != mCurrentScreen;
		View focusedView = getFocusedChild();
		if(changeScreen && focusedView != null && focusedView == getChildAt(mCurrentScreen)){
			clearChildFocus(focusedView);
		}
		
		Log.i(TAG,"snapToScreen "
        		+", screenDelta="+screenDelta
        		+", newX="+newX
        		+", deltaX="+deltaX
        		+", whichScreen="+whichScreen
        		+", velocity="+velocity
        		+", duration="+duration);
		
		mNextScreen = whichScreen;
		
		mScroller.startScroll(getScrollX(), 0, deltaX, 0, duration);
		invalidate();
	}
	
	public void snapToScreen(int screenId){
    	if(screenId < 0 || screenId > getScreenCount() - 1){
    		return;
    	}
    	snapToScreen(screenId, 0);
    }
	
	private void aquireVelocityTrackerAndAddMotion(MotionEvent ev){
		if(mVelocityTracker == null){
			mVelocityTracker = VelocityTracker.obtain();
		}
		mVelocityTracker.addMovement(ev);
	}
	
    private void releaseVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }
	
	public int getScreenCount(){
		//return mScreenCount;
		return (int)((getChildCount() + (mVisibleRange - 1)) / mVisibleRange);
	}
	
    public static class OvershootInterpolator implements Interpolator {
        private static final float DEFAULT_TENSION = 1.3f;
        private float mTension;

        public OvershootInterpolator() {
            mTension = 0.0f;//DEFAULT_TENSION;
        }

        public void setDistance(int distance) {
            mTension = distance > 0 ? DEFAULT_TENSION / distance : DEFAULT_TENSION;
        }
        
        public void setTension(float tension){
        	mTension = tension;
        }

        public void disableSettle() {
            mTension = 0.f;
        }

        public float getInterpolation(float t) {
            // _o(t) = t * t * ((tension + 1) * t + tension)
            // o(t) = _o(t - 1) + 1
            t -= 1.0f;
            return t * t * ((mTension + 1) * t + mTension) + 1.0f;
        }
    }
}


相關文章