参考鸿洋博客的一篇文章:
http://blog.csdn.net/lmj623565791/article/details/39257409
最终效果图
一、菜单布局
采用列表视图ListView
left_menu.xml
<ListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/list" android:layout_width="match_parent" android:layout_height="wrap_content" android:entries="@array/color" android:paddingTop="5dp" android:layout_marginTop="80dp"> </ListView>
其中android:entries用于通过数组资源为ListView指定列表项(也可以在代码中通过Adapter来为ListView指定要显示的列表项)
valuesarrays.xml
<resources> <string-array name="color"> <item>红</item> <item>橙</item> <item>黄</item> <item>绿</item> </string-array> </resources>
二、主布局
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:my="http://schemas.android.com/apk/res/com.example.slidemenutest" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <!-- 自定义View,等下会定义MyView类来实现 --> <com.example.slidemenutest.MyView android:id="@+id/menu" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/menu_background" my:rightPadding="150dp" > <!-- 自定义View里面只允许一个控件,所以要嵌套 --> <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="horizontal" > <!-- 导入刚才的菜单布局 --> <include layout="@layout/left_menu" /> <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@drawable/background" android:orientation="vertical" > <!-- 除了手势滑动外,还可以点击按钮弹出菜单 --> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#80000070" android:onClick="toogle" android:text="菜单" android:textColor="@android:color/white" /> </LinearLayout> </LinearLayout> </com.example.slidemenutest.MyView> </RelativeLayout>
自定义View布局里我们使用了一个自定义属性my:rightPadding=”150dp”,代表菜单弹出后跟屏幕右边界的距离,即除了菜单界面剩下的宽度。my是自己定义的命名空间标识 xmlns:my=”http://schemas.android.com/apk/res/com.example.slidemenutest” ,最后一个分隔符后面的是应用程序包名
三、自定义属性
valuesattr.xml
<resources> <attr name="rightPadding" format="dimension" /> <declare-styleable name="MyView"> <attr name="rightPadding"/> </declare-styleable> </resources>
四、自定义View –MyView类实现
MyView继承HorizontalScrollView
public class MyView extends HorizontalScrollView { private LinearLayout layout; private ViewGroup mMenu; // 菜单布局 private ViewGroup mContent; // 内容布局 private int mScreenWidth; // 屏幕宽度 private int mMenuWidth; // 菜单宽度 private int mRightPadding; // 菜单右边距 private boolean once; // 子view宽高初始化标识 private boolean isOpen; // 菜单打开标识 // 代码中使用 new MyView()时会调用此方法 public MyView(Context context) { this(context, null); } public MyView(Context context, AttributeSet attrs) { this(context, attrs, 0); // 调用我们需要的第三个构造方法 } /** * 当使用了自定义属性时,会调用该构造方法 */ public MyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 通过WindowManager获取屏幕相关信息 WindowManager wm = (WindowManager) context .getSystemService(Context.WINDOW_SERVICE); DisplayMetrics outMetrics = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(outMetrics); mScreenWidth = outMetrics.widthPixels; // 获取屏幕宽度 Log.v("TAG","density:"+outMetrics.density); Log.v("TAG","densityDpi:"+outMetrics.densityDpi); // 通过TypedArray获取自定义的属性 TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, 0); // 通过遍历自定义属性找到我们需要的rightPadding属性 for(int i = 0 ;i<a.getIndexCount();i++){ // attr为属性名 int attr = a.getIndex(i); switch (attr) { case R.styleable.MyView_rightPadding: // 如果没有设置右边距,则定义一个默认值,用TypedValue.applyDimension函数把50dp值转为像素值 // 不同设备的density不同 ,dp和px的转换也不同 。 如果density为1.5,则1dp = 1.5px int defValue = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, context.getResources().getDisplayMetrics()); // 获取属性值转为像素返回。如果属性没有设置值,则返回设置的默认值 mRightPadding = a.getDimensionPixelSize(attr, defValue); break; default: break; } } a.recycle(); // 使用完后记得要释放掉 } /** * 系统显示布局前先要为每个子view设置宽高,然后再设置它们的摆放位置 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // onMeasure方法会被系统多次调用,这里设置只进行一次初始化 if (!once) { layout = (LinearLayout) getChildAt(0); // 自定义View里的第一个view,还记得我们用LinearLayout嵌套其它子view吗? mMenu = (ViewGroup) layout.getChildAt(0); // LinearLayout里的第一个view是菜单 mContent = (ViewGroup) layout.getChildAt(1);// LinearLayout里的第二个view是内容 // 设置菜单宽度 = 屏幕宽度-我们设置的右边距 (剩下的宽度还是要显示一点内容的~) mMenuWidth = mMenu.getLayoutParams().width = mScreenWidth - mRightPadding; // 内容宽度当然是整个屏幕宽啦 mContent.getLayoutParams().width = mScreenWidth; Log.v("TAG","mScreenWidth:"+mScreenWidth); Log.v("TAG","mRightPadding:"+mRightPadding); Log.v("TAG","mMenuWidth:"+mMenuWidth); once = true; // 第二次系统调用onMeasure方法后就不会再重复初始化了 } } /** * 设置了子view的宽高后,自然就是它们自己选择在屏幕中的摆放位置啦 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); // onLayout也会多次调用 if(changed){ this.scrollTo(mMenuWidth, 0); // 隐藏菜单。 虽然只有简单的一句代码,但如果不知道原理的话是很难理解的 /* * 滚动视图有一个固定的坐标系,这个坐标系的原点在一开始它显示到屏幕中的左上角的位置。原点往左是视图的负坐标,原点往右是视图的正坐标 * 在这里一开始先显示的是菜单视图,所以菜单的左上角为这个滚动视图的原点,菜单和内容的边界 x坐标值=菜单的宽度 * scrollTo(x,0)是把视图坐标系的(x,0)位置移动到屏幕左边界(见示意图) */ isOpen = false; // 菜单关闭状态 } }
假设屏幕宽度400,菜单宽度300,右边距100
没有移动视图之前,即没有调用this.scrollToX(mMenuWidth,0); 之前,视图显示如下第一个图,因为是水平顺序排列,所以先显示菜单。此时就决定了滚动视图的坐标:菜单左上角为视图原点,且这视图坐标不会再变。
调用了this.scrollToX(mMenuWidth,0); 之后,视图坐标(300,0)移动到屏幕坐标系(0,0)(即左上角),这样菜单就隐藏了。
菜单拖动过程中,左边界所在滚动视图位置 x 坐标值不断变化,从300变化到0
/* * 通过判断手势实现 滑动多少距离才执行 打开/隐藏 菜单 */ @Override public boolean onTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_UP: // 当手指拖动后松开判断此时屏幕左边界位置所在的x坐标值 来 决定显示还是隐藏 int scrollx = getScrollX(); // getScrollX()就是此时屏幕左边界所在的视图的x坐标值,即getScrollX() == x // 左边界位置的x值大于菜单宽度的一半,即未显示的菜单宽度大于显示的菜单宽度,此时应该隐藏 if(scrollx >= mMenuWidth/2){ this.scrollTo(mMenuWidth, 0); isOpen=false; return true; } // 左边界位置的x值小于菜单宽度的一半,即显示的菜单宽度大于未显示的菜单宽度,此时应该显示 else{ // 把视图原点(即菜单左上角)移动至屏幕原点(左上角) this.scrollTo(0, 0); isOpen = true; return true; } default: break; } return super.onTouchEvent(ev); }
// 打开菜单 private void openMenu(){ if(isOpen) return; // 如果菜单已经打开则返回 this.smoothScrollTo(0, 0); isOpen=true; } // 关闭菜单 private void closeMenu(){ if(!isOpen) return; // 如果菜单已经关闭则返回 this.smoothScrollTo(mMenuWidth, 0); isOpen = false; } // 点击按钮后会调用切换方法 public void toogle(){ if(isOpen) closeMenu(); else openMenu(); }
五、MainActivity类的实现
public class MainActivity extends Activity { private MyView myview; // 自定义view类 private ListView listView; // 菜单 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); myview = (MyView)findViewById(R.id.menu); listView = (ListView) findViewById(R.id.list); // 监听列表视图的点击事件 listView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { toogle(view); // 点击菜单项后切换到内容视图 String result = parent.getItemAtPosition(position).toString(); // 在内容视图显示消息提示框,提示框里面是被点击菜单项的文本 Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show(); } }); } /* * 布局中为Button注册了toogle方法,注意是public */ public void toogle(View view) { myview.toogle(); // 调用MyView类中的toogle方法 } }
普通侧滑菜单演示:
可以看到当菜单拖出不到菜单宽度的一半时 ,手指松开菜单会隐藏回去,只有超过了那个距离才能打开菜单。关闭菜单也是同理。点击按钮也会弹出菜单,点击菜单项会切换回内容视图 并显示菜单项的文本。
六、被覆盖式菜单
当拖出菜单的时候,菜单显示效果就像 菜单原来是铺在内容视图的下层,移开内容就可以看见被覆盖的菜单,移动中菜单是静止的。和上面菜单跟内容相连滑动的效果不同
实现这个效果只需要简单的一行代码,包含的信息量也很大。需要用到开源包com.nineoldandroids.view.ViewHelper。这个包主要是为了兼容3.0以下使用Animation。jar包粘贴到libs目录下即可使用
在重写的父类onScrollChanged方法中实现
/** * 抽屉式滑动菜单 * 当视图滚动时,会实时调用此方法。l实际上是屏幕左边界处的 滚动视图x坐标值 * 原理:在滑动过程中,保持菜单视图在屏幕左边界。利用开源框架nineoldandroids中的ViewHelper.setTranslationX方法, * 把菜单视图移动到x坐标为l处,即左边界。菜单视图的移动不影响屏幕左边界处的滚动视图x坐标值 */ @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); // 让菜单实时贴边.菜单打开过程中 l的变化范围是 mMenuWidth ~ 0 ViewHelper.setTranslationX(mMenu, l ); }
效果:
七、缩放效果菜单
在菜单拖出的过程中,菜单从缩放后的大小逐渐恢复成正常大小,从半透明到不透明,同时 内容视图从正常大小缩小到一定比例
同样是在onScrollChanged中实现
@Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); // 让菜单实时贴边 ViewHelper.setTranslationX(mMenu, l * 0.8f ); // l*0.8是让菜单项文本跟屏幕左边界尽量贴边 /* * 利用实时变化的比率来达到实时缩放效果 */ // 在拉出过程中,l从 255 ~ 0 float rate = l * 1.0f / mMenuWidth; // rate 从 1.0 ~ 0.0 float menuScale = 1.0f - 0.3f * rate; // 菜单缩放效果从 0.7 ~ 1.0 float menuAlpha = menuScale; // 菜单透明度从 0.7 ~ 1.0 float contentScale = 1.0f - 0.3f * ( 1 - rate ); // 内容缩放效果从 1.0 ~ 0.7 // 菜单缩放 ViewHelper.setScaleX(mMenu, menuScale); ViewHelper.setScaleY(mMenu, menuScale); ViewHelper.setAlpha(mMenu, menuAlpha); // 内容缩放 ViewHelper.setScaleX(mContent, contentScale); ViewHelper.setScaleY(mContent, contentScale); }
效果(即文章最开始的图):
八、总结
继承HorizontalScrollView实现自定义View,并设置自定义属性,然后在主布局中引入。之后利用scrollToX移动视图,把菜单隐藏。接着监听手势判断执行菜单的关闭和打开。通过开源包中的ViewHelper.setTranslationX实现菜单实时贴紧屏幕左边界,利用边界值的变化设置缩放比例,实现菜单和内容的缩放。
HorizontalScrollView实现滑动菜单比较简单,且整个项目我已经按照我的理解详细注释,配合示意图,应该不难理解。
最后,奉上项目包,容各位慢慢体会
点击下载