请注意:本答案最初是针对 Android 4.4(KitKat)撰写的,当时这个版本还比较新。自从 Android 5.0 发布以来,尤其是由于引入了 ToolBar
,本答案已经不再适用!但从技术角度和想要学习 Android 内部工作原理的人来说,本答案可能仍然具有很大价值。
NavigationDrawer
专门设计为位于 ActionBar
下方,并且无法实现 NavigationDrawer
使 ActionBar
跟随移动 - 除非查找构成 ActionBar
的 View
并将其与 NavigationDrawer
一起进行动画处理,但我绝不建议这样做,因为这会很困难并且容易出错。在我看来,你只有两个选择:
- 使用类似于 SlidingMenu 的库
- 实现一个自定义滑动菜单
既然你说不想使用库,那么实现一个自定义滑动菜单就是唯一的选择了。幸运的是,一旦你知道如何做,这并不难。
1)基本解释
您可以通过在组成 Activity
的 View
上添加 margin 或 padding 来移动整个内容 - 包括 ActionBar
。这个 View
是具有 id 为 android.R.id.content
的 View
的父级:
View content = (View) activity.findViewById(android.R.id.content).getParent();
在Honeycomb(Android版本3.0-API级别11)或更高版本中 - 换句话说,即在介绍
ActionBar
之后 - 您需要使用边距来更改
Activities
的位置,在之前的版本中,您需要使用填充。为了简化操作,建议创建帮助方法来为每个API级别执行正确的操作。让我们首先看如何设置
Activity
的位置:
public void setActivityPosition(int x, int y) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
contentParams.setMargins(x, y, -x, -y);
this.content.setLayoutParams(contentParams);
} else {
this.content.setPadding(x, y, -x, -y);
}
}
请注意,在这两种情况下,相反的边缘都有负边距或负填充。这实质上是为了增加Activity
的大小超出其正常范围。这可以防止将Activity
滑动到其他位置时实际大小发生更改。
此外,我们还需要两种方法来获取Activity
的当前位置。一种是获取x位置,另一种是获取y位置:
public int getActivityPositionX() {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
return contentParams.leftMargin;
} else {
return this.content.getPaddingLeft();
}
}
public int getActivityPositionY() {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
return contentParams.topMargin;
} else {
return this.content.getPaddingTop();
}
}
添加动画效果也非常简单。这里唯一重要的事情就是使用一些数学知识将其从先前的位置动画到新位置
// We get the current position of the Activity
final int currentX = getActivityPositionX();
final int currentY = getActivityPositionY();
// The new position is set
setActivityPosition(x, y);
// We animate the Activity to slide from its previous position to its new position
TranslateAnimation animation = new TranslateAnimation(currentX - x, 0, currentY - y, 0);
animation.setDuration(500);
this.content.startAnimation(animation);
您可以通过将 View 添加到其父 View 中,在通过滑动 Activity 显示位置的同时显示它:
final int currentX = getActivityPositionX();
FrameLayout menuContainer = new FrameLayout(context);
// The width of the menu is equal to the x position of the `Activity`
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(currentX, ViewGroup.LayoutParams.MATCH_PARENT);
menuContainer.setLayoutParams(params);
ViewGroup parent = (ViewGroup) content.getParent();
parent.addView(menuContainer);
这基本上是创建一个适用于大多数设备(Eclair(Android 2.1 - API level 7)及以上)的基本滑动菜单所需的全部内容。
2) 动画化Activity
创建滑动菜单的第一步是使Activity
移开。因此,我们首先应该尝试像这样移动Activity
:
![enter image description here](https://istack.dev59.com/993vv.gif)
要实现这个效果,我们只需要将上面的代码组合在一起即可:
import android.os.Build;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
public class ActivitySlider {
private final FragmentActivity activity;
private final View content;
public ActivitySlider(FragmentActivity activity) {
this.activity = activity;
this.content = (View) activity.findViewById(android.R.id.content).getParent();
}
public void slideTo(int x, int y) {
final int currentX = getActivityPositionX();
final int currentY = getActivityPositionY();
setActivityPosition(x, y);
TranslateAnimation animation = new TranslateAnimation(currentX - x, 0, currentY - y, 0);
animation.setDuration(500);
this.content.startAnimation(animation);
}
public void setActivityPosition(int x, int y) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
contentParams.setMargins(x, y, -x, -y);
this.content.setLayoutParams(contentParams);
} else {
this.content.setPadding(x, y, -x, -y);
}
}
public int getActivityPositionX() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
return contentParams.leftMargin;
} else {
return this.content.getPaddingLeft();
}
}
public int getActivityPositionY() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
return contentParams.topMargin;
} else {
return this.content.getPaddingTop();
}
}
}
您可以像这样使用
ActivitySlider
类:
ActivitySlider slider = new ActivitySlider(activity);
// This would move the Activity 400 pixel to the right and 100 pixel down
slider.slideTo(400, 100);
3) 添加滑动菜单
现在我们想要在 Activity
移开时显示一个菜单,就像这样:
![enter image description here](https://istack.dev59.com/wYSpD.gif)
如您所见,它也将 ActionBar
推到了一侧。
ActivitySlider
类不需要太多修改来创建一个滑动菜单,基本上我们只需要添加两个方法: showMenu()
和 hideMenu()
。我会坚持最佳实践,使用一个 Fragment
作为滑动菜单。我们需要的第一件事是一个 View
- 例如一个 FrameLayout
- 作为我们 Fragment
的容器。我们需要将这个 View
添加到 Activity
的父级 View
中:
View content = (View) activity.findViewById(android.R.id.content).getParent();
ViewGroup parent = (ViewGroup) content.getParent();
FrameLayout menuContainer = new FrameLayout(this.activity);
menuContainer.setId(R.id.flMenuContainer);
menuContainer.setVisibility(View.GONE);
parent.addView(menuContainer);
由于我们只在滑动菜单实际打开时将容器 View
的可见性设置为 VISIBLE,因此我们可以使用以下方法检查菜单是打开还是关闭:
public boolean isMenuVisible() {
return this.menuContainer.getVisibility() == View.VISIBLE;
}
为了设置菜单
Fragment
,我们添加一个设置器方法,执行
FragmentTransaction
并将菜单
Fragment
添加到
FrameLayout
中。
public void setMenuFragment(Fragment fragment) {
FragmentManager manager = this.activity.getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(R.id.flMenuContainer, fragment);
transaction.commit();
}
为了方便起见,我通常会添加第二个setter方法,通过从Class
实例化Fragment
:
public <T extends Fragment> void setMenuFragment(Class<T> cls) {
Fragment fragment = Fragment.instantiate(this.activity, cls.getName());
setMenuFragment(fragment);
}
在考虑菜单片段
时,还有一件非常重要的事情需要考虑。我们所操作的View
层次比通常更高。因此,我们必须考虑状态栏的高度等因素。如果我们没有考虑到这一点,菜单片段
的顶部将被隐藏在状态栏后面。可以像这样获取状态栏的高度:
Rect rectangle = new Rect();
Window window = this.activity.getWindow();
window.getDecorView().getWindowVisibleDisplayFrame(rectangle);
final int statusBarHeight = rectangle.top;
我们需要给菜单
Fragment
的容器
View
加上一个顶部边距,就像这样:
// These are the LayoutParams for the menu Fragment
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(width, ViewGroup.LayoutParams.MATCH_PARENT)
// We put a top margin on the menu Fragment container which is equal to the status bar height
params.setMargins(0, statusBarHeight, 0, 0)
menuContainer.setLayoutParams(fragmentParams)
最后,我们可以把所有这些内容综合起来:
import android.graphics.Rect;
import android.os.Build;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import at.test.app.R;
import at.test.app.helper.LayoutHelper;
public class ActivitySlider {
private final FragmentActivity activity;
private final View content;
private final FrameLayout menuContainer;
public ActivitySlider(FragmentActivity activity) {
this.activity = activity;
this.content = (View) activity.findViewById(android.R.id.content).getParent();
ViewGroup parent = (ViewGroup) this.content.getParent();
this.menuContainer = new FrameLayout(this.activity);
this.menuContainer.setId(R.id.flMenuContainer);
this.menuContainer.setVisibility(View.GONE);
parent.addView(this.menuContainer);
}
public <T extends Fragment> void setMenuFragment(Class<T> cls) {
Fragment fragment = Fragment.instantiate(this.activity, cls.getName());
setMenuFragment(fragment);
}
public void setMenuFragment(Fragment fragment) {
FragmentManager manager = this.activity.getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(R.id.flMenuContainer, fragment);
transaction.commit();
}
public boolean isMenuVisible() {
return this.menuContainer.getVisibility() == View.VISIBLE;
}
public void showMenu(int dpWidth) {
final int menuWidth = LayoutHelper.dpToPixel(this.activity, dpWidth);
slideTo(menuWidth, 0);
Rect rectangle = new Rect();
Window window = this.activity.getWindow();
window.getDecorView().getWindowVisibleDisplayFrame(rectangle);
final int statusBarHeight = rectangle.top;
FrameLayout.LayoutParams fragmentParams = new FrameLayout.LayoutParams(menuWidth, ViewGroup.LayoutParams.MATCH_PARENT);
fragmentParams.setMargins(0, statusBarHeight, 0, 0);
this.menuContainer.setLayoutParams(fragmentParams);
if(!isMenuVisible()) {
this.menuContainer.setVisibility(View.VISIBLE);
TranslateAnimation animation = new TranslateAnimation(-menuWidth, 0, 0, 0);
animation.setDuration(500);
this.menuContainer.startAnimation(animation);
}
}
public void hideMenu() {
if(isMenuVisible()) {
slideTo(0, 0);
final int menuWidth = this.menuContainer.getWidth();
TranslateAnimation menuAnimation = new TranslateAnimation(0, -menuWidth, 0, 0);
menuAnimation.setDuration(500);
menuAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
menuContainer.setVisibility(View.GONE);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
this.menuContainer.startAnimation(menuAnimation);
}
}
public void slideTo(int x, int y) {
final int currentX = getActivityPositionX();
final int currentY = getActivityPositionY();
setActivityPosition(x, y);
TranslateAnimation animation = new TranslateAnimation(currentX - x, 0, currentY - y, 0);
animation.setDuration(500);
this.content.startAnimation(animation);
}
public void setActivityPosition(int x, int y) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
contentParams.setMargins(x, y, -x, -y);
this.content.setLayoutParams(contentParams);
} else {
this.content.setPadding(x, y, -x, -y);
}
}
public int getActivityPositionX() {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
return contentParams.leftMargin;
} else {
return this.content.getPaddingLeft();
}
}
public int getActivityPositionY() {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
return contentParams.topMargin;
} else {
return this.content.getPaddingTop();
}
}
}
我在showMenu()
中使用了一个静态的帮助方法来将dip转换为像素。以下是该方法的代码:
public static int dpToPixel(Context context, int dp) {
float scale = getDisplayDensityFactor(context);
return (int) (dp * scale + 0.5f);
}
private static float getDisplayDensityFactor(Context context) {
if (context != null) {
Resources res = context.getResources();
if (res != null) {
DisplayMetrics metrics = res.getDisplayMetrics();
if(metrics != null) {
return metrics.density;
}
}
}
return 1.0f;
}
你可以像这样使用新版本的
ActivitySlider
类:
ActivitySlider slider = new ActivitySlider(activity);
slider.setMenuFragment(MenuFragment.class);
// The menu is shown with a width of 200 dip
slider.showMenu(200);
...
// Hide the menu again
slider.hideMenu();
4) 结论与测试
当你知道可以在 Activity
的 View
上设置margin或padding时,像这样做起来非常容易。但难点在于如何使其在许多不同的设备上正常工作。实现可能会在多个 API Level 上发生很大变化,并且这可能会对其行为产生重大影响。话虽如此,我在此发布的任何代码都应该可以在大多数(如果不是所有)Eclair(Android 2.1 - API level 7)以上的设备上正常运行,没有任何问题。
当然,我在这里发布的解决方案并不完整,它可能需要一些额外的改进和清理,因此请随意根据您的需求改进代码!
我已在以下设备上进行了测试:
HTC
- One M8(Android 4.4.2-奇巧巧克力):正常工作
- Sensation(Android 4.0.3-冰淇淋三明治):正常工作
- Desire(Android 2.3.3-姜饼):正常工作
- One(Android 4.4.2-奇巧巧克力):正常工作
三星(Samsung)
- Galaxy S3 Mini(Android 4.1.2-果冻豆):正常工作
- Galaxy S4 Mini(Android 4.2.2-果冻豆):正常工作
- Galaxy S4(Android 4.4.2-奇巧巧克力):正常工作
- Galaxy S5(Android 4.4.2-奇巧巧克力):正常工作
- Galaxy S Plus(Android 2.3.3-姜饼):正常工作
- Galaxy Ace(Android 2.3.6-姜饼):正常工作
- Galaxy S2(Android 4.1.2-果冻豆):正常工作
- Galaxy S3(Android 4.3-果冻豆):正常工作
- Galaxy Note 2(Android 4.3-果冻豆):正常工作
- Galaxy Nexus(Android 4.2.1-果冻豆):正常工作
Motorola
- Moto G(Android 4.4.2-奇巧巧克力):正常工作
LG
- Nexus 5(Android 4.4.2-奇巧巧克力):正常工作
ZTE
- Blade(Android 2.1-Eclair):正常工作
我希望我能够帮助您,如果您有任何进一步的问题或其他不清楚的地方,请随时提出!