说明
前边几节课我们已经把自定义View和自定义ViewGroup的套路及源码已经讲解完了,那么我们接下来就会带大家去做一些炫酷的效果,来充分把我们之前所讲解的这一波理论知识来实践一遍,来操作一遍,要不然如果只是讲解理论知识,而不去实践,那么我觉得这个是没什么卵用的,废话不多说,下边就开始我们今天的课程,我们今天的任务就是做一个 —— 酷狗侧滑菜单效果。思路分析
目前像这种侧边栏滑动的效果可以实现的方式有很多,我们就来罗列下几种常用的方式。
2.1 直接用系统的DrawerLayout 这个控件一般直接是用作侧滑效果
2.2 自定义ViewGroup + 手势处理类 (处理比较麻烦,代码也比较多)
2.3 自定义ScrollView代码实现分析
3.1 自己定义一个类继承HorizontalScrollView,写好两个布局文件(menu,content),表示侧边栏的布局文件和主页面的布局文件,运行后看看效果;
3.2 运行后布局全是乱套的,menu和content的宽度不对,我们的应对方法就是:content宽度就是屏幕宽度,menu宽度就是屏幕宽度 - 最右边的一段距离(可以用自定义属性);
3.3 该侧滑默认是关闭的,手指抬起的时候要判断是关闭的还是打开的状态,然后用代码滚动到它对应的位置;
3.4 处理快速滑动;
3.5 处理内容部分缩放,菜单部分有位移和透明度,需要时时刻刻监听当前的滚动位置;
3.6 充分考虑前几次看的源码(理论+实践)
在这里需要注意一个问题,就是菜单打开和菜单关闭时候的getScrollX()的值,可能有些小伙伴分不清楚,这里直接看这张图就可以。
自定义侧边栏SlidingMeun代码如下:
/**
* Email 2185134304@qq.com
* Created by JackChen on 2018/2/10.
* Version 1.0
* Description:
*/
public class SlidingMenu extends HorizontalScrollView {
//菜单宽度
private int mMenuWidth;
//内容的View、菜单的View
private View mContentView , mMenuView ;
//处理快速滑动
GestureDetector mGestureDetector ;
//判断菜单是否打开
boolean mMenuIsOpen = false ;
//是否拦截
boolean mIsIntercept = false ;
public SlidingMenu(Context context) {
this(context, null);
}
public SlidingMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 初始化自定义属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SlidingMenu);
float rightMargin = array.getDimension(
R.styleable.SlidingMenu_menuRightMargin, ScreenUtils.dip2px(context, 50));
// 菜单页的宽度是 = 屏幕的宽度 - 右边的一小部分距离(自定义属性)
mMenuWidth = (int) (getScreenWidth(context) - rightMargin);
array.recycle();
mGestureDetector = new GestureDetector(context , mGestureListener) ;
}
private GestureDetector.OnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//快速往左边滑是负数,往右边滑动是正数
Log.e("TAG", "velocityX -> " + velocityX);
//由于我们只关心快速滑动,只要快速滑动就会回调onFling
//当打开的时候,需要快速向右边滑动,就让关闭
//当关闭的时候,需要快速向左边滑动,就让打开
if (mMenuIsOpen){
//关闭
if (velocityX < 0){
closeMenu();
return true ;
}
}else{
//打开
if (velocityX > 0){
openMenu();
return true ;
}
}
return super.onFling(e1, e2, velocityX, velocityY);
}
} ;
//1.运行之后发现宽度不对(乱套了),这个时候需要去指定宽度,使用下边方法
//这个方法是布局解析完毕后 即setContentView之后调用 也就是XML文件解析完毕后调用
@Override
protected void onFinishInflate() {
super.onFinishInflate();
/* ========== 指定宽高 START ========== */
//1.内容页的宽度 = 屏幕的宽度
//由于activity_main布局是 LinearLayout包裹了layout_home_menu和layout_home_content,
// 所以先获取LinearLayout,然后从LinearLayout中获取2个子View
//获取LinearLayout 这个为什么不是根布局 com.view.day12.SlidingMeun
ViewGroup container = (ViewGroup) getChildAt(0);
//这里获取LinearLayout容器中所有子View个数,判断只能放置2个子View,如果不是2个则抛异常
int childCount = container.getChildCount();
if (childCount != 2){
throw new RuntimeException("只能放置两个子View!") ;
}
mMenuView = container.getChildAt(0);//获取LinearLayout的第一个子View,即菜单页
ViewGroup.LayoutParams menuParams = mMenuView.getLayoutParams();//设置宽高只能通过 LayoutParams
menuParams.width = mMenuWidth ;
mMenuView.setLayoutParams(menuParams);//7.0以下手机必须采用下边的方式
//2.菜单页宽度 = 屏幕宽度 - 右边一小段距离(自定义属性)
mContentView = container.getChildAt(1) ; //获取LinearLayout的第二个子View,即内容页
ViewGroup.LayoutParams contentParams = mContentView.getLayoutParams();
contentParams.width=getScreenWidth(getContext()) ;
mContentView.setLayoutParams(contentParams);//7.0以下手机必须采用下边的方式
/* 此时侧滑默认是打开的 , 我们需要将侧滑关闭,需要调用smoothScrollTo()来关闭,并且需要在onLayout()方法调用*/
}
//4. 处理右边的缩放,左边呢的缩放和透明度,需要不断的获取当前滚动的位置
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
// 算一个梯度值
float scale = 1f * l / mMenuWidth;// scale 变化是 1 - 0
// 右边的缩放: 最小是 0.7f, 最大是 1f
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 leftAlpha = 0.5f + (1-scale)*0.5f;
ViewCompat.setAlpha(mMenuView,leftAlpha);
// 缩放 0.7f - 1.0f
float leftScale = 0.7f + (1-scale)*0.3f;
ViewCompat.setScaleX(mMenuView,leftScale);
ViewCompat.setScaleY(mMenuView, leftScale);
// 最后一个效果 退出这个按钮刚开始是在右边,安装我们目前的方式永远都是在左边
// 设置平移,先看一个抽屉效果
// ViewCompat.setTranslationX(mMenuView,l);
// 平移 l*0.7f
ViewCompat.setTranslationX(mMenuView, 0.25f*l);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
smoothScrollTo(mMenuWidth , 0);
}
//3.手指抬起时二选一,侧滑要么关闭、要么打开
@Override
public boolean onTouchEvent(MotionEvent ev) {
//如果有拦截,就不要执行自己的onTouchEvent了
if (mIsIntercept){
return true ;
}
//这里需要注意:如果快速滑动了,下边的代码就不要执行了
//这里我们需要把onTouchEvent交给mGestureDetector来处理
if (mGestureDetector.onTouchEvent(ev)){
return true ;
}
//获取手指滑动的速率,当其大于一定值就认为是快速滑动,GestureDetector(系统提供好的类)
//当菜单打开的时候,手指触摸右边内容部分需要关闭菜单,这个时候点击头像是没有反应的,不让其响应点击事件,
// 所以这里还需要事件拦截,想都不用想,肯定在onInterceptTouchEvent()方法处理
if (ev.getAction() == MotionEvent.ACTION_UP){
//这个时候只需要管手指抬起,根据当前滚动的距离来判断
int currentX = getScrollX() ;
//由我画的图三分析可知,如果getScrollX(),就需要关闭侧滑;否则打开
if (currentX > mMenuWidth/2){
closeMenu() ;
}else{
//打开
openMenu() ;
}
//确保super.onTouchEvent(ev)不会执行
return true ;
}
return super.onTouchEvent(ev);
}
/**
* 当菜单打开时,点击右上角头像,让菜单关闭;当菜单关闭时点击头像,就会进入个人中心页面
*
* 只要当这个方法 return true,就表示拦截:
* 所以需要判断:
* 1.菜单是打开的情况
* 2.点击了头像
* 这个时候就return true,去拦截,不让响应点击事件即可
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
mIsIntercept = false ;
if (mMenuIsOpen){
float currentX = ev.getX() ;
if (currentX > mMenuWidth){
//1.关闭菜单
closeMenu();
//2.子View不需要相应任何事件的点击和触摸 , 拦截子View的事件,直接return true即可
//这里需要注意:如果返回true,代表我会拦截子View的事件,但是我会相应自己的onTouchEvent事件
mIsIntercept = true ;
return true ;
}
}
return super.onInterceptTouchEvent(ev);
}
/**
* 打开菜单 滚动到 0 的位置
*/
private void openMenu() {
// smoothScrollTo 有动画
smoothScrollTo(0, 0);
mMenuIsOpen = true ;
}
/**
* 关闭菜单 滚动到 mMenuWidth 的位置
*/
private void closeMenu() {
smoothScrollTo(mMenuWidth, 0);
mMenuIsOpen = false ;
}
/**
* 获得屏幕高度
*
* @param context
* @return
*/
private int getScreenWidth(Context context) {
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.widthPixels;
}
/**
* Dip into pixels
*/
private int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
}
layout_home_content和layout_home_menu布局文件如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_centerInParent="true"
android:text="主页内容"
android:layout_height="wrap_content" />
</RelativeLayout>
<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:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="72dp"
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="@drawable/morentouxiang" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="22dp"
android:orientation="vertical">
<TextView
android:id="@+id/user_name_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:drawableRight="@drawable/user_write_paint"
android:text="请登录"
android:textColor="#c6b178"
android:textSize="18dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="42dp"
android:orientation="horizontal">
<TextView
android:id="@+id/user_attention_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:text="关注 0"
android:textColor="#c6b178"
android:textSize="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="40dp"
android:drawablePadding="10dp"
android:text="粉丝 0"
android:textColor="#c6b178"
android:textSize="12dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<ListView
android:id="@+id/menu_item_lv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@null"
android:dividerHeight="0dp"
android:layout_marginTop="60dp"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_margin="20dp"
android:text="退出"
android:textColor="#FFFFFF"
android:layout_height="wrap_content" />
</RelativeLayout>
具体代码已上传至github:
https://github.com/shuai999/View_day12_1.git