如何使ActionBar与NavigationDrawer一起滑动

15

我想要做的是,当导航抽屉被打开时,与之一起滑动ActionBar。目前我没有使用任何第三方库,如果可能的话,我希望保持不变。我需要的只是像这样实现的方法:getActionBarView.slide(dp);

这是我目前用来创建NavigationDrawer的代码:

mDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close) {

    public void onDrawerClosed(View view) {
        invalidateOptionsMenu();

        // calling onPrepareOptionsMenu() to hide action bar icons
    }

    @Override
    public void onDrawerSlide(View drawerView, float slideOffset) {
        if (getDeviceType(getApplicationContext()) == DEVICE_TYPE_PHONE) {
            drawerLayout.setScrimColor(Color.parseColor("#00FFFFFF"));
            float moveFactor = (listView.getWidth() * slideOffset);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                all_menu_container_parent.setTranslationX(moveFactor);
            } else {
                TranslateAnimation anim = new TranslateAnimation(lastTranslate, moveFactor, 0.0f, 0.0f);
                anim.setDuration(0);
                anim.setFillAfter(true);
                all_menu_container_parent.startAnimation(anim);

                lastTranslate = moveFactor;
            }
        }
    }

    public void onDrawerOpened(View drawerView) {
        // calling onPrepareOptionsMenu() to hide action bar icons
    }
};
drawerLayout.setDrawerListener(mDrawerToggle);

但它不能做我想要的事情,它生成了这个:

I am currently stuck with this

我想要实现的是这个:

current screen shot from app

1个回答

60

请注意:本答案最初是针对 Android 4.4(KitKat)撰写的,当时这个版本还比较新。自从 Android 5.0 发布以来,尤其是由于引入了 ToolBar,本答案已经不再适用!但从技术角度和想要学习 Android 内部工作原理的人来说,本答案可能仍然具有很大价值。

NavigationDrawer 专门设计为位于 ActionBar 下方,并且无法实现 NavigationDrawer 使 ActionBar 跟随移动 - 除非查找构成 ActionBarView 并将其与 NavigationDrawer 一起进行动画处理,但我绝不建议这样做,因为这会很困难并且容易出错。在我看来,你只有两个选择:

  1. 使用类似于 SlidingMenu 的库
  2. 实现一个自定义滑动菜单

既然你说不想使用库,那么实现一个自定义滑动菜单就是唯一的选择了。幸运的是,一旦你知道如何做,这并不难。


1)基本解释

您可以通过在组成 ActivityView 上添加 margin 或 padding 来移动整个内容 - 包括 ActionBar。这个 View 是具有 id 为 android.R.id.contentView 的父级:

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) {
    // With this if statement we can check if the devices API level is above Honeycomb or below
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        // On Honeycomb or abvoe we set a margin
        FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
        contentParams.setMargins(x, y, -x, -y);
        this.content.setLayoutParams(contentParams);
    } else {
        // And on devices below Honeycomb we set a padding
        this.content.setPadding(x, y, -x, -y);
    }
}

请注意,在这两种情况下,相反的边缘都有负边距或负填充。这实质上是为了增加Activity的大小超出其正常范围。这可以防止将Activity滑动到其他位置时实际大小发生更改。

此外,我们还需要两种方法来获取Activity的当前位置。一种是获取x位置,另一种是获取y位置:

public int getActivityPositionX() {
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        // On Honeycomb or above we return the left margin
        FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
        return contentParams.leftMargin;
    } else {
        // On devices below Honeycomb we return the left padding
        return this.content.getPaddingLeft();
    }
}

public int getActivityPositionY() {
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        // On Honeycomb or above we return the top margin
        FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
        return contentParams.topMargin;
    } else {
        // On devices below Honeycomb we return the top padding
        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

要实现这个效果,我们只需要将上面的代码组合在一起即可:

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;

        // Here we get the content View from the Activity.
        this.content = (View) activity.findViewById(android.R.id.content).getParent();
    }

    public void slideTo(int x, int y) {

        // 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);
    }

    public void setActivityPosition(int x, int y) {
        // With this if statement we can check if the devices API level is above Honeycomb or below
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // On Honeycomb or above we set a margin
            FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
            contentParams.setMargins(x, y, -x, -y);
            this.content.setLayoutParams(contentParams);
        } else {
            // And on devices below Honeycomb we set a padding
            this.content.setPadding(x, y, -x, -y);
        }
    }

    public int getActivityPositionX() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // On Honeycomb or above we return the left margin
            FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
            return contentParams.leftMargin;
        } else {
            // On devices below Honeycomb we return the left padding
            return this.content.getPaddingLeft();
        }
    }

    public int getActivityPositionY() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // On Honeycomb or above we return the top margin
            FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
            return contentParams.topMargin;
        } else {
            // On devices below Honeycomb we return the top padding
            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
如您所见,它也将 ActionBar 推到了一侧。

ActivitySlider 类不需要太多修改来创建一个滑动菜单,基本上我们只需要添加两个方法: showMenu()hideMenu()。我会坚持最佳实践,使用一个 Fragment 作为滑动菜单。我们需要的第一件事是一个 View - 例如一个 FrameLayout - 作为我们 Fragment 的容器。我们需要将这个 View 添加到 Activity 的父级 View 中:

// We get the View of the Activity
View content = (View) activity.findViewById(android.R.id.content).getParent();

// And its parent
ViewGroup parent = (ViewGroup)  content.getParent();

// The container for the menu Fragment is a FrameLayout
// We set an id so we can perform FragmentTransactions later on
FrameLayout menuContainer = new FrameLayout(this.activity);
menuContainer.setId(R.id.flMenuContainer);

// The visibility is set to GONE because the menu is initially hidden
menuContainer.setVisibility(View.GONE);

// The container for the menu Fragment is added to the parent
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;

        // We get the View of the Activity
        this.content = (View) activity.findViewById(android.R.id.content).getParent();

        // And its parent
        ViewGroup parent = (ViewGroup) this.content.getParent();

        // The container for the menu Fragment is added to the parent. We set an id so we can perform FragmentTransactions later on
        this.menuContainer = new FrameLayout(this.activity);
        this.menuContainer.setId(R.id.flMenuContainer);

        // We set visibility to GONE because the menu is initially hidden
        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;
    }

    // We pass the width of the menu in dip to showMenu()
    public void showMenu(int dpWidth) {

        // We convert the width from dip into pixels
        final int menuWidth = LayoutHelper.dpToPixel(this.activity, dpWidth);

        // We move the Activity out of the way
        slideTo(menuWidth, 0);

        // We have to take the height of the status bar at the top into account!
        Rect rectangle = new Rect();
        Window window = this.activity.getWindow();
        window.getDecorView().getWindowVisibleDisplayFrame(rectangle);
        final int statusBarHeight = rectangle.top;

        // These are the LayoutParams for the menu Fragment
        FrameLayout.LayoutParams fragmentParams = new FrameLayout.LayoutParams(menuWidth, ViewGroup.LayoutParams.MATCH_PARENT);

        // We put a top margin on the menu Fragment container which is equal to the status bar height
        fragmentParams.setMargins(0, statusBarHeight, 0, 0);
        this.menuContainer.setLayoutParams(fragmentParams);

        // Perform the animation only if the menu is not visible
        if(!isMenuVisible()) {

            // Visibility of the menu container View is set to VISIBLE
            this.menuContainer.setVisibility(View.VISIBLE);

            // The menu slides in from the right
            TranslateAnimation animation = new TranslateAnimation(-menuWidth, 0, 0, 0);
            animation.setDuration(500);
            this.menuContainer.startAnimation(animation);
        }
    }

    public void hideMenu() {

        // We can only hide the menu if it is visible
        if(isMenuVisible()) {

            // We slide the Activity back to its original position
            slideTo(0, 0);

            // We need the width of the menu to properly animate it
            final int menuWidth = this.menuContainer.getWidth();

            // Now we need an extra animation for the menu fragment container
            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) {
                    // As soon as the hide animation is finished we set the visibility of the fragment container back to GONE
                    menuContainer.setVisibility(View.GONE);
                }

                @Override
                public void onAnimationRepeat(Animation animation) {

                }
            });
            this.menuContainer.startAnimation(menuAnimation);
        }
    }

    public void slideTo(int x, int y) {

        // 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);
    }

    public void setActivityPosition(int x, int y) {
        // With this if statement we can check if the devices API level is above Honeycomb or below
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // On Honeycomb or above we set a margin
            FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
            contentParams.setMargins(x, y, -x, -y);
            this.content.setLayoutParams(contentParams);
        } else {
            // And on devices below Honeycomb we set a padding
            this.content.setPadding(x, y, -x, -y);
        }
    }

    public int getActivityPositionX() {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // On Honeycomb or above we return the left margin
            FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
            return contentParams.leftMargin;
        } else {
            // On devices below Honeycomb we return the left padding
            return this.content.getPaddingLeft();
        }
    }

    public int getActivityPositionY() {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // On Honeycomb or above we return the top margin
            FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
            return contentParams.topMargin;
        } else {
            // On devices below Honeycomb we return the top padding
            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) 结论与测试

当你知道可以在 ActivityView 上设置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):正常工作

我希望我能够帮助您,如果您有任何进一步的问题或其他不清楚的地方,请随时提出!


我需要你的帮助在这个。 - young_08
1
@young_08 那又怎样?如果你有问题,请另起一条提问。 - Xaver Kapeller
我想询问关于上面的内容。我已经实现了相同的功能。但现在想知道如何在抽屉项上实现点击监听器?因为对于抽屉,我们正在传递片段。 - young_08
@young_08 你可以像在任何其他“Fragment”中一样进行操作。但是为什么要使用这种解决方案呢?你知道有一些库比这个答案中描述的类更好用且功能更强大,对吧?这个答案只是作为一个教育练习而存在,并不是应该在任何真实应用程序中使用的东西。此外还有“NavigationDrawer”。它是来自Google的一个组件,实现了非常相似的行为。 - Xaver Kapeller
是的,我知道。但我想要实现抽屉移动操作栏。我认为这不能通过使用它来实现。请给出建议。我该如何实现?不使用库。我将非常感激。 - young_08
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接