Ответ 1
ОБРАТИТЕ ВНИМАНИЕ: Этот ответ был первоначально написан, когда Android 4.4 (KitKat) все еще был довольно новым. Начиная с Android 5.0 и особенно из-за введения
ToolBar
этот ответ не может быть считается актуальным больше! Но с технической точки зрения и для те из вас, кто хочет узнать о внутренней работе Android этот ответ может по-прежнему иметь большую ценность!
NavigationDrawer
был специально разработан для размещения ниже ActionBar
, и нет способа реализовать NavigationDrawer
, чтобы сделать ActionBar
перемещение с ним - если, возможно, не поиск View
, который составляет ActionBar
и оживить его вместе с NavigationDrawer
, но я бы никогда не рекомендовал что-то подобное, поскольку это было бы трудно и подвержено ошибкам. По-моему, у вас есть только два варианта:
- Использование библиотека, такая как SlidingMenu
- Реализация пользовательского скользящего меню
Поскольку вы сказали, что не хотите использовать библиотеку, реализующую пользовательское скользящее меню, это ваш единственный вариант, к счастью, это действительно не так сложно, как только вы знаете, как это сделать.
1) Основное объяснение
Вы можете перемещать весь контент Activity
- я имею в виду все, включая ActionBar
-, помещая маркер или дополнение на View
, которое составляет Activity
. Этот View
является родительским элементом View
с id android.R.id.content
:
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
в месте, которое открывается, сместив Activity
, добавив его к родительскому элементу View
:
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 уровня 7).
2) Анимация Activity
Первая часть создания скользящего меню делает переход Activity
в сторону. Поэтому мы должны сначала попытаться переместить Activity
следующим образом:
Чтобы создать это, нам просто нужно добавить код выше:
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
будет удаляться следующим образом:
Как вы можете видеть, это также толкает ActionBar
в сторону.
Класс ActivitySlider
не нуждается в модификации, чтобы создать скользящее меню, в основном мы просто добавляем два метода: showMenu()
и hideMenu()
. Я буду придерживаться лучших практик и использовать Fragment
в качестве скользящего меню. Первое, что нам нужно, это View
- например, FrameLayout
- как контейнер для нашего Fragment
. Нам нужно добавить этот View
к родительскому элементу View
Activity
:
// 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
, мы добавим метод setter, который выполняет 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();
}
Я также склонен добавить второй сеттер, который для удобства создает Fragment
из Class
:
public <T extends Fragment> void setMenuFragment(Class<T> cls) {
Fragment fragment = Fragment.instantiate(this.activity, cls.getName());
setMenuFragment(fragment);
}
Есть еще одна важная вещь, которую нужно учитывать, когда дело доходит до меню Fragment
. Мы работаем гораздо дальше в иерархии View
, чем обычно. Таким образом, мы должны принимать во внимание такие вещи, как высота строки состояния. Если бы мы не учли это в верхней части меню Fragment
, мы были бы скрыты за строкой состояния. Вы можете получить высоту строки состояния следующим образом:
Rect rectangle = new Rect();
Window window = this.activity.getWindow();
window.getDecorView().getWindowVisibleDisplayFrame(rectangle);
final int statusBarHeight = rectangle.top;
Мы должны поместить верхнее поле в контейнер View
меню Fragment
следующим образом:
// 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) Заключение и тестирование
Выполнение чего-то подобного на удивление легко, когда вы знаете, что вы можете просто поместить маржу или дополнение на View
в Activity
. Но сложность заключается в том, чтобы заставить его работать на множестве разных устройств. Реализации могут сильно измениться на нескольких уровнях API и могут иметь значительное влияние на то, как это ведет себя. Сказав, что любой код, который я написал здесь, должен работать на большинстве, если не на всех устройствах выше Eclair (Android 2.1 - API уровня 7) без проблем.
Конечно, решение, которое я разместил здесь, не является полным, он может использовать небольшую дополнительную полировку и очистку, поэтому не стесняйтесь улучшать код в соответствии с вашими потребностями.
Я тестировал все на следующих устройствах:
HTC
- One M8 (Android 4.4.2 - KitKat): Работает
- Сенсация (Android 4.0.3 - сэндвич с мороженым): Работа
- Desire (Android 2.3.3 - Gingerbread): Работает
- Один (Android 4.4.2 - KitKat): Работающий
Samsung
- Galaxy S3 Mini (Android 4.1.2 - желе Bean): Работающий
- Galaxy S4 Mini (Android 4.2.2 - желе Bean): Работающий
- Galaxy S4 (Android 4.4.2 - KitKat): Работает
- Galaxy S5 (Android 4.4.2 - KitKat): Работающий
- Galaxy S Plus (Android 2.3.3 - Gingerbread): Работа
- Galaxy Ace (Android 2.3.6 - Gingerbread): Работающий
- Galaxy S2 (Android 4.1.2 - желе Bean): Работа
- Galaxy S3 (Android 4.3 - желе Bean): Работает
- Galaxy Note 2 (Android 4.3 - желе Bean): Работа
- Galaxy Nexus (Android 4.2.1 - желе Bean): Работа
Motorola
- Moto G (Android 4.4.2 - KitKat): Работа
LG
- Nexus 5 (Android 4.4.2 - KitKat): Работает
ZTE
- Blade (Android 2.1 - Eclair): Работает
Я надеюсь, что смогу помочь вам, и если у вас возникнут какие-либо дополнительные вопросы или что-то еще непонятное, не стесняйтесь спрашивать!