Pull to refresh

Переводим ActionBar на следующий уровень

Reading time9 min
Views37K
Original author: Cyril Mottier
Еще в ноябре 2012 года, я написал сообщение в блоге озаглавленное как "ActionBar в движении". Эта статья в основном касалась методики того, как красиво и уникально оживить ваш ActionBar . Хотя я упомянул некоторые из возможностей применения данного эффекта, я никогда не имел времени, чтобы добавить данный вид анимации ActionBar к какому-либо из моих собственных приложений. Но я видел в Play Store приложение использовавшее его.

Будучи на Google I/O, я наконец нашел приложение, использующее в ActionBar технику анимации. Давайте будем честными, это буквально взорвало мой взгляд когда я в первый раз это увидел. Я влюбился в хороший, тонкий и чрезвычайно полезный анимационный эффект, и, вероятно, даже больше, чем в само приложение! Я уверен, вы знаете приложение о котором я говорю, так как оно было представлено во время Google I/O. Это приложение Play Music!

Последнее обновление Play Music (v5.0) претерпело полный редизайн и внешний вид страниц с описанием исполнителя и альбома. Если вы откроете такую страницу, то вы заметите, что ActionBar изначально невидим и наслаивается на большое изображение, описывающее исполнителя или альбом. Но как только вы начинаете прокручивать страницу вниз (если это возможно), то ActionBar постепенно проявляется. ActionBar оказывается полностью непрозрачным, когда изображение становится прокрученным за экран.

Вот два основных преимущества такой ActionBar анимации:
  • Изысканный UI: анимация синхронизирована с элементом на который вы взаимодействуете и положительно оценивается пользователями, потому что заставляет их чувствовать себя естественно с вашим UI, которое реагирует на их действия. Затухание анимации является прямым следствием попиксельного состоянии прокрутки, а не однократно запущенной анимации.
  • Использует преимущество реального состояния экрана: сохраняя UX платформы, такая модель позволяет пользователю сосредоточиться в первую очередь на содержании, а не на управлении. Используется как дополнение к красивому дизайну и может восприниматься как увлекательная игра в интерфейсе вашего приложения.

В этой статье я углублюсь в детали реализации методики, описанной в статье "ActionBar в движении", чтобы создать эффект аналогичный тому, который используется в приложении Play Music. Для того, чтобы лучше понять какие цели мы планируем достигнуть, вы можете взглянуть на скриншоты ниже или же загрузить пример приложения.
image

Download the sample APK

Темы и стили для приложения


Как вы можете легко заметить, для того чтобы воспроизвести такой эффект, ActionBar должен перекрывать содержимое экрана. Это делается с помощью XML-атрибута android:windowActionBarOverlay. Приведенный ниже код описывает определение темы, которую мы будем использовать:
values/themes.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Theme.TranslucentActionBar" parent="@android:style/Theme.Holo.Light.DarkActionBar">
        <item name="android:actionBarStyle">@style/Widget.ActionBar</item>
    </style>

    <style name="Theme.TranslucentActionBar.ActionBar" />

    <style name="Theme.TranslucentActionBar.ActionBar.Overlay">
        <item name="android:actionBarStyle">@style/Widget.ActionBar.Transparent</item>
        <item name="android:windowActionBarOverlay">true</item>
    </style>
</resources>

Логично, что стиль ActionBar определен в values/styles.xml следующим образом:
values/styles.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Widget.ActionBar" parent="@android:style/Widget.Holo.Light.ActionBar.Solid.Inverse">
        <item name="android:background">@drawable/ab_background</item>
    </style>

    <style name="Widget.ActionBar.Transparent">
        <item name="android:background">@android:color/transparent</item>
    </style>
</resources>

В заключение, мы должны использовать данную тему, чтобы применить этот стиль к нашей Activity:
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.cyrilmottier.android.translucentactionbar"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="17" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.TranslucentActionBar">

        <activity
                android:name=".HomeActivity"
                android:theme="@style/Theme.TranslucentActionBar.ActionBar.Overlay">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>

Обратите внимание, что с помощью тем и стилей мы удаляем все потенциальные проблемы мерцания при запуске (см. «Правильно сделанный запуск Android-приложения» для получения дополнительной информации).

Получение готового содержания


Как я пояснил ранее, затухание ActionBar синхронизировано на попиксельным состоянии контейнера прокрутки. В этом примере мы просто будем использовать ScrollView как контейнер прокрутки. Одним из основных недостатков этого контейнера является то, что вы не можете зарегистрировать слушателя для того, чтобы получать уведомления, когда состояние прокрутки изменилось. Но это можно легко обойти, создав NotifyingScrollView унаследованный от оригинального ScrollView:
NotifyingScrollView.java
package com.cyrilmottier.android.translucentactionbar;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.ScrollView;

/**
 * @author Cyril Mottier
 */
public class NotifyingScrollView extends ScrollView {

    /**
     * @author Cyril Mottier
     */
    public interface OnScrollChangedListener {
        void onScrollChanged(ScrollView who, int l, int t, int oldl, int oldt);
    }

    private OnScrollChangedListener mOnScrollChangedListener;

    public NotifyingScrollView(Context context) {
        super(context);
    }

    public NotifyingScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public NotifyingScrollView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (mOnScrollChangedListener != null) {
            mOnScrollChangedListener.onScrollChanged(this, l, t, oldl, oldt);
        }
    }

    public void setOnScrollChangedListener(OnScrollChangedListener listener) {
        mOnScrollChangedListener = listener;
    }

}

Теперь мы можем использовать этот новый контейнер прокрутки в нашем XML-макете:
layout/activity_home.xml
<?xml version="1.0" encoding="utf-8"?>
<com.cyrilmottier.android.translucentactionbar.NotifyingScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scroll_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/image_header"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:scaleType="centerCrop"
            android:src="@drawable/daft_punk"/>

      <! -- Some long content -->

    </LinearLayout>

</com.cyrilmottier.android.translucentactionbar.NotifyingScrollView>


Показываем\скрываем ActionBar


Теперь большая часть нашего шаблона готова, и мы можем подключить все эти компоненты вместе. Используемый для ActionBar алгоритм достаточно прост и состоит только из расчета альфа-канала в зависимости от текущего пиксельного состояния нашего контейнера прокрутки NotifyingScrollView. Следует отметить, что эффективное расстояние прокрутки должно быть прикреплено на [0, image_height - actionbar_height], чтобы избежать ошибочных значений, которые могут возникнуть из-за поведения полос контейнера прокрутки в Android:
HomeActivity.java
package com.cyrilmottier.android.translucentactionbar;

import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.util.Log;
import android.view.Menu;
import android.widget.ScrollView;

public class HomeActivity extends Activity {

    private Drawable mActionBarBackgroundDrawable;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);

        mActionBarBackgroundDrawable = getResources().getDrawable(R.drawable.ab_background);
        mActionBarBackgroundDrawable.setAlpha(0);

        getActionBar().setBackgroundDrawable(mActionBarBackgroundDrawable);

        ((NotifyingScrollView) findViewById(R.id.scroll_view)).setOnScrollChangedListener(mOnScrollChangedListener);
    }

    private NotifyingScrollView.OnScrollChangedListener mOnScrollChangedListener = new NotifyingScrollView.OnScrollChangedListener() {
        public void onScrollChanged(ScrollView who, int l, int t, int oldl, int oldt) {
            final int headerHeight = findViewById(R.id.image_header).getHeight() - getActionBar().getHeight();
            final float ratio = (float) Math.min(Math.max(t, 0), headerHeight) / headerHeight;
            final int newAlpha = (int) (ratio * 255);
            mActionBarBackgroundDrawable.setAlpha(newAlpha);
        }
    };
}

Как описано в статье «ActionBar в движении», этот вышеприведенный фрагмент кода не работает для pre-JELLY_BEAN_MR1 устройств. Действительно, ActionBar не обозначает себя недействительным, когда требуется, поскольку он не регистрирует себя в качестве обратного вызова для Drawable. Вы можете обойти эту проблему просто прикрепив следующие Callback в onCreate(Bundle) методе:
HomeActivity.java
private Drawable.Callback mDrawableCallback = new Drawable.Callback() {
    @Override
    public void invalidateDrawable(Drawable who) {
        getActionBar().setBackgroundDrawable(who);
    }

    @Override
    public void scheduleDrawable(Drawable who, Runnable what, long when) {
    }

    @Override
    public void unscheduleDrawable(Drawable who, Runnable what) {
    }
};


HomeActivity.java
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
    mActionBarBackgroundDrawable.setCallback(mDrawableCallback);
}

Вы уже можете запустить код. Хотя результат выглядит также как и в анимации приложения Play Music, мы можем продолжить дорабатывать его, чтобы сделать ещё лучше.

Завершающие мазки кисти


Усиливаем контраст ActionBar

Наличие прозрачной ActionBar может привести к вопросам дизайна, потому что вы, как правило, не знаете о цвете фона, который вы будете отображать в верхней части. Например, вы можете в конечном итоге отображать прозрачный ActionBar с белым заголовком и белым изображением в описании. Не нужно говорить, что это делает ActionBar невидимым и бесполезным.
Самый простой способ избежать такой проблемы состоит в модификации изображения, чтобы сделать его немного темнее в верхней части. Таким образом, в самом худшем случае (т.е. белое изображение) у нас серая область в верхней части ActionBar делает его содержание (название, иконки, кнопки и т.д.) видимым.
Сделать это можно наложив темный полупрозрачный градиент в верхней части изображения, который мы создадим в XML-описании Drawable:
drawable/gradient.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">

    <size android:height="100dp"/>

    <gradient
        android:angle="270"
        android:startColor="#8000"
        android:endColor="#0000"/>

</shape>

Наложение градиента реализуем через FrameLayout:
layout/activity_home.xml
<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/image_header"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scaleType="centerCrop"
        android:src="@drawable/daft_punk"/>

    <View
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/gradient"/>

</FrameLayout>

Избегаем чрезмерной прокрутки

В Gingerbread (API 9), Android представил новый способ уведомления пользователя о том, что контейнер прокрутки пытается выйти за рамки содержания границ. Впервые было введено понятие EdgeEffect (есть в API начиная API 14), включающее over-scroll. Хотя это не является проблемой, но в общем, это может сильно раздражать, когда один из краев прокручиваемого содержимого, отличается от цвета фона.
Вы можете воспроизвести это просто быстро прокрутив ScrollView вверх. И вы заметите как что-то белого цвета (цвет фона) появляется в верхней части экрана, потому что изображение прокручено за его пределы. Лично я считаю этот глюком интерфейса и обычно предпочитаю отключить такое поведение.
Одним из лучших способов избежать over-scroll является использование View#setOverScrollMode(int), чтобы изменить режим View#OVER_SCROLL_NEVER. Несмотря на то, что это работает, но такой вариант также удаляет EdgeEffect, что может быть визуально не совсем приемлемым. Поэтому проще изменить NotifyingScrollView, чтобы ограничить over-scroll значения до нуля, когда это необходимо:
NotifyingScrollView.java
private boolean mIsOverScrollEnabled = true;

public void setOverScrollEnabled(boolean enabled) {
    mIsOverScrollEnabled = enabled;
}

public boolean isOverScrollEnabled() {
    return mIsOverScrollEnabled;
}

@Override
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY,
                               int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
    return super.overScrollBy(
            deltaX,
            deltaY,
            scrollX,
            scrollY,
            scrollRangeX,
            scrollRangeY,
            mIsOverScrollEnabled ? maxOverScrollX : 0,
            mIsOverScrollEnabled ? maxOverScrollY : 0,
            isTouchEvent);
}


Заключение


Я не знаю, использовала ли команда приложения Play Music поведение, основанное на моей статье. Но они блестяще использовали эту технику, чтобы отполировать и подчеркнуть пользовательский интерфейс. Очевидно, что это замечательный паттерн для тех случаев, когда вам нужно реализовать экран, содержание которого говорит само за себя и является более важным, чем содержание ActionBar.
Tags:
Hubs:
Total votes 30: ↑26 and ↓4+22
Comments6

Articles