第五章 Android Scroll 分析

Android 座標系

將屏幕的左上角的頂點作爲Android座標系的原點,從這個點向右是 x 軸正方向,向下是 y 軸正方向。

getRawX()getRawY()獲得的座標是Android座標系上的座標。

視圖座標系

描述子視圖在父視圖的位置關係,視圖座標系同樣是從原點向右是 x 軸正方向,向下是 y 軸正方向。,原點不再是屏幕的左上角,而是父佈局的左上角爲座標原點。

getX()getY()所獲得的座標是視圖座標系中的座標。

觸控事件 —— MotionEvent (P91)

getTop():獲取到的是View自身的頂邊到父佈局的距離。
getLeft()getRight():。
getBottom():獲取到的是View自身的底邊到父佈局的距離。
getX():獲取點擊事件距離控件左邊的距離,即視圖座標。
getRawX():獲取點擊事件距離整個屏幕的左邊的距離,即絕對座標。
getScrollX():View 左邊緣和 View 內容左邊緣在水平方向上的距離。

控件滑動

public class DragView extends View {
    private static final String TAG = "DragView";
    private Scroller mScroller;
    public DragView(Context context) {
        this(context, null);
    }

    public DragView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initData(context);
    }

    private void initData(Context context) {
        mScroller = new Scroller(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(getViewMeasuredWidth(widthMeasureSpec), getViewMeasuredWidth(heightMeasureSpec));
    }

    private int getViewMeasuredHeight(int heightMeasureSpec) {
        return 0;
    }

    private int getViewMeasuredWidth(int widthMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int result = 0;
        switch (widthMode){
            case MeasureSpec.EXACTLY:
                Log.i(TAG, "EXACTLY: ");
                break;
            case MeasureSpec.AT_MOST:  //wrap_content
                Log.i(TAG, "AT_MOST: ");
                result = 200;
                result = Math.min(result, widthSize);
                break;
        }
        return result;
    }

    private int lastX;
    private int lastY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        /**
         * 通過layout方法使得View達到滑動效果
         *   getX() 獲取的是點擊事件距離控件左邊的距離,即視圖座標
         *   getRawX() 獲取的是點擊事件距離整個屏幕左邊的距離,即絕對距離
         */
        //textGetXY(event);
        //textGetRawXY(event);
        /**
         * 通過使用offsetLeftAndRight(offsetX)使得View達到平移的效果
         */
        //textOffset(event);
        /**
         * 使用LayoutParams使得view達到平滑效果
         */
        //textLayoutParams(event);
        /**
         * 使用scrollBy使得view達到平滑效果
         */
        //textScrollToOrBy(event);

        textScrollSlowly(event);

        return true;
    }

    private void textScrollSlowly(MotionEvent event) {
        int startX = (int) event.getRawX();
        int startY = (int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = startX - lastX;
                int offsetY = startY - lastY;
                ((View)getParent()).scrollBy(-offsetX, -offsetY);
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_UP:
                View viewGroup = (View) getParent();
                /**
                 * getScrollX(): 總是等於View左邊緣和View內容左邊緣在水平方向上的距離
                 *      要注意正負情況和ScrollBy()、ScrollTo()是一樣的
                 *
                 *  viewGroup.getScrollX()獲得的是當前viewGroup左邊界和ViewGroup內容左邊緣在水平方向上的距離,
                 *              也就是當前view左邊界和ViewGroup左邊界的距離,如果view沒有滑動前在(0,0),那麼現在就是滑動後的座標的相反數
                 *
                 *     -viewGroup.getScrollX(), -viewGroup.getScrollY() 讓滑動後的view再回原來位置
                 */
                mScroller.startScroll(viewGroup.getScrollX(), viewGroup.getScrollY(), -viewGroup.getScrollX(), -viewGroup.getScrollY());
                invalidate();
                break;
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()){
            ((View)getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

    private void textScrollToOrBy(MotionEvent event) {
        int startX = (int) event.getRawX();
        int startY = (int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = startX - lastX;
                int offsetY = startY - lastY;
                /**
                 * 並不能移動,因爲scrollTo、scrollBy移動的是view的內容
                 */
                //scrollBy(offsetX, offsetY);
                /**
                 * 移動了ViewGroup中的所有子view
                 *
                 * scrollBy :視圖移動的知識,相當於:view控件固定在手機屏幕上,而屏幕下是一個巨大的畫布,也就是我們想要展示的視圖,
                 *                  scrollBy僅僅是移動了view控件和手機屏幕,而view中的文本是畫在畫布上的,沒有隨着控件和手機屏
                 *                  幕滑動。     因此,要用-offsetX, -offsetY
                 */
                ((View)getParent()).scrollBy(-offsetX, -offsetY);
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
    }


    private void textLayoutParams(MotionEvent event) {
        int startX = (int) event.getRawX();
        int startY = (int) event.getRawY();
        ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = startX - lastX;
                int offsetY = startY - lastY;
                /**
                 * 在這裏使用getLeft()使得view滑動紊亂  layoutParams.leftMargin才行
                 */
                layoutParams.leftMargin = layoutParams.leftMargin + offsetX;
                layoutParams.topMargin = layoutParams.topMargin + offsetY;
                setLayoutParams(layoutParams);
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
    }

    /**
     *  offsetLeftAndRight(offsetX)、offsetTopAndBottom(offsetY)
     *  相當於對左右、上下平移的一個封裝
     * @param event
     */
    private void textOffset(MotionEvent event) {
        int startX = (int) event.getRawX();
        int startY = (int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = startX - lastX;
                int offsetY = startY - lastY;
                offsetLeftAndRight(offsetX);
                offsetTopAndBottom(offsetY);
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
    }

    /**
     * 使用getRawX()的時候,每次執行完ACTION_MOVE需要對lastX重新賦值,爲什麼?
     *    因爲getRawX()獲取的是點擊事件距離整個屏幕左邊的距離,即絕對距離,在控件滑動的時候,lastX時刻發生變化
     * @param event
     */
    private void textGetRawXY(MotionEvent event) {
        int startX = (int) event.getRawX();
        int startY = (int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = startX - lastX;
                int offsetY = startY - lastY;
                layout(getLeft()+offsetX, getTop()+offsetY, getRight()+offsetX, getBottom()+offsetY);
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
    }

    /**
     * 使用getX()的時候不需要每次執行完ACTION_MOVE重新賦值,爲什麼?
     *     原因:由於ACTION_DOWN中的lastX是當前按壓點到當前控件左邊界的距離,
     *          而當手指移動一個像素,控件也應該移動一個像素,所以lastX應該是不變的,所以不需要重新賦值。
     */
    private void textGetXY(MotionEvent event) {
        int startX = (int) event.getX();
        int startY = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = startX - lastX;
                int offsetY = startY - lastY;
                layout(getLeft()+offsetX, getTop()+offsetY, getRight()+offsetX, getBottom()+offsetY);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
    }
}

ViewDragView–實現類似QQ側邊欄的效果

public class DragViewGroup extends FrameLayout {

    private static final String TAG = "DragViewGroup";

    private ViewDragHelper mViewDragHelper;
    private View mMainView,mMenuView;
    private int mWidth;

    public DragViewGroup(@NonNull Context context) {
        this(context, null);
    }

    public DragViewGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragViewGroup(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initData();
    }

    private void initData() {
        /**
         * ViewDragHelper的構造方法是私有的
         */
        mViewDragHelper = ViewDragHelper.create(this, mCallback);
    }

    /**
     * 當佈局的xml文件加載完成後調用該方法
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMenuView = getChildAt(0);
        mMainView = getChildAt(1);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = getMeasuredWidth();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        /**
         * 把觸摸事件傳遞給ViewDragHelper,這一步必不可少
         */
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

    private ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {

        /**
         * 剛觸摸屏幕的時候就調用該方法,用於判斷何時開始檢測屏幕,
         *     該例中當觸摸到的View爲mMainView的時候返回true,表示開始檢測屏幕
         */
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return mMainView == child;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return super.clampViewPositionVertical(child, top, dy);
        }

        /**
         * 觸摸屏幕的時候不斷調用該方法
         */
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return left;
        }

        /**
         * 拖動結束後鬆開會調用該方法
         */
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            if (mMainView.getLeft() < mWidth/2){
                /**
                 * 與下面兩個方法類似:
                 *     mScroller.startScroll(x, y, dx, dy);
                 *     invalidate();
                 *
                 *   查看下面兩個方法的源碼,內部也是調用的 mScroller.startScroll()、invalidate();
                 *  那麼就得重寫computeScroll()
                 */
                mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }else {
                mViewDragHelper.smoothSlideViewTo(mMainView, mWidth/3, 0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }
        }

        /**
         * 在用戶觸摸到view後回調
         */
        @Override
        public void onViewCaptured(View capturedChild, int activePointerId) {
            super.onViewCaptured(capturedChild, activePointerId);
            Log.i(TAG, "onViewCaptured: ");
        }

        /**
         * 拖曳狀態發生改變的時候回調,整個過程只調用三次,如果單擊的話調用兩次
         *      1:手指剛開始按壓屏幕拖拽的時候回調(1)
         *      2:手指剛離開屏幕的時候回調(2),離開後view停止滑動的時候回調(3)。
         *      3:手指一直按壓在屏幕上的時候,此時就算停止拖動,或者停止後再拖動都不會調用該方法
         *
         * @param state
         */
        @Override
        public void onViewDragStateChanged(int state) {
            super.onViewDragStateChanged(state);
            Log.i(TAG, "onViewDragStateChanged: ");
        }

        /**
         * 拖動的時候View位置發生改變的時候回調該方法
         */
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            Log.i(TAG, "onViewPositionChanged: ");
        }
    };

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mViewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章