前文
之前看到酷狗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>
複製程式碼
- app:sml_content_id 內容控制元件id
- app:sml_menu_id 選單控制元件id
- app:sml_scale_mode 是否啟用內容控制元件隨動縮放
常用方法
- animToOpen() 執行開啟選單動畫
- animToClose() 執行關閉選單動畫
- isOpen() 當前是否處於選單開啟狀態
本文程式碼:github.com/Kongdy/Slid…
個人github地址:github.com/Kongdy
個人掘金主頁:juejin.im/user/595a64…
csdn主頁:blog.csdn.net/u014303003