自定义View - 仿酷狗音乐主页面侧滑效果
作者:
Delusion
链接:https://juejin.cn/post/7085922621761519623
概述
最近在用酷狗音乐时发现酷狗音乐的主页侧滑效果不错,忍不住手痒痒,就想实现一下看看
效果分析
我们可以看出分为两个部分 (菜单页面、主题页面)所以这里采用自定义 ViewGroup
向左滑动时会有一个放大和缩小的动画,在这里基本可以确定需要处理触摸事件,及触摸时的动画 快速滑动时的打开关闭处理( GestureDetector
收拾处理类)
实现思路
这里我们选择继承HorizontalScrollView
水平滚动布局 来实现,然后写好布局文件这个和HorizontalScrollView
的用法一样,初时进来滚动到只有主页面的地方,在onTouch
中根据滑动的距离来控制布局的缩放动画
实现
public KGSlidingMenu(Context context) {
this(context, null);
}
public KGSlidingMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public KGSlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
运行起来之后发现布局不对,完全乱了,这里我们在代码里设置菜单页面、主题页面的宽度
菜单页面的宽度 这里我们定义一个属性 menu_width 主题页面的宽度 = 屏幕的宽度
private View mMenuView;
private View mContainerView;
private int mMenuWidth;//需要自定义属性获取
public KGSlidingMenu(Context context) {
this(context, null);
}
public KGSlidingMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public KGSlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.KGSlidingMenu);
mMenuWidth = Math.round(ta.getDimension(R.styleable.KGSlidingMenu_menu_width, 325));
ta.recycle();
}
@Override
protected void onFinishInflate() {//在onCreate中执行
super.onFinishInflate();
//这个方法代表整个布局解析完毕
//指定宽高 :内容页的宽度为屏幕的宽度、菜单页的宽度由使用者自己在xml中定义 app:menu_width=""
//获取菜单和容器View
//获取的是LinearLayout
ViewGroup mRootView = (ViewGroup) getChildAt(0);
if (mRootView == null) {
throw new NullPointerException("child can not be null !!!");
}
if (mRootView.getChildCount() != 2) {
throw new RuntimeException("Only two can be placed View !!!");
}
//获取的是菜单和容器的跟布局
mMenuView = mRootView.getChildAt(0);
mContainerView = mRootView.getChildAt(1);
//给菜单布局设置指定宽度 给容器布局设置屏幕的宽高
//只能通过layoutParams设置宽高
mMenuView.getLayoutParams().width = mMenuWidth;
mContainerView.getLayoutParams().width = getScreenWidth(mContext);
}
//获取屏幕宽度
public int getScreenWidth(Context mContext) {
WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(displayMetrics);
return displayMetrics.widthPixels;
}
目前的效果就是可以滑动,并且菜单和主页面内容的布局宽度正常,现在存在的问题:
在刚进来时,菜单是打开的,正常情况应该是关闭的 在手指抬起的时候,应该根据当前位置去判断菜单时打开,还是关闭 在滑动时 没有相应的动画
第一点 在刚进来时,菜单是打开的,这一点我们可以在布局摆放完成时,是我们KGSlidingMenu
滚动到相应的位置 具体实现如下
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {//在onResume中执行
super.onLayout(changed, l, t, r, b);
//初始化进来 mMenuView 模式是不显示的
//用来摆放子View,等所有子View 摆放完毕才能滚动
scrollTo(mMenuWidth, 0);
}
第二点 在手指抬起的时候,应该根据当前位置去判断菜单时打开,还是关闭。判断菜单打开关闭的条件是我们滑动的距离和菜单的宽做对比
1).在关闭时,向右滑动,如果滑动的距离小于菜单宽度的一半,这是当我们抬起手指时,应当关闭菜单,大于菜单宽度的一半时,应当打开菜单. 2).在打开时,向左滑动,如果滑动的距离小于菜单宽度的一半,这是当我们抬起手指时,应当打开菜单,大于菜单宽度的一半时,应当关闭菜单.
因此我们需要在onTouchEvent()
中处理相应的触摸滑动事件
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mGestureDetector != null) {
if (mGestureDetector.onTouchEvent(event)) return true; //快速滑动执行了,就不要执行onTouch中手指抬起事件
}
if (onInterceptTouchEvent(event)) return true;
//手指抬起
if (MotionEvent.ACTION_UP == event.getAction()) {
int mCurrentScrollX = getScrollX();
float mCurrentScrollY = getScrollY(); //避免竖向触摸时触发事件
if (Math.abs(mCurrentScrollX) > Math.abs(mCurrentScrollY) * 2 / 3) {
if (mCurrentScrollX < mMenuWidth / 2) {//未滚动到mMenuView宽度的一半
//打开菜单
openMenu();
} else {
//关闭菜单
closeMenu();
}
}
return false;//确保 super.onTouchEvent(event) 不会执行
}
return super.onTouchEvent(event);
}
private void closeMenu() {
smoothScrollTo(mMenuWidth, 0)
}
private void openMenu() {
smoothScrollTo(0, 0);
}
第三点 在滑动时 没有相应的动画 通过效果我们可以看到
1). 右边的动画效果为缩放效果 2). 左边的动画效果有渐变、缩放和位移
这些动画执行的进度和效果都是根据滑动的距离来决定时,因此我们需要获得滑动的距离
//滚动回调 处理右边View的缩放 左边View的缩放和透明度
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
//l:left 变化mMenuWidth -> 0
//算一个梯度值
float scale = 1f * l / mMenuWidth; //从1->0
//计算右边的缩放值
float rightScaleValue = 0.85f + 0.15f * scale;
//设置右边的缩放 默认以view的中心点缩放
ViewCompat.setPivotX(mContainerView, 0);
ViewCompat.setPivotY(mContainerView, mContainerView.getMeasuredHeight() / 2);
ViewCompat.setScaleX(mContainerView, rightScaleValue);
ViewCompat.setScaleY(mContainerView, rightScaleValue);
//设置右边的菜单的透明度 由半透明到全透明 0。85-1
//缩放 0.85-1
//透明度
float leftAlphaValue = (1 - scale) * 0.5f + 0.5f;
ViewCompat.setAlpha(mMenuView, leftAlphaValue);
float leftScaleValue = (1 - scale) * 0.15f + 0.85f;
ViewCompat.setScaleX(mMenuView, leftScaleValue);
ViewCompat.setScaleY(mMenuView, leftScaleValue);
//刚开始退出是在右边而不是左边
//设置平移
// ViewCompat.setTranslationX(mMenuView,l);抽屉效果
ViewCompat.setTranslationX(mMenuView, 0.2f * l);
}
到此我们可以看到效果基本成型了,下面处理一下细节方面的优化
处理一下在手指快速滑动的是效果,如果我们的触摸事件响应了快速滑动的话,就不要执行onTouchEvent
中的相关操作,否则会出现效果不对的冲突事件,因此我们需要在onTouchEvent
中添加一下这段代码
if (mGestureDetector != null) {
if (mGestureDetector.onTouchEvent(event)) return true; //快速滑动执行了,就不要执行onTouch中手指抬起事件
}
复制代码
mGestureDetector = new GestureDetector(mContext, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {//快速滑动
//只要快速滑动就会回调
//打开的时候往右快速滑动,就去关闭 关闭的时候往左边快速滑动就去打开
//快速往右边滑动是一个正数 快速往左边滑动是一个负数
if (Math.abs(velocityX) < Math.abs(velocityY) * 2 / 3) return false;
if (isMenuOpen) {
//打开的时候往右快速滑动,就去关闭
if (velocityX < 0) {
closeMenu();
return true;
}
} else {
//关闭的时候往左边快速滑动就去打开
if (velocityX > 0) {
openMenu();
return true;
}
}
return false;
}
});
注意查看的话可以发现当菜单打开时,我们们点击菜单的右边主题部分,不会响应主题页面的相关事件,只会关闭菜单,只有在菜单关闭时,才会触发右边主题部分的相关事件,因此我们这地方需要在拦截事件onInterceptTouchEvent
中判断菜单是否关闭,进行事件的拦截,因此我们需要在上面打开关闭菜单的方法里添加一个标识符isMenuOpen
来区分菜单是否打开,这里事件拦截之后我们直接关闭了菜单,所以不需要执行onTouchEvent
中手指抬起的相关操作。
if (onInterceptTouchEvent(event)) return true;
复制代码
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (isMenuOpen && event.getX() > mMenuView.getWidth() && event.getAction() == MotionEvent.ACTION_UP) {
closeMenu();
return true;
}
return false;
}
至此相关效果基本就完成了,我们来看一下吧
完工!
---END---