Android 侧滑菜单栏

今天讲解的是侧滑菜单栏的实现,需要预先明白的知识点有:

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值