帶有視覺滾動差的選單側滑欄

Kongdy發表於2018-01-11

前文

之前看到酷狗app的側滑欄比較有有意思,帶有視覺滾動差還有縮放效果,自己就嘗試的實現了一個。
這個元件其實可以使用HorScrollView實現,但是使用HorScrollView終歸還是要重寫觸控事件的,並且HorScrollView對這個控制元件沒有任何幫助,不如使用更輕量級的ViewGroup來實現。

我們先來看看效果

帶有視覺滾動差的側滑選單

如何實現視覺滾動差效果

我的實現方法比較笨,在layout根據一個滑動引數offset來進行layout的錯位增量。
layout的程式碼如下

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // to layout menu view and content view
        if (contentView != null) {
            int contentHeight = contentView.getMeasuredHeight();
            final int contentLeft = (int) (l + slideOffset * MAX_DRAG_FACTOR * (r - l));
            final int contentRight = contentLeft + contentView.getMeasuredWidth();
            final int contentTop = t + (b - t - contentHeight) / 2;
            final int contentBottom = contentTop + contentHeight;
            contentView.layout(contentLeft, contentTop, contentRight, contentBottom);
        }
        if (slideMenuView != null) {
            final int slideMenuWidth = slideMenuView.getMeasuredWidth();
            final int slideMenuHeight = slideMenuView.getMeasuredHeight();
            // 視覺滾動差效果
            final int menuLeft = (int) (l - (1 - slideOffset) * MAX_DRAG_FACTOR * (r - l) * slideMenuParallaxOffset);
            final int menuRight = menuLeft + slideMenuWidth;
            slideMenuView.layout(menuLeft, t, menuRight, t + slideMenuHeight);
        }
    }
複製程式碼

一個contentView,一個menuView,分別進行layout,但是他們從哪裡被控制元件獲取到的呢?或者說,控制元件怎麼知道哪個是contentView,哪個是menuVIew?這裡,我採用了根據attr獲取子控制元件id的方法。如下圖所示

 <com.kongdy.slidemenulib.SlideMenuLayout
        android:id="@+id/sml_menu"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorAccent"
        app:sml_content_id="@+id/cl_content"
        app:sml_menu_id="@+id/cl_slide_menu"
        app:sml_scale_mode="true">
			
			<android.support.constraint.ConstraintLayout
	            android:id="@id/cl_content">
	            ...
			</android.support.constraint.ConstraintLayout>

			 <android.support.constraint.ConstraintLayout
		            android:id="@id/cl_slide_menu">
		            ...
            </android.support.constraint.ConstraintLayout>

        </com.kongdy.slidemenulib.SlideMenuLayout>
複製程式碼


把menuView和contentView的控制元件id分別賦值到屬性中。然而這裡並沒有結束,因為,我們在構造方法中獲取到了兩個id,但是我們並拿不到這兩個控制元件,因為佈局還沒有inflate完畢。但是,還好,安卓為我們提供了這個方法。如下:

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        slideMenuView = findViewById(slideMenuId);
        contentView = findViewById(contentViewId);

        if (null != contentView)
            bringChildToFront(contentView);
    }

複製程式碼

這裡還用到了bringChildToFront這個方法,這是viewGroup提供的一個方法,我們來看看這個方法:

 @Override
    public void bringChildToFront(View child) {
        final int index = indexOfChild(child);
        if (index >= 0) {
            removeFromArray(index);
            addInArray(child, mChildrenCount);
            child.mParent = this;
            requestLayout();
            invalidate();
        }
    }
複製程式碼


這個方法把目標子view從childiList中取出來,然後重新放到childList的最後端,那麼viewGroup正在渲染它的時候,就會把它放到最後渲染上去,也就會顯示在最上層。這麼一來,就可以保證我們的contentView一直在我們當前viewGroup的最上層顯示。

處理觸控事件

之前在android圖片裁剪拼接實現(二):觸控實現 中講解了觸控的流程。在本控制元件中,viewGroup的分發機制已經很完善,我們不需要去重寫dispatchTouchEvent,只需要去寫onInterceptTouchEvent來判斷是否去攔截。onInterceptTouchEvent的程式碼如下:

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // handle weather intercept touch event
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN: {
                Rect rect = new Rect();
                contentView.getDrawingRect(rect);

                final int touchDownX = (int) ev.getX();
                final int touchDownY = (int) ev.getY();

                if (rect.contains(touchDownX, touchDownY)) {
                    viewMode = VIEW_MODE_TOUCH;
                    return true;
                }
            }
            break;
            case MotionEvent.ACTION_MOVE: {
                if (viewMode == VIEW_MODE_DRAG)
                    return true;
                if (viewMode == VIEW_MODE_TOUCH) {
                    Rect rect = new Rect();
                    contentView.getDrawingRect(rect);

                    final int touchDownX = (int) ev.getX();
                    final int touchDownY = (int) ev.getY();

                    if (rect.contains(touchDownX, touchDownY)) {
                        viewMode = VIEW_MODE_DRAG;
                        final ViewParent viewParent = getParent();
                        if (viewParent != null)
                            viewParent.requestDisallowInterceptTouchEvent(false);
                        return true;
                    } else {
                        resetTouchMode();
                    }
                }
            }
            break;
        }
        return super.onInterceptTouchEvent(ev);
    }
複製程式碼


這裡首先判斷了觸控落下的點是否在contentView之內,然後再判斷首次滑動的的觸控點是否仍然在contentView之內,如果兩個都符合的話,就呼叫requestDisallowInterceptTouchEvent方法請求父控制元件不要攔截自己接下來的觸控事件,並且返回true,此次的觸控事件交給viewGroup的touchEvent來處理。以下是touchEvent裡面的處理程式碼:


 @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                preTouchX = event.getX();
                preTouchY = event.getY();
                isClickEvent = true;
                break;
            case MotionEvent.ACTION_MOVE: {
                final float currentTouchX = event.getX();
                final float offsetX = currentTouchX - preTouchX;
                if(Math.abs(offsetX) > touchSlop || !isClickEvent) {
                    isClickEvent = false;
                    int contentLeft = contentView.getLeft();
                    int preCalcLeft = (int) (contentLeft + offsetX);
                    if (preCalcLeft >= 0 && preCalcLeft <= getWidth() * MAX_DRAG_FACTOR) {
                        slideOffset = preCalcLeft / (getWidth() * MAX_DRAG_FACTOR);
                        reDraw();
                    }
                    preTouchX = currentTouchX;
                } else {
                    isClickEvent = true;
                }
            }
            break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP: {
                Rect contentViewRect = new Rect();
                contentView.getDrawingRect(contentViewRect);
                if(isClickEvent && isOpen() && contentViewRect.contains((int)event.getX(),(int)event.getY())) {
                    animToClose();
                } else {
                    int contentLeft = contentView.getLeft();
                    final int currentWidth = getWidth();
                    final int halfWidth = currentWidth / 2;
                    int animFactor = (contentLeft + halfWidth) / currentWidth;
                    if (animFactor > 0) {
                        animToOpen();
                    } else {
                        animToClose();
                    }
                    resetTouchMode();
                }
            }

            break;
        }
        return true;
    }

複製程式碼


這裡我們先計算出本次觸控點與上一次觸控點移動的距離offsetX,然後判斷這個offsetX是否大於touchSlop,touchSlop是在構造方法中,從系統中獲取到的滑動最小值。當超過這個值得時候,我們判定為滑動,並且將isClickEvent置為false,否則isClickEvent置為ture,相當於點選事件。隨後進入拖動狀態,我們要預計算出contentView的left值,如果這個值小於左邊的邊界,或者大於向右邊的最大滑動距離都不被允許,雖然把這個preCalcLeft的預計算left根據引數計算成當前滑動的位移率來供全域性使用。

動畫

最後,我們在觸控抬起或者取消的時候,要做一個滑動動畫,動畫的實現方式很簡單,我這裡貼出程式碼即可:

   public void animToClose() {
        if (viewMode == VIEW_MODE_ANIM)
            return;
        viewMode = VIEW_MODE_ANIM;
        Animator valueAnimator = createValueAnim(slideOffset, 0f, SLIDE_MODE_CLOSE);
        valueAnimator.start();
    }

    public void animToOpen() {
        if (viewMode == VIEW_MODE_ANIM)
            return;
        viewMode = VIEW_MODE_ANIM;
        Animator valueAnimator = createValueAnim(slideOffset, 1f, SLIDE_MODE_OPEN);
        valueAnimator.start();
    }

    private Animator createValueAnim(float startValue, float endValue, final int result_mode) {
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(startValue, endValue);
        valueAnimator.setDuration(DEFAULT_ANIMATION_TIME);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                slideOffset = (float) animation.getAnimatedValue();
                reDraw();
            }
        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                viewMode = VIEW_MODE_IDLE;
                slideMode = result_mode;
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                viewMode = VIEW_MODE_IDLE;
                slideMode = result_mode;
            }
        });
        return valueAnimator;
    }
複製程式碼

如何使用

首先在自己的專案的根目錄的gradle下新增:

	allprojects {
		repositories {
			...
			maven { url 'https://jitpack.io' }
		}
	}
複製程式碼


隨後新增依賴:

dependencies {
		compile 'com.github.Kongdy:SlideMenuLayout:v1.0.2'
	}
複製程式碼


在專案中,xml標籤裡面如下宣告:

 <com.kongdy.slidemenulib.SlideMenuLayout
        android:id="@+id/sml_menu"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorAccent"
        app:sml_content_id="@+id/cl_content"
        app:sml_menu_id="@+id/cl_slide_menu"
        app:sml_scale_mode="true">
			
			<android.support.constraint.ConstraintLayout
	            android:id="@id/cl_content">
	            ...
			</android.support.constraint.ConstraintLayout>

			 <android.support.constraint.ConstraintLayout
		            android:id="@id/cl_slide_menu">
		            ...
            </android.support.constraint.ConstraintLayout>

        </com.kongdy.slidemenulib.SlideMenuLayout>
複製程式碼
  1. app:sml_content_id 內容控制元件id
  2. app:sml_menu_id 選單控制元件id
  3. app:sml_scale_mode 是否啟用內容控制元件隨動縮放

常用方法

  1. animToOpen() 執行開啟選單動畫
  2. animToClose() 執行關閉選單動畫
  3. isOpen() 當前是否處於選單開啟狀態

本文程式碼:github.com/Kongdy/Slid…
個人github地址:github.com/Kongdy
個人掘金主頁:juejin.im/user/595a64…
csdn主頁:blog.csdn.net/u014303003

相關文章