今天来写的是关于侧滑的,说道侧滑,我们可以想导用什么来实现呢?首先肯定是系统为我们写好的DrawerLayout来写,或者用ViewGroup再加上一些手势处理,下面我来写的是自定义view继承自我们的HorizontalScrollView。
先看一下实际的QQ效果和酷狗的实际效果
先说一下实现思路:
就是在自定义的ViewGroup下面放两个布局,一个是菜单布局,一个是内容布局,看看我们在代码中使用情况吧
activity_main:
<?xml version="1.0" encoding="utf-8"?>
<moocollege.cn.kugougouslidemenu.KuGouSlideMenu xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:background="@mipmap/home_bg_menu"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:menuRightMargin="100dp"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<include layout="@layout/layout_menu" />
<include layout="@layout/layout_content" />
</LinearLayout>
</moocollege.cn.kugougouslidemenu.KuGouSlideMenu>
不做任何处理的
public class KuGouSlideMenu extends HorizontalScrollView {
public KuGouSlideMenu(Context context) {
this(context, null);
}
public KuGouSlideMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public KuGouSlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
layout_menu
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/layout_menu_top"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_above="@+id/layout_bottom"
android:layout_marginTop="20dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/enter_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="23dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/user_head_iv"
android:layout_width="56dp"
android:layout_height="56dp"
android:src="@mipmap/toux2" />
<TextView
android:id="@+id/user_name_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="16dp"
android:text="登录/注册"
android:textColor="#FFFFFF"
android:textSize="18sp" />
</LinearLayout>
<ListView
android:id="@+id/menu_item_lv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:divider="@null"
android:dividerHeight="0dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_bottom"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentBottom="true"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:text="设置"
android:textColor="#FFFFFF"
android:textSize="18sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:text="退出"
android:textColor="#FFFFFF"
android:textSize="18sp" />
</LinearLayout>
</RelativeLayout>
layout_content
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/rl_content"
android:background="#FFFFFF"
android:orientation="vertical">
<ImageView
android:onClick="clickImage"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:src="@mipmap/toux2" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="酷狗主页内容"
android:textColor="@color/colorAccent"
android:textSize="30sp" />
</RelativeLayout>
上面这样布局会乱的
这就要我们在自定义view中为我们的两个布局去指定宽度,在onFinishInflate()中去处理,这个方法在解析完xml的时候会调用,
菜单的宽度 = 屏幕的宽度 - 菜单离右边屏幕的距离;
我们一进来菜单是默认关闭的,那么我们就要在onLayout中调用scroolTo(菜单宽度,0),类比我们的ScrooView来理解。
触摸事件处理,在我们的手指抬起的时候,是要关闭还是要打开呢。在这里注意我们当前滑动的距离和菜单的宽度的关系
酷狗中的菜单特效,缩放等一些效果和QQ侧滑中的内容布局的阴影我们在onScrooChanged()方法中去做,处理手势的快速滑动我们借助于系统提供的一个类GestureDetector
点击内容页面的时候我们要判断当前菜单是否关闭,如果需要关闭菜单,那么我们就要拦截子view的事件,在onInterCept中去做相应的处理,并且在拦截子view事件的时候,不要去触发我们自己的onTouc方法,具体看代码:
酷狗:
public class KuGouSlideMenu extends HorizontalScrollView {
//左边的菜单
private View mMenuView;
//主页的内容
private View mContentView;
private Context mContext;
//菜单的宽度
private int mMenuWidth;
//当前是否打开 默认一进来是关闭的
private boolean mMenuIsOpen = false;
private GestureDetector mGestureDetector; // 系统自带的手势处理类
private boolean mIntercept = false;
public KuGouSlideMenu(Context context) {
this(context, null);
}
public KuGouSlideMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public KuGouSlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
mGestureDetector = new GestureDetector(mContext, new GestureDetectorListener());
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.KuGouSlideMenu);
float rightMargin = array.getDimension(R.styleable.KuGouSlideMenu_menuRightMargin, ScreenUtils.dip2px(mContext, 50));
//菜单的宽度 = 屏幕的宽度-菜单离右边的距离
mMenuWidth = (int) (ScreenUtils.getScreenWidth(mContext) - rightMargin);
array.recycle();
}
/**
* 手势处理的监听类
*/
private class GestureDetectorListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// 快速滑动
// 向右快速滑动会是正的 + 向左快速滑动 是 -
// 如果菜单是打开的 向右向左快速滑动都会回调这个方法
if (mMenuIsOpen) {
if (velocityX < 0) {
closeMenu();
return true;
}
} else {
if (velocityX > 0) {
openMenu();
return true;
}
}
return false;
}
}
/**
* 1.这个方法在整个布局xml解析完毕走这个方法
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//获取菜单和主页内容
//我们在这里getChildAt(0)拿的是我们布局中的LinearLayout
ViewGroup container = (ViewGroup) getChildAt(0);
int childCount = container.getChildCount();
if (childCount != 2) {
//抛运行时异常,只能放置两个子view
throw new RuntimeException("You can only place two sub view");
}
//拿到我们的菜单布局
mMenuView = container.getChildAt(0);
//拿到我们的主页内容的布局
mContentView = container.getChildAt(1);
ViewGroup.LayoutParams layoutMenuParams = mMenuView.getLayoutParams();
//指定菜单的宽度
layoutMenuParams.width = mMenuWidth;
//7.0
mMenuView.setLayoutParams(layoutMenuParams);
ViewGroup.LayoutParams layoutContentParams = mContentView.getLayoutParams();
//指定内容的宽度 指定宽高后会重新摆放 在onLayout中
layoutContentParams.width = ScreenUtils.getScreenWidth(mContext);
mContentView.setLayoutParams(layoutContentParams);
}
//2 布局摆放 默认进来进来是关闭的
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
// 用来排放子布局的 等子View全部摆放完才能去滚动 我们一进来的时候默认是关闭菜单的
//类比纵向的ScrollVew的来理解
scrollTo(mMenuWidth, 0);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
mIntercept = false;
if (mMenuIsOpen) {
float currentX = ev.getX();
if (currentX > mMenuWidth) {
//关闭菜单
closeMenu();
//子view不响应任何事件 拦截子view的触摸事件
//如果返回true 代表会拦截子view的触摸事件,但是会相应自己的onTouch事件
mIntercept = true;
return true;
}
}
return super.onInterceptTouchEvent(ev);
}
//3 事件的拦截处理
@Override
public boolean onTouchEvent(MotionEvent ev) {
//获取手指滑动速率,获取手指滑动的速率,当期大于一定值就认为是快速滑动 , GestureDetector(系统提供好的类)
//当菜单打开的时候,手指触摸右边内容部分需要关闭菜单,还需要拦截事件(打开情况下点击内容页不会响应点击事件)
//这里保证了手势处理类的调用
//快速滑动了 下面的拦截事件就不要处理了、
if (mGestureDetector.onTouchEvent(ev)) {
return true;
}
//如果有拦截,则不执行自己的onTouch方法
if (mIntercept){
return true;
}
// 拦截处理事件
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_UP:
int currentScrollX = getScrollX();
//在这里注意currentScrollX的变化,当我们默认关闭菜单的时候去拉动ScrollView,数值在不断的变小
if (currentScrollX < mMenuWidth / 2) {
//打开菜单
openMenu();
} else {
//关闭菜单
closeMenu();
}
//确保super.onTouchEvent不会执行 这里看super.onTouchEvent源码中的fling方法
//和smoothScrollTo的源码
return true;
}
return super.onTouchEvent(ev);
}
//4 处理主页内容的缩放,左边的缩放和透明度的调节 这就需要不断的获取当前的滑动位置
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
//l是从mMuneWidth一直变化到0
//计算梯度值
float scale = 1f * l / mMenuWidth; //梯度从1逐渐变为0
//右边的缩放 最小0.7f 最大是1
float rightScale = 0.7f + 0.3f * scale;
//设置主页内容的缩放,默认是中心点缩放
//设置缩放的中心点
ViewCompat.setPivotX(mContentView, 0);
ViewCompat.setPivotY(mContentView, mContentView.getMeasuredHeight() / 2);
ViewCompat.setScaleX(mContentView, rightScale);
ViewCompat.setScaleY(mContentView, rightScale);
//设置菜单的缩放和透明度 从半透明到完全透明 0.5f到1.0f
float menuAlpha = 0.5f + (1 - scale) * 0.5f;
ViewCompat.setAlpha(mMenuView, menuAlpha);
//缩放处理
float menuScale = 0.7f + (1 - scale) * 0.3f;
ViewCompat.setScaleX(mMenuView, menuScale);
ViewCompat.setScaleY(mMenuView, menuScale);
//设置平移 l*0.7f
ViewCompat.setTranslationX(mMenuView, 0.25f * l);
}
/**
* 关闭菜单
*/
private void closeMenu() {
smoothScrollTo(mMenuWidth, 0);
mMenuIsOpen = false;
}
/**
* 打开菜单
*/
private void openMenu() {
smoothScrollTo(0, 0);
mMenuIsOpen = true;
}
}
QQ侧滑代码:
public class QQSlider extends HorizontalScrollView {
//左边菜单布局
private View mLeftMenu;
//内容布局
private View mContentView;
//菜单是否打开
private boolean mMenuIsOpen;
//是否拦截事件
private boolean mIntercept;
//手势处理类
private GestureDetector mGestureDetector;
//菜单的宽度
private int mMenuWidth;
private View mShadeView;
private Context mContext;
public QQSlider(Context context) {
this(context,null);
}
public QQSlider(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public QQSlider(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QQSlider);
//菜单离右边屏幕边缘的距离
float rightMargin = array.getDimension(R.styleable.QQSlider_qqMenuRightMargin, ScreenUtils.dip2px(mContext, 50));
mMenuWidth = (int) (ScreenUtils.getScreenWidth(mContext) - rightMargin);
array.recycle();
mGestureDetector = new GestureDetector(mContext, mGestureDetectorListener);
}
private GestureDetector.SimpleOnGestureListener mGestureDetectorListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// 快速滑动
// 向右快速滑动会是正的 + 向左快速滑动 是 -
// 如果菜单是打开的 向右向左快速滑动都会回调这个方法
if (mMenuIsOpen) {
if (velocityX < 0) {
closeMenu();
return true;
}
} else {
if (velocityX > 0) {
openMenu();
return true;
}
}
return super.onFling(e1, e2, velocityX, velocityY);
}
};
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//拿到根布局
ViewGroup rootView = (ViewGroup) getChildAt(0);
int childCount = rootView.getChildCount();
if (childCount != 2)
throw new RuntimeException("You can only place two sub view in the root");
//拿到菜单布局
mLeftMenu= rootView.getChildAt(0);
//指定菜单的宽度
ViewGroup.LayoutParams mLeftMenuLayoutParams = mLeftMenu.getLayoutParams();
mLeftMenuLayoutParams.width = mMenuWidth;
mLeftMenu.setLayoutParams(mLeftMenuLayoutParams);
//拿到内容布局
mContentView= rootView.getChildAt(1);
//指定内容的宽度
ViewGroup.LayoutParams mContentLayoutParams = mContentView.getLayoutParams();
//把内容布局单读提出来
rootView.removeView(mContentView);
//在外面套一层阴影
RelativeLayout contentContainer = new RelativeLayout(mContext);
contentContainer.addView(mContentView);
mShadeView = new View(mContext);
mShadeView.setBackgroundColor(Color.parseColor("#55000000"));
contentContainer.addView(mShadeView);
//把容器放回去
mContentLayoutParams.width = ScreenUtils.getScreenWidth(mContext);
contentContainer.setLayoutParams(mContentLayoutParams);
rootView.addView(contentContainer);
mShadeView.setAlpha(0.0f);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
scrollTo(mMenuWidth,0);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
mIntercept = false;
if (mMenuIsOpen) {
float currentX = ev.getX();
if (currentX > mMenuWidth) {
//关闭菜单
closeMenu();
//子view不响应任何事件 拦截子view的触摸事件
//如果返回true 代表会拦截子view的触摸事件,但是会相应自己的onTouch事件
mIntercept = true;
return true;
}
}
return super.onInterceptTouchEvent(ev);
}
//3 事件的拦截处理
@Override
public boolean onTouchEvent(MotionEvent ev) {
//获取手指滑动速率,获取手指滑动的速率,当期大于一定值就认为是快速滑动 , GestureDetector(系统提供好的类)
//当菜单打开的时候,手指触摸右边内容部分需要关闭菜单,还需要拦截事件(打开情况下点击内容页不会响应点击事件)
//这里保证了手势处理类的调用
//快速滑动了 下面的拦截事件就不要处理了、
if (mGestureDetector.onTouchEvent(ev)) {
return true;
}
//如果有拦截,则不执行自己的onTouch方法
if (mIntercept){
return true;
}
// 拦截处理事件
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_UP:
int currentScrollX = getScrollX();
//在这里注意currentScrollX的变化,当我们默认关闭菜单的时候去拉动ScrollView,数值在不断的变小
if (currentScrollX < mMenuWidth / 2) {
//打开菜单
openMenu();
} else {
//关闭菜单
closeMenu();
}
//确保super.onTouchEvent不会执行 这里看super.onTouchEvent源码中的fling方法
//和smoothScrollTo的源码
return true;
}
return super.onTouchEvent(ev);
}
/**
* 关闭菜单
*/
private void closeMenu() {
smoothScrollTo(mMenuWidth, 0);
mMenuIsOpen = false;
}
/**
* 打开菜单
*/
private void openMenu() {
smoothScrollTo(0, 0);
mMenuIsOpen = true;
}
//4 处理主页内容的,这就需要不断的获取当前的滑动位置
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
//l是从mMuneWidth一直变化到0
//计算梯度值
float scale = 1f * l / mMenuWidth; //梯度从1逐渐变为0
//控制阴影 从0变化到1
float alphaScale = 1 -scale;
mShadeView.setAlpha(alphaScale);
ViewCompat.setTranslationX(mLeftMenu, 0.6f * l);
}
}
来看一下我们自己实现的效果:
附上github地址:点击打开链接