今天讲解的是侧滑菜单栏的实现,需要预先明白的知识点有:
1:ViewTreeObserver
2:GestureDetector
3:Scroller
4:ViewGroup
了解了上面4个知识点,再来分析侧滑的例子,这样就简单多了,俗话说“磨刀不误砍柴工”
1:首先讲解ViewTreeObserver类,这个类用于监听View树的变化,下面简单分析一下该类的结构,以及一些使用的方法。
Android ViewTreeObserver简介
一、结构
public final class ViewTreeObserver extends Object
java.lang.Object
android.view.ViewTreeObserver
二、概述
这是一个注册监听视图树的观察者(observer),在视图树中全局事件改变时得到通知。这个全局事件包括整个树的布局,从绘画过程开始,触摸模式的改变等。ViewTreeObserver不能够被应用程序实例化,因为它是由视图提供,参照getViewTreeObserver()以查看更多信息。
三、内部类
interface ViewTreeObserver.OnGlobalFocusChangeListener
当在一个视图树中的焦点状态发生改变时,所要调用的回调函数的接口类
interface ViewTreeObserver.OnGlobalLayoutListener
当在一个视图树中全局布局发生改变或者视图树中的某个视图的可视状态发生改变时,所要调用的回调函数的接口类
interface ViewTreeObserver.OnPreDrawListener
当一个视图树将要绘制时,所要调用的回调函数的接口类,该接口中返回true,否则视图树的绘制将会不完整
interface ViewTreeObserver.OnScrollChangedListener
当一个视图树中的一些组件发生滚动时,所要调用的回调函数的接口类
interface ViewTreeObserver.OnTouchModeChangeListener
当一个视图树的触摸模式发生改变时,所要调用的回调函数的接口类
四、公共方法
(1)public void addOnGlobalFocusChangeListener (ViewTreeObserver.OnGlobalFocusChangeListener listener)
注册一个回调函数,当在一个视图树中的焦点状态发生改变时调用这个回调函数。
参数 listener 将要被添加的回调函数;如果isAlive() 返回false,该方法将抛出异常 IllegalStateException 。
(2)public void addOnGlobalLayoutListener (ViewTreeObserver.OnGlobalLayoutListener listener)
注册一个回调函数,当在一个视图树中全局布局发生改变或者视图树中的某个视图的可视状态发生改变时调用这个回调函数。
参数 listener 将要被添加的回调函数; 如果isAlive() 返回false,该方法将抛出异常 IllegalStateException 。
(3)public void addOnPreDrawListener (ViewTreeObserver.OnPreDrawListener listener)
注册一个回调函数,当一个视图树将要绘制时调用这个回调函数。
参数listener 将要被添加的回调函数;如果isAlive() 返回false,该方法将抛出异常 IllegalStateException 。
(4)public void addOnScrollChangedListener (ViewTreeObserver.OnScrollChangedListener listener)
注册一个回调函数,当一个视图发生滚动时调用这个回调函数。
参数listener 将要被添加的回调函数;如果isAlive() 返回false,该方法将抛出异常 IllegalStateException 。
(5)public void addOnTouchModeChangeListener (ViewTreeObserver.OnTouchModeChangeListener listener)
注册一个回调函数,当一个触摸模式发生改变时调用这个回调函数。
参数listener将要被添加的回调函数; 如果isAlive() 返回false,该方法将抛出异常 IllegalStateException 。
(6)public final void dispatchOnGlobalLayout ()
当整个布局发生改变时通知相应的注册监听器。如果你强制对视图布局或者在一个没有附加到一个窗口的视图的层次结构或者在GONE状态下,它可以被手动的调用。
(7)public final boolean dispatchOnPreDraw ()
当一个视图树将要绘制时通知相应的注册监听器。如果这个监听器返回true,则这个绘制将被取消并重新计划。如果你强制对视图布局或者在一个没有附加到一个窗口的视图的层次结构或者在一个GONE状态下,它可以被手动的调用。返回值:当前绘制能够取消并重新计划则返回true,否则返回false。
(8)public boolean isAlive ()
指示当前的ViewTreeObserver是否可用(alive)。当observer不可用时,任何方法的调用(除了这个方法)都将抛出一个异常。如果一个应用程序保持和ViewTreeObserver一个历时较长的引用,它应该总是需要在调用别的方法之前去检测这个方法的返回值。返回值:这个对象可用则返回true,否则返回false 。
(9)public void removeGlobalOnLayoutListener (ViewTreeObserver.OnGlobalLayoutListener victim)
移除之前已经注册的全局布局回调函数。
参数victim 将要被移除的回调函数;如果isAlive() 返回false,该方法将抛出异常 IllegalStateException 。
(10)public void removeOnGlobalFocusChangeListener (ViewTreeObserver.OnGlobalFocusChangeListener victim)
移除之前已经注册的焦点改变回调函数。
参数victim 将要被移除的回调函数;如果isAlive() 返回false,该方法将抛出异常 IllegalStateException 。
(11)public void removeOnPreDrawListener (ViewTreeObserver.OnPreDrawListener victim)
移除之前已经注册的预绘制回调函数。
参数victim 将要被移除的回调函数;如果isAlive() 返回false,该方法将抛出异常 IllegalStateException 。
(12)public void removeOnScrollChangedListener (ViewTreeObserver.OnScrollChangedListener victim)
移除之前已经注册的滚动改变回调函数。
参数victim 将要被移除的回调函数;如果isAlive() 返回false,该方法将抛出异常 IllegalStateException 。
(13)public void removeOnTouchModeChangeListener (ViewTreeObserver.OnTouchModeChangeListener victim)
移除之前已经注册的触摸模式改变回调函数
参数victim 将要被移除的回调函数;如果isAlive() 返回false,该方法将抛出异常 IllegalStateException 。
2:了解完毕了ViewTreeObserver的类结构和方法后,下面简单的讲解GestureDetector类。
一般的View只能响应点击(Click)和长按(LongPress)事件。这是因为View里只暴露了这些listener给我们使用。而实质上,View是在onTouchEvent(MotionEvent event)里对用户的动作做了一定的分析,从而通知我们是发生了点击还是长按等事件。
View里提供的回调在我描述的场景里,并不能满足要求。因此,GestureDetector出场了。我需要对其啃透才能写出自己的ActionDetector。
GestureDetector类可以帮助我们分析用户的动作,和View的onTouchEvent的处理方式差不多,但分析的动作类型更加细致,以下是它的回调接口:
java代码:
public interface OnGestureListener {
// Touch down时触发, e为down时的MotionEvent
boolean onDown(MotionEvent e);
// 在Touch down之后一定时间(115ms)触发,e为down时的MotionEvent
void onShowPress(MotionEvent e);
// Touch up时触发,e为up时的MotionEvent
boolean onSingleTapUp(MotionEvent e);
// 滑动时触发,e1为down时的MotionEvent,e2为move时的MotionEvent
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
// 在Touch down之后一定时间(500ms)触发,e为down时的MotionEvent
void onLongPress(MotionEvent e);
// 滑动一段距离,up时触发,e1为down时的MotionEvent,e2为up时的MotionEvent
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}
public interface OnDoubleTapListener {
// 完成一次单击,并确定没有二击事件后触发(300ms),e为down时的MotionEvent
boolean onSingleTapConfirmed(MotionEvent e);
// 第二次单击down时触发,e为第一次down时的MotionEvent
boolean onDoubleTap(MotionEvent e);
// 第二次单击down,move和up时都触发,e为不同时机下的MotionEvent
boolean onDoubleTapEvent(MotionEvent e);
}
有了这么多的回调消息,我们就能更加方便的对用户的动作进行响应,那么,这个类如何使用呢?以下是使用该类的一个范例:
private GestureDetector mGestureDetector;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mGestureDetector = new GestureDetector(this, new MyGestureListener());
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
class MyGestureListener extends GestureDetector.SimpleOnGestureListener{
@Override
public boolean onSingleTapUp(MotionEvent ev) {
Log.d("onSingleTapUp",ev.toString());
return true;
}
@Override
public void onShowPress(MotionEvent ev) {
Log.d("onShowPress",ev.toString());
}
@Override
public void onLongPress(MotionEvent ev) {
Log.d("onLongPress",ev.toString());
}
}
基本的内容就是创建一个GestureDetector的对象,传入listener对象,在自己接收到的onTouchEvent中将event传给GestureDetector进行分析,listener会回调给我们相应的动作。其中GestureDetector.SimpleOnGestureListener(Framework帮我们简化了)是实现了上面提到的OnGestureListener和OnDoubleTapListener两个接口的类,我们只需要继承它并重写其中我们关心的回调即可。
最后,再提一下双击和三击的识别过程:在第一次单击down时,给Hanlder发送了一个延时300ms的消息,如果300ms里,发生了第二次单击的down事件,那么,就认为是双击事件了,并移除之前发送的延时消息。
3:接下来介绍的Scroller类。
可以参考下面的连接:http://www.cnblogs.com/wanqieddy/archive/2012/05/05/2484534.html
4:ViewGroup的讲解。
可以参考下面的连接:http://my.oschina.net/u/565871/blog/132350
很抱歉,由于晚上时间有限,这篇博文没有讲解菜单栏侧滑的例子,下面讲解具体的例子。
(1)首先给出ViewGroup的代码。
package com.jj.sliding_6;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.View.MeasureSpec;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.AbsoluteLayout;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.Scroller;
/***
* 自定义view
*
* @author zhangjia
*
*/
public class MyViewGroup extends ViewGroup {
private Scroller scroller;//滑动类,其中只是记录一些滑动的位置信息
private int distance;//滑动距离
private View menu_view;//左侧的menu菜单
private View content_view;//主页View
private int duration = 500;//动画时间
private Context context;//上下文动画
private CloseAnimation closeAnimation;//左侧菜单关闭的动画
public static boolean isMenuOpned = false;//菜单是否打开
//代码创建View必须实现此构造函数
public MyViewGroup(Context context) {
super(context, null);
}
//设置关闭左侧菜单的动画
public void setCloseAnimation(CloseAnimation closeAnimation) {
this.closeAnimation = closeAnimation;
}
//在xml文件中使用此view必须实现的构造函数
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
scroller = new Scroller(context);//创建scroller类
}
//设置滑动的距离
public void setDistance(int distance) {
this.distance = distance;
}
//对ViewGroup中的孩子进行布局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
menu_view = getChildAt(0);//获取滑动菜单的view
content_view = getChildAt(1);//获得主页view
content_view.measure(0, 0);//0相当于Match_Parent
//主页填充屏幕
content_view.layout(0, 0, getWidth(), getHeight());
}
}
//重写view中的computeScroll方法,父容器重画孩子的时候,会调用孩子的computeScroll方法
@Override
public void computeScroll() {
//如果像scroller中设置了数据,computeScrollOffset方法会返回true,否则返回false
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());//只会滚动一点的位置
postInvalidate();//不断的刷新就会不断的滚动
if (closeAnimation != null)
closeAnimation.closeMenuAnimation();
}
}
void showMenu() {
isMenuOpned = true;
scroller.startScroll(getScrollX(), 0, -distance, 0, duration);
invalidate();// 刷新
}
// 关闭菜单(执行自定义动画)
void closeMenu() {
isMenuOpned = false;
scroller.startScroll(getScrollX(), 0, distance, 0, duration);
invalidate();// 刷新
}
// 关闭菜单(执行自定义动画)
void closeMenu_1() {
isMenuOpned = false;
//此处getWidth()得到的屏幕的宽度
scroller.startScroll(getScrollX(), 0, distance - getWidth(), 0,duration);
invalidate();// 刷新
}
// 关闭菜单(执行自定义动画)
void closeMenu_2() {
isMenuOpned = false;
scroller.startScroll(getScrollX(), 0, getWidth(), 0, duration);
invalidate();// 刷新
}
/***
* Menu startScroll(startX, startY, dx, dy)
*
* dx=e1的减去e2的x,所以右移为负,左移动为正 dx为移动的距离,如果为正,则标识向左移动|dx|,如果为负,则标识向右移动|dx|
*/
public void slidingMenu() {
// 没有超过半屏
if (getScrollX() > -getWidth() / 2) {
scroller.startScroll(getScrollX(), 0, -getScrollX(), 0, duration);
isMenuOpned = false;
}else if (getScrollX() <= -getWidth() / 2) {// 超过半屏
scroller.startScroll(getScrollX(), 0, -(distance + getScrollX()),0, duration);
isMenuOpned = true;
}
invalidate();// 刷新
}
}
abstract class CloseAnimation {
// 点击list item 关闭menu动画
public abstract void closeMenuAnimation();
}
(2)接下来给出Activity代码。
package com.jj.sliding_6;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnPreDrawListener;
import android.view.animation.Animation;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.Toast;
public class MainActivity extends Activity implements OnClickListener,
GestureDetector.OnGestureListener {
private MyViewGroup myViewGroup;//自定义的ViewGroup
private LayoutInflater layoutInflater;//xml文件的充气泵
private View menu_view;//左侧滑动后出现的菜单栏
private View content_view;//主界面
private ImageView iv_button;//主页左上角的按钮
private int iv_button_width;//按钮宽度
private int window_windth;//宽度
private ListView lv_menu;//listview引用
private boolean hasMeasured = false;//是否已经Measure过的标志为
private int distance;//滑动距离
private GestureDetector gestureDetector;//手势操作类
private boolean isScrolling = false;//是否正在滚动的标志位
private boolean direction = false;// true,左,false,右
private View click_view;//点击的view
private String title[] = { "待发送队列", "同步分享设置", "编辑我的资料", "找朋友"};
private ViewTreeObserver viewTreeObserver;//视图树的监听者
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
InitView();
}
private void InitView() {
//初始化手势操作类
gestureDetector = new GestureDetector(this);
//初始化自定义ViewGroup
myViewGroup = (MyViewGroup) findViewById(R.id.vg_main);
//初始化view填充者
layoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//初始化左侧菜单栏
menu_view = layoutInflater.inflate(R.layout.menu, null);
//初始化主页view
content_view = layoutInflater.inflate(R.layout.content, null);
//将菜单栏添加进入ViewGroup,索引为0
myViewGroup.addView(menu_view);
//将主页添加进ViewGroup,索引为1
myViewGroup.addView(content_view);
//获得主页中button按钮的引用
iv_button = (ImageView) content_view.findViewById(R.id.iv_button);
//获得listview的引用
lv_menu = (ListView) menu_view.findViewById(R.id.lv_menu);
//为listview设置填充数据
lv_menu.setAdapter(new ArrayAdapter<String>(this, R.layout.item,R.id.tv_item, title));
//此方法有待验证
setListViewHeightBaseOnChildren(lv_menu);
//设置主界面的监听
iv_button.setOnClickListener(this);
//计算滑动的距离
getMAX_WIDTH();
//为主页界面增加监听
content_view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//如果此时菜单栏为显示状态,关闭菜单栏,ScrollX为正数,说明向左滑动,为负数,说明向右滑动
if (myViewGroup.getScrollX() <= -distance) {
myViewGroup.closeMenu();
}
}
});
//此种效果为:点击菜单栏中的条目后,主界面会先向右运动到屏幕边缘在关闭菜单栏
lv_menu.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
myViewGroup.setCloseAnimation(new CloseAnimation() {
@Override
public void closeMenuAnimation() {
if (myViewGroup.getScrollX() == -window_windth)
myViewGroup.closeMenu_2();
}
});
myViewGroup.closeMenu_1();
Toast.makeText(MainActivity.this, title[position], 1).show();
}
});
}
//对返回键进行监听
@Override
public void onBackPressed() {
if(MyViewGroup.isMenuOpned){
myViewGroup.closeMenu();
return;
}
super.onBackPressed();
}
/***
* 计算滑动的距离
*
*/
public void getMAX_WIDTH() {
//得到左侧菜单栏的监听树
viewTreeObserver = menu_view.getViewTreeObserver();
//用于在屏幕上画 View 之前,要做什么额外的工作,增加此监听
viewTreeObserver.addOnPreDrawListener(new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
//全局标志位,下面的代码只执行一次
if (!hasMeasured) {
//得到屏幕的宽度
window_windth = getWindowManager().getDefaultDisplay().getWidth();
//得到按钮的宽度
iv_button_width = content_view.findViewById(R.id.iv_button).getWidth();
//计算滑动的距离,此距离滑动后,在屏幕的右上侧正好显示此按钮,不多不少的距离
distance = window_windth - iv_button_width;
myViewGroup.setDistance(distance);
// 在这里我们要设置lv_menu的宽度,(因为viewpager和listview
// 会发送冲突,效果和scrollview一样,怀疑他们私底下有一腿.)
ViewGroup.LayoutParams layoutParams = (ViewGroup.LayoutParams) lv_menu.getLayoutParams();
layoutParams.width = distance;
lv_menu.setLayoutParams(layoutParams);
hasMeasured = true;
}
return true;//如果此处不返回 true , 则整个界面不能完整显示。
}
});
}
//点击主界面“按钮”时,打开或者关闭菜单,此处移动后,点击移动后的button,仍然管用,说明View真正的移动了,view的属性改变了
@Override
public void onClick(View v) {
if (v == iv_button) {
if (myViewGroup.isMenuOpned)
myViewGroup.closeMenu();
else
myViewGroup.showMenu();
}
}
/***
* 动态设置listview的高度 注:在listview和scrollview冲突的时候我们可以用下面这个方法动态设置listview的高度,
* 但是在这里不行,原因不明确,但是还有一个办法就是把listview设置和屏幕一样的高度,
* 这样不管是viewgroup和scrillview肯定都管用.
* 有待研究
* @param listView
*/
public void setListViewHeightBaseOnChildren(ListView listView) {
ViewGroup.LayoutParams layoutParams = listView.getLayoutParams();
layoutParams.height = getWindowManager().getDefaultDisplay().getHeight();
listView.setLayoutParams(layoutParams);
}
//重写dispatchTouchEvent方法,将事件传递给GestureDetector类中的onTouchEvent()方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//当手指抬起来的时候,根据滑动的距离判断是该展开菜单栏还是关闭菜单栏
if (ev.getAction() == MotionEvent.ACTION_UP && isScrolling) {
myViewGroup.slidingMenu();
}
gestureDetector.onTouchEvent(ev);
return super.dispatchTouchEvent(ev);
}
//重写GestureDetector类中的onDown方法
@Override
public boolean onDown(MotionEvent e) {
// 在这里要进行对menu_view 布局.
// 对menu_view进行布局.
menu_view.measure(0, 0);//设置菜单栏的大小
//设置菜单栏的位置
menu_view.layout(-distance, 0, 0, getWindowManager().getDefaultDisplay().getHeight());
lv_menu.setSelection(0);
return true;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,float distanceY) {
//如果菜单没有打开向左不允许滑动
if (!MyViewGroup.isMenuOpned && distanceX > 0) {
//如果菜单打开向右滑动,右滑distanceX为负数,左滑distanceX为正数
} else if (myViewGroup.getScrollX() == -distance
&& MyViewGroup.isMenuOpned && distanceX < 0) {
isScrolling = false;
}else if (myViewGroup.getScrollX() >= -distance
&& myViewGroup.getScrollX() <= 0) {
isScrolling = true;
myViewGroup.scrollBy((int) distanceX, 0);
}
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
return false;
}
}
下载链接:
http://download.csdn.net/detail/ly985557461/7387415