Android 自定义上拉菜单和侧滑菜单
2018-11-29 本文已影响0人
果园_
一、概述
在Android的support库中有一个专门的侧滑菜单组件DrawerLayout,它的使用非常简单,效果也非常灵活。
但在实际开发中有很多特殊的要求,这时候就需要我们自己去实现效果了。
本文通过ViewDragHelper和对事件分发的一些处理实现了侧滑菜单和类似高德地图的可拉伸抽屉菜单。
大家可以根据类似的思路去定制自己所需要的控件。
二、效果展示
侧滑菜单.gif 上拉菜单.gif三、代码实现
1.主布局的实现
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/myMenu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:stateListAnimator="@null"
android:text="菜单" />
<com.example.slideview.MyViewDragHelper
android:id="@+id/myCustom"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:orientation="horizontal"
android:visibility="gone">
<LinearLayout
android:id="@+id/myLl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:orientation="horizontal">
<com.example.slideview.FragEdge
android:id="@+id/myBlankView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1000" />
<com.example.slideview.MyScroView
android:id="@+id/myScroView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toRightOf="@id/myBlankView"
android:background="@color/colorPrimary">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:background="#FFFF22">
<TextView
android:id="@+id/myTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="标题栏"
android:textSize="40sp" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginTop="570dp"
android:background="#2FFF22">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="底部栏"
android:textSize="40sp" />
</RelativeLayout>
</LinearLayout>
</com.example.slideview.MyScroView>
</LinearLayout>
</com.example.slideview.MyViewDragHelper>
</RelativeLayout>
布局的嵌套关系
嵌套布局.pngFragEdge的实现
package com.example.slideview;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.FrameLayout;
/**
* Created by 果园 on 21/11/2018.
*/
public class FragEdge extends FrameLayout {
public FragEdge(@NonNull Context context) {
super(context);
}
public FragEdge(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public FragEdge(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private int mylastx;
private int mylasty;
public boolean dispatchTouchEvent(MotionEvent ev) {
int y = (int) ev.getY();
int x = (int) ev.getX();
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
int moveX = x - mylastx;
int moveY = y - mylasty;
if (Math.abs(moveX) > Math.abs(moveY)) {
getParent().getParent().requestDisallowInterceptTouchEvent(false);
} else {
getParent().getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_DOWN:
mylastx = x;
mylasty = y;
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return super.dispatchTouchEvent(ev);
}
}
FragEdge的主要作用是在点击菜单外部的时候将菜单收回。
实现方式是通过内部拦截将点击事件和滑动事件分开处理。
MyScroView的实现
package com.example.slideview;
import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.v4.widget.NestedScrollView;
import android.util.AttributeSet;
import android.view.MotionEvent;
/**
* Created by 果园 on 13/11/2018.
*/
public class MyScroView extends NestedScrollView {
public MyScroView(Context context) {
this(context, null);
}
public MyScroView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyScroView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private int mylastx;
private int mylasty;
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int y = (int) ev.getY();
int x = (int) ev.getX();
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
int moveX = x - mylastx;
int moveY = y - mylasty;
if (Math.abs(moveX) > Math.abs(moveY)) {
getParent().getParent().requestDisallowInterceptTouchEvent(false);
} else {
getParent().getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_DOWN:
mylastx = x;
mylasty = y;
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
System.out.println("Scrol");
return super.onTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
}
MyScroView的实现方式和FragEdge一样。
里面可以嵌套RecyclerView,在上拉菜单中只需要在列表滑动到顶部的时候处理滑动事件即可。
上拉菜单中事件的处理
package com.example.slideview;
import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.v4.widget.NestedScrollView;
import android.util.AttributeSet;
import android.view.MotionEvent;
/**
* Created by 果园 on 13/11/2018.
*/
public class MyScroView extends NestedScrollView {
private float myFlag;
public boolean myScroSign = false;
public MyScroView(Context context) {
this(context, null);
}
public MyScroView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyScroView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private float lastY;
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
myFlag = lastY - ev.getY();
if (lastY > ev.getY()) {
if (!canScrollVertically(1)) {
getParent().requestDisallowInterceptTouchEvent(false);
return false;
} else {
getParent().requestDisallowInterceptTouchEvent(true);
}
} else if (ev.getY() > lastY) {
if (!canScrollVertically(-1)) {
getParent().requestDisallowInterceptTouchEvent(false);
return false;
} else {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
break;
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
lastY = ev.getY();
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (lastY > ev.getY()) {
return myScroSign;
} else if (ev.getY() > lastY) {
return myScroSign;
}
return super.onInterceptTouchEvent(ev);
}
}
MyViewDragHelper的实现
package com.example.slideview;
import android.content.Context;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
/**
* Created by 果园 on 12/11/2018.
*/
public class MyViewDragHelper extends LinearLayout {
private ViewDragHelper mDragHelper;
private View mDragView;
private FragmentEdge myBlankView;
private LinearLayout autoTextView;
private int myleft;
int screenWidth;
private MyScroView myScroView;
public MyViewDragHelper(Context context) {
super(context);
initView();
}
public MyViewDragHelper(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView();
}
public MyViewDragHelper(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
mDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return mDragView == child;
}
//按压状态的处理:0和1
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
}
//平滑滑动的时候返货的xy:支持判断快速滑动
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
if (xvel > 0) {
translateScreenWidth();
} else if (myleft > screenWidth / 3 && myleft > 0) {
translateScreenWidth();
} else if (myleft < screenWidth / 3 && myleft > 0) {
translateEdge();
}
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
myleft = left;
Log.e("距离", left + "");
if (myleft == screenWidth) {
MainActivity.yinc();
}
}
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return Math.max(Math.min(left, screenWidth), 0);
}
});
}
@Override
public void computeScroll() {
super.computeScroll();
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(MyViewDragHelper.this);
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
autoTextView = findViewById(R.id.myLl);
myBlankView = findViewById(R.id.myBlankView);
myScroView = findViewById(R.id.myScroView);
mDragView = autoTextView;
myBlankView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mDragHelper.smoothSlideViewTo(autoTextView, screenWidth, 0);
ViewCompat.postInvalidateOnAnimation(MyViewDragHelper.this);
}
});
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mDragHelper.cancel();
return false;
} else if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onTouchEvent(ev);
return false;
}
return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
mDragHelper.processTouchEvent(ev);
return true;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
}
public void getHeight(int screenWidth) {
this.screenWidth = screenWidth;
}
public void showMyMenu() {
translateEdge();
setShowSize();
}
private void translateScreenWidth() {
mDragHelper.smoothSlideViewTo(autoTextView, screenWidth, 0);
ViewCompat.postInvalidateOnAnimation(MyViewDragHelper.this);
}
private void translateEdge() {
mDragHelper.smoothSlideViewTo(autoTextView, 0, 0);
ViewCompat.postInvalidateOnAnimation(MyViewDragHelper.this);
}
private void setShowSize() {
LinearLayout.LayoutParams layoutParams = (LayoutParams) myScroView.getLayoutParams();
layoutParams.width = (screenWidth / 4) * 3;
myScroView.setLayoutParams(layoutParams);
}
}
代码中写了简单的注释。
onViewReleased方法中,xvel和yvel是分别鉴别手势快速滑动的方向,滑动回弹的效果看代码逻辑。
onViewPositionChanged可以获得实时的滑动距离,根据业务逻辑做不同的处理。
侧滑菜单需要实现clampViewPositionHorizontal,上拉菜单需要实现clampViewPositionVertical。
可以使用本地的ViewDragHelper类,操作就是在jar包中将其拷贝到本地,这样做的目的
是可以自定义其属性。
例如回弹速度差值器的实现
private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
if (forParent == null) {
throw new IllegalArgumentException("Parent view may not be null");
}
if (cb == null) {
throw new IllegalArgumentException("Callback may not be null");
}
mParentView = forParent;
mCallback = cb;
final ViewConfiguration vc = ViewConfiguration.get(context);
final float density = context.getResources().getDisplayMetrics().density;
mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);
mTouchSlop = vc.getScaledTouchSlop();
mMaxVelocity = vc.getScaledMaximumFlingVelocity();
mMinVelocity = vc.getScaledMinimumFlingVelocity();
mScroller = new OverScroller(context, new AccelerateInterpolator());
}
我们可以根据自己的需求去重写各种效果。
主方法的调用
package com.example.slideview;
import android.content.Context;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private static MyViewDragHelper myDrawView;
private static Button myShowView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myDrawView = findViewById(R.id.myCustom);
myShowView = findViewById(R.id.myMenu);
getHight();
myShowView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
myDrawView.setVisibility(View.VISIBLE);
myDrawView.showMyMenu();
}
});
}
public static void yinc() {
myDrawView.setVisibility(View.GONE);
}
public void getHight() {
WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
int screenWidth = wm.getDefaultDisplay().getWidth();
myDrawView.getHeight(screenWidth);
}
}
总结
控件的整体实现方法并不难。
我们也可以将所有的资源放到自定义的Dialog上面去展示,再去单独的处理滑动事件从而做到进一步的封装。
欢迎大家的讨论。