上两篇播客说了下View和ViewGroup的事件处理,这里实现下酷狗音乐和QQ6.0侧滑菜单的效果,先看下酷狗音乐侧滑菜单的效果;
侧滑菜单其实系统已经提供了DrawerLayout,这里没有用系统的DrawerLayout来实现的,而是自定义View继承自horizontalscrollview来实现的;
1自定义view继承自horizontalscrollview并计算菜单的宽度
public class KGSlidingMenu extends HorizontalScrollView {
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 array = context.obtainStyledAttributes(attrs, R.styleable.KGSlidingMenu);
//距离右边的距离
float rightMargin = array.getDimension(R.styleable.KGSlidingMenu_menuRightMargin, dip2px(50));
//计算菜单的宽度
mMenuWidth = (int) (getScreenWidth(context) - rightMargin);
array.recycle();
}
}
这里采用的是两个布局,左边是菜单的布局,右边是内容的布局;
2测量菜单和内容的宽高,并重新设置宽高
在onFinishInflate()方法里面进行宽高的测量和设置,
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//布局解析完毕会回调 也就是xml文件布局解析完毕 在onCreate方法中调用
//指定宽高
ViewGroup container = (ViewGroup) getChildAt(0);
int childCount = container.getChildCount();
if (childCount != 2) {
throw new RuntimeException("只能放置两个子view!");
}
//菜单页的宽度是屏幕的宽度 - 右边的一小部分距离(自定义属性)
menuView = container.getChildAt(0);
ViewGroup.LayoutParams menuParams = menuView.getLayoutParams();
menuParams.width = mMenuWidth;
menuView.setLayoutParams(menuParams);
//内容页的宽度是屏幕的宽度
contentView = container.getChildAt(1);
ViewGroup.LayoutParams contentParams = contentView.getLayoutParams();
contentParams.width = getScreenWidth(getContext());
contentView.setLayoutParams(contentParams);
}
onFinishInflate()方法是在布局解析完毕,也就是xml文件布局解析完毕会调用,在http://www.jianshu.com/p/39ca98752bfd,activtiy中setContentView布局加载的播客里面说了布局的加载,不管是setContentView还是LayoutInflater都会走到下面这个方法,
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
拿到XmlResourceParser这个解析器继续往下走,
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
try {
...
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
...
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
...
}
} catch (XmlPullParserException e) {
...
} catch (Exception e) {
...
} finally {
...
}
return result;
}
}
就会调用rInflate()和rInflateChildren(),rInflateChildren()方法后面还是调用的是rInflate(),
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
...
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
...
}
//如果finishInflate为true,也就是加载解析完布局就会调用onFinishInflate
if (finishInflate) {
parent.onFinishInflate();
}
}
所以在inflate方法中的rInflate();是不会调用onFinishInflate()方法的,只有 rInflateChildren();才会调用onFinishInflate()方法
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//这里finishInflate传递的是false不会调用onFinishInflate方法
rInflate(parser, root, inflaterContext, attrs, false);
}
// Inflate all children under temp against its context.
//这里才会调用onFinishInflate方法,
rInflateChildren(parser, temp, attrs, true);
这样初步的效果就实现了,但是一进入界面是打开的,所以就要让它每次进入界面时都移动到菜单的宽度,即关闭状态;
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//初始化进入的时候是关闭的,移动一段距离就可以了 在onResume之后调用
scrollTo(mMenuWidth, 0);
}
3处理DOWN,UP,MOVE等状态下的滑动
接下来就要在onTouchEvent方法中对DOWN,UP,MOVE这几种状态进行处理,要获取到手指滑动时X的的距离,如果X大于菜单宽度的一半在UP状态下就要自动关闭菜单,反之则打开;
@Override
public boolean onTouchEvent(MotionEvent ev) {
//如果有拦截就不执行自己的onTouchEvent
if (mIsIntercept) {
return true;
}
//快速滑动触发了下面的就不要执行了
if (mGestureDetector.onTouchEvent(ev)) {
return true;
}
if (ev.getAction() == MotionEvent.ACTION_UP) {
// 只需要处理手指抬起 根据当前滚动的距离来判断
int currentScrollX = getScrollX();
if (currentScrollX > mMenuWidth / 2) {
//关闭
closeMenu();
} else {
//打开
openMenu();
}
//确保super.onTouchEvent(ev)不执行
return true;
}
return super.onTouchEvent(ev);
}
4处理滑动过程中右边的缩放,左边的缩放和透明度
onScrollChanged()方法会在滑动过程中时时监听一个滑动的状态,
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
//计算一个梯度值 1f - 0f
float scale = 1f * l / mMenuWidth;
//右边的缩放最小 0.7f 最大是1f 默认是以中心点缩放
float rightScale = 0.7f + 0.3f * scale;
//设置缩放的中心点位置
ViewCompat.setPivotX(contentView, 0);
ViewCompat.setPivotY(contentView, contentView.getMeasuredHeight() / 2);
//设置右边的缩放
ViewCompat.setScaleX(contentView, rightScale);
ViewCompat.setScaleY(contentView, rightScale);
//设置菜单缩放和透明度
//透明度是由半透明到全部透明 0.7 - 1.0f
float leftAlpha = 0.5f + (1 - scale) * 0.5f;
ViewCompat.setAlpha(menuView, leftAlpha);
//缩放 0.7f - 1.0f
float leftScale = 0.7f + (1 - scale) * 0.3f;
ViewCompat.setScaleX(menuView, leftScale);
ViewCompat.setScaleY(menuView, leftScale);
//设置文字平移
ViewCompat.setTranslationX(menuView, l * 0.26f);
}
ViewCompat是android官方实现兼容的一个帮助类,可以设置一些动画,缩放等效果;
5处理手势快速滑动
在处理手势快速滑动方法,系统提供了一个GestureDetector类,它底层已经帮我们做了很好的处理,使用的时候直接new一个就可以了,
GestureDetector mGestureDetector = new GestureDetector(context, mGestureListener);
需要传一个上下文和OnGestureListener,在new一个OnGestureListener的时候发现需要实现很多我们不需要的方法,但是不实现就会报错,可以选择SimpleOnGestureListener,SimpleOnGestureListener继承自OnGestureListener,点进去一看什么都没有做,但是可以根据自己的需要去实现方法;
private GestureDetector.OnGestureListener mGestureListener = 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);
}
};
这里是左右滑动,所以只需要根据velocityX的值去判断;
6处理事件拦截
在菜单栏打开状态下,点击右边的内容区域要将菜单栏关闭,同时右边的内容区域不能有相应的点击效果,在菜单栏关闭状态下,点击右边内容区域才会有相应的点击效果,这样子就需要在onInterceptTouchEvent()方法中进行事件拦截的处理;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
mIsIntercept = false;
//当菜单打开的时候,手指触摸右边内容部分需要关闭菜单,还需要拦截事件,
if (mMenuIsOpen) {
float currentX = ev.getX();
if (currentX > mMenuWidth) {
//关闭菜单
closeMenu();
//子view不需要相应任何的事件 拦截子view的事件
mIsIntercept = true;
return true;
}
}
return super.onInterceptTouchEvent(ev);
}
这样就大致实现了上面的效果,不过右边内容区域用的是TextView,如果右边用的是ListView或者recyclerview或者ViewPager等,还要对事件分发及事件拦截等进行相应处理才可以达到想要的效果;
QQ6.0侧滑菜单效果就是在酷狗音乐侧滑菜单栏效果的基础进行了些修改,下面是QQ6.0侧滑菜单的效果;
和酷狗音乐侧滑菜单栏效果相比,左右滑动的时候没有缩放效果,但是在左右滑动的时候右边的内容区域会根据滑动的距离会有透明度的变化;实现透明度的变化这里有两种思路;
1、在布局文件中放一个大的背景,在滑动的的时候做一个回调,根据回调获取的值设置背景的透明度
2、在测量的时候先将布局类容移除调,创建一个RelativeLayout将内容布局添加到RelativeLayout,接着创建一个透明度的View,将View也添加到RelativeLayout中,最后将RelativeLayout添加的之前的内容布局的父布局中,
这里采用的是第二种思路实现的;
在onFinishInflate()方法中进行布局移除,创建,添加的一系列操作;
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//布局解析完毕会回调 也就是xml文件布局解析完毕 在onCreate方法中调用
//指定宽高
ViewGroup container = (ViewGroup) getChildAt(0);
int childCount = container.getChildCount();
if (childCount != 2) {
throw new RuntimeException("只能放置两个子view!");
}
//菜单页的宽度是屏幕的宽度 - 右边的一小部分距离(自定义属性)
menuView = container.getChildAt(0);
ViewGroup.LayoutParams menuParams = menuView.getLayoutParams();
menuParams.width = mMenuWidth;
menuView.setLayoutParams(menuParams);
//内容页的宽度是屏幕的宽度
contentView = container.getChildAt(1);
ViewGroup.LayoutParams contentParams = contentView.getLayoutParams();
//移除之前的布局
container.removeView(contentView);
//然后在外面套一层阴影
RelativeLayout contentContainer=new RelativeLayout(getContext());
contentContainer.addView(contentView);
//创建阴影
mShadowView=new View(getContext());
mShadowView.setBackgroundColor(Color.parseColor("#55000000"));
contentContainer.addView(mShadowView);
//最后放回原来的位置
contentParams.width = getScreenWidth(getContext());
contentContainer.setLayoutParams(contentParams);
container.addView(contentContainer);
//设置为透明
mShadowView.setAlpha(0.0f);
}
添加进去后,在滑动的时候进行监听不断改变内容区域的透明度;
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
// //计算一个梯度值 1f - 0f
float scale = 1f * l / mMenuWidth;
//控制阴影 0--1
float alphaScale=1-scale;
mShadowView.setAlpha(alphaScale);
//设置文字平移
ViewCompat.setTranslationX(menuView, l * 0.6f);
}
这样效果就实现了,如上面写的有不对的地方,欢迎交流;
仿酷狗音乐侧滑菜单源码:
http://download.csdn.net/detail/wangwo1991/9896972
仿QQ6.0侧滑菜单源码:
http://download.csdn.net/detail/wangwo1991/9899845