参考鸿洋博客的一篇文章:
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实现滑动菜单比较简单,且整个项目我已经按照我的理解详细注释,配合示意图,应该不难理解。
最后,奉上项目包,容各位慢慢体会
点击下载