Пользователь
0,0
рейтинг
5 декабря 2013 в 15:08

Разработка → Библиотека Header2ActionBar для Android

Библиотека Header2ActionBar для Android



(демо для привлечения внимания)


Вы, наверное, уже видели похожее в приложениях от Google (Play Музыка, Google Пресса) и, возможно, каких-либо других. Для этих целей уже довольно давно существует библиотека от ManuelPeinadoFadingActionBar, которая прекрасно выполняет свою задачу, но к сожалению, имеет два «фатальных» недостатка.

Второй из них описан как известная проблема:
Known Issues

There is an important issue with the library and ListViews. More specifically, things don't work quite right when the activity is re-created due to a configuration change. So, unless you handle configuration changes yourself (or your activity is portrait/landscape only), I strongly suggest you stick to having your content in a ScrollView until a solution to this issue is found.


Стараясь исправить этот недостаток, я решил написать свою реализацию, тем самым устранив и оба недостатка :)



Библиотека состоит из трёх файлов:
FadingActionBarActivity.java
/**
 * Created by AChep@xda <artemchep@gmail.com>
 */
public class FadingActionBarActivity extends Activity {

    private static final String TAG = "FadingActionBarActivity";

    private int mAlpha = 255;
    private Drawable mDrawable;

    private boolean isAlphaLocked;

    public void setActionBarBackgroundDrawable(Drawable drawable) {
        getActionBar().setBackgroundDrawable(drawable);
        mDrawable = drawable;

        if (mAlpha == 255) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
                mAlpha = drawable.getAlpha();
        } else {
            setActionBarAlpha(mAlpha);
        }
    }

    /**
     * An {@link android.app.ActionBar} background drawable.
     *
     * @see #setActionBarBackgroundDrawable(android.graphics.drawable.Drawable)
     * @see #setActionBarAlpha(int)
     */
    public Drawable getActionBarBackgroundDrawable() {
        return mDrawable;
    }

    /**
     * Please use this method for global changes only!
     * Otherwise, please, use {@link android.graphics.drawable.Drawable#setAlpha(int)}
     * to {@link #getActionBarBackgroundDrawable()} directly.
     *
     * @param alpha a value from 0 to 255
     * @see #getActionBarBackgroundDrawable()
     * @see #getActionBarAlpha()
     */
    public void setActionBarAlpha(int alpha) {
        if (mDrawable == null) {
            Log.w(TAG, "Set action bar background before setting alpha!");
            return;
        }
        if (!isAlphaLocked) mDrawable.setAlpha(alpha);
        mAlpha = alpha;
    }

    public int getActionBarAlpha() {
        return mAlpha;
    }

    public void setActionBarAlphaLocked(boolean isLocked) {
        isAlphaLocked = isLocked;
    }

}
HeaderFragment .java
/**
 * Little header fragment.
 * <p>
 * Created by AChep@xda <artemchep@gmail.com>
 * </p>
 */
public class HeaderFragment extends Fragment {

    private static final String TAG = "HeaderFragment";

    private FrameLayout mRoot;
    private View mContentOverlay;

    private View mHeader;
    private int mHeaderHeight;
    private int mCurrentHeaderHeight;
    private int mCurrentHeaderTranslateY;

    private Space mFakeHeader;
    private boolean mListViewEmpty;

    private OnHeaderScrollChangeListener mOnHeaderScrollChangeListener;

    public interface OnHeaderScrollChangeListener {
        public void onHeaderScrollChanged(float progress, int height, int scroll);
    }

    public void setOnHeaderScrollChangeListener(OnHeaderScrollChangeListener listener) {
        mOnHeaderScrollChangeListener = listener;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final Activity activity = getActivity();

        mHeader = inflater.inflate(getHeaderResource(), container, false);
        mHeaderHeight = mHeader.getLayoutParams().height;
        mCurrentHeaderHeight = mHeaderHeight;
        mCurrentHeaderTranslateY = 0;
        onPrepareHeaderView(mHeader);

        // Perform fake header view.
        mFakeHeader = new Space(activity);
        mFakeHeader.setLayoutParams(new ListView.LayoutParams(
                0, mHeaderHeight));

        View content = inflater.inflate(getContentResource(), container, false);
        assert content != null;
        if (content instanceof ListView) {
            final ListView listView = (ListView) content;

            mListViewEmpty = true;
            listView.addHeaderView(mFakeHeader);
            onPrepareContentListView(listView);
            listView.setOnScrollListener(new AbsListView.OnScrollListener() {

                @Override
                public void onScrollStateChanged(AbsListView absListView, int i) { /* unused */ }

                @Override
                public void onScroll(AbsListView absListView, int i, int i2, int i3) {
                    if (mListViewEmpty) { // poor poor listview :(
                        updateHeaderScroll(0);
                    } else {
                        final View child = absListView.getChildAt(0);
                        if (child == mFakeHeader) {
                            updateHeaderScroll(child.getTop());
                        } else {
                            updateHeaderScroll(-mHeaderHeight);
                        }
                    }
                }
            });
        } else {
            onPrepareContentView(content);

            // Merge fake header view and content view
            final LinearLayout ll = new LinearLayout(activity);
            ll.setLayoutParams(new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT));
            ll.setOrientation(LinearLayout.VERTICAL);
            ll.addView(mFakeHeader);
            ll.addView(content);

            final NotifyingScrollView scrollView = new NotifyingScrollView(activity);
            scrollView.addView(ll);
            scrollView.setOnScrollChangedListener(new NotifyingScrollView.OnScrollChangedListener() {
                @Override
                public void onScrollChanged(ScrollView who, int l, int t, int oldl, int oldt) {
                    updateHeaderScroll(-t);
                }
            });
            content = scrollView;
        }

        mRoot = new FrameLayout(activity);
        mRoot.addView(content, new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT));
        mRoot.addView(mHeader);

        // Overlay view always shows at the top of content.
        mContentOverlay = onCreateContentOverlayView();
        if (mContentOverlay != null) {
            mRoot.addView(mContentOverlay, new FrameLayout.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT));
        }

        // Initial notify
        notifyOnHeaderScrollChangeListener(0, mHeaderHeight, 0);

        return mRoot;
    }

    private void updateHeaderScroll(int scrollTo) {
        scrollTo = scrollTo > 0 ? 0 : scrollTo < -mHeaderHeight ? -mHeaderHeight : scrollTo;

        final boolean allowChangeHeight = isHeaderHeightFloating();
        final int height = mHeaderHeight + scrollTo / 2;
        final int transY = allowChangeHeight ? scrollTo / 2 : scrollTo;

        if (height != mCurrentHeaderHeight && allowChangeHeight) {
            final ViewGroup.LayoutParams lp = mHeader.getLayoutParams();
            lp.height = height;
            mHeader.setLayoutParams(lp);
            mCurrentHeaderHeight = height;
        }
        if (transY != mCurrentHeaderTranslateY) {
            mHeader.setTranslationY(transY);
            mCurrentHeaderTranslateY = transY;

            if (mContentOverlay != null) {
                final ViewGroup.LayoutParams lp = mContentOverlay.getLayoutParams();
                final int delta = mHeaderHeight + scrollTo;
                lp.height = mRoot.getHeight() - delta;
                mContentOverlay.setLayoutParams(lp);
                mContentOverlay.setTranslationY(delta);
            }

            notifyOnHeaderScrollChangeListener((float) -scrollTo / mHeaderHeight,
                    mHeaderHeight, -scrollTo);
        }
    }

    private void notifyOnHeaderScrollChangeListener(float progress, int height, int scroll) {
        if (mOnHeaderScrollChangeListener != null) {
            // Notify upper fragment to update ActionBar's alpha or whatever.
            mOnHeaderScrollChangeListener.onHeaderScrollChanged(progress, height, scroll);
        }
    }

    /**
     * If true, header's height might be changed on scroll.
     * <p>Note: It takes a lot of calculations to measure the header all the time.</p>
     */
    public boolean isHeaderHeightFloating() {
        return false;
    }

    /**
     * Int reference to header's resource.
     *
     * @see #onPrepareHeaderView(android.view.View)
     * @see #getContentResource()
     */
    public int getHeaderResource() {
        return 0;
    }

    /**
     * This is the place for setting up the header.
     *
     * @param view inflated header view.
     * @see #getHeaderResource()
     */
    public void onPrepareHeaderView(View view) { /* for my child */ }

    /**
     * Int reference to content's resource.
     * <p>
     * <b>Attention</b>: Parent view must be {@link android.widget.ListView ListView}
     * or something else which will work inside of {@link android.widget.ScrollView ScrollView}.
     * Otherwise it <b>WON'T</b> work.
     * </p>
     *
     * @see #getHeaderResource()
     * @see #onPrepareContentListView(ListView)
     */
    public int getContentResource() {
        return 0;
    }

    /**
     * Called if the content's parent is a {@link android.widget.ListView ListView}.
     *
     * @see #getContentResource()
     * @see #setListViewAdapter(android.widget.ListView, android.widget.ListAdapter)
     */
    public void onPrepareContentListView(ListView listView) { /* for my child */ }

    public void setListViewAdapter(ListView listView, ListAdapter adapter) {
        mListViewEmpty = adapter == null;
        listView.removeHeaderView(mFakeHeader);
        listView.addHeaderView(mFakeHeader);
        listView.setAdapter(adapter);
    }

    /**
     * Called if the content's parent is NOT a {@link android.widget.ListView ListView}.
     *
     * @see #getContentResource()
     */
    public void onPrepareContentView(View view) { /* for my child */ }

    public View onCreateContentOverlayView() {
        return null;
    }

}
NotifyingScrollView .java
/**
 * @author Cyril Mottier with modifications from Manuel Peinado
 */
public class NotifyingScrollView extends ScrollView {
    // Edge-effects don't mix well with the translucent action bar in Android 2.X
    private boolean mDisableEdgeEffects = true;

    /**
     * @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;
    }

    @Override
    protected float getTopFadingEdgeStrength() {
        // http://stackoverflow.com/a/6894270/244576
        if (mDisableEdgeEffects && Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            return 0.0f;
        }
        return super.getTopFadingEdgeStrength();
    }

    @Override
    protected float getBottomFadingEdgeStrength() {
        // http://stackoverflow.com/a/6894270/244576
        if (mDisableEdgeEffects && Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            return 0.0f;
        }
        return super.getBottomFadingEdgeStrength();
    }
}


и лежит на GitHub'е как проект библиотеки созданной в Android Studio.

Использование


HeaderFragment и FadingActionBarActivity наследуются от нативных собратьев, так что пока Android < 4.0 не поддерживается из коробки.


Наше приложение будет подобием демо на скриншоте сверху. Итак, пример Activity:

public class MainActivity extends FadingActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Инициализация фона ActionBar'a  
        setActionBarBackgroundDrawable(getResources().getDrawable(R.drawable.actionbar_bg));

        FragmentManager fragmentManager = getFragmentManager();
        fragmentManager.beginTransaction()
                .replace(R.id.container,  new TestHeaderFragment()
                ).commit();
    }
}

public class TestHeaderFragment extends HeaderFragment {

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        // Меняем прозрачность ActionBar'a во время скроллинга
        setOnHeaderScrollChangeListener(new OnHeaderScrollChangeListener() {
            @Override
            public void onHeaderScrollChanged(float progress, int height, int scroll) {
                height -= getActivity().getActionBar().getHeight();
                progress = (float) scroll / height;
                if (progress > 1f) progress = 1f;
                ((FadingActionBarActivity) getActivity()).setActionBarAlpha((int) (255 * progress));
            }
        });
    }

    @Override
    public int getHeaderResource() {
        return R.layout.header;
    }

    @Override
    public void onPrepareHeaderView(View view) {
        super.onPrepareHeaderView(view);
        // Заполняем view контентом
    }

    @Override
    public int getContentResource() {
        return R.layout.content;
    }

    @Override
    public void onPrepareContentListView(ListView listView) {
        super.onPrepareContentListView(listView);
        // Заполняем view контентом
        setListViewAdapter(listView, new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.title, new String[]{"Android", "Android", "Android", "Android", "Android", "Android", "Android", "Android", "Android", "Android", "Android"}));
    }


Так-же в стиль Activity необходимо добавить флаг:
<item name="android:windowActionBarOverlay">true</item>

, что бы разрешить контенту находиться под ActionBar'ом


Я заранее закрываю свое лицо руками и прошу прощения за свой код и английский. :(
@AChep
карма
13,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (24)

  • –7
    зачем?
    • +4
      Мне нужна была нормальная работа с ListView. Исправить эту ошибку я не смог, так-как не совсем точно понимал, как это вообще работает. У библиотек разные реализации — я наследую Activity и Fragment, а ManuelPeinado написал «хелпер» который это реализует.
      • –9
        Это выглядит и работает как «странная непонятная хрень». Я искренне не понимаю зачем плодить бестолковые вещи. Потому что можем?
        • +7
          Спросите у Google зачем они такую штуку «вставляют» в свои приложения. Мне этот элемент очень нравится, и я уверен что не одинок.
          • –7
            Google не рекомендует эту хрень как UI- паттерн и не вставляет ее в андроид. Это фан, не больше. Cyril Mottier — такие штуки повторяет — потому что он на этом деньги зарабатывает. А зачем это Вам я так и не понял. Вы теперь все свои приложения будете делать так же как делает Гугл? Свое не получается придумать?
            • +5
              А я считаю это красивым элементом интерфейса, которой при правильном использовании поможет «оживить» оболочку вашего приложения. Если вам это не нравится — не используйте такие приложения. Откуда столько негатива? Потому что можем?
            • +2
              А вы всё делаете порекомендации, нельзя отходить на лево и на право ни на шаг? Фантазировать и импровизировать нельзя? Многие современные элементы интерфейса на мобильных устройствах были рождены вот такими эксперементами: потянуть, чтоб обновить, инерционная прокрутка, боковое выезжающее меню, стэк окон…
              Если вы против этого — не нужно навязывать свое отношение другим, пусть человек эксперементирует. Тем более что получилось достаточно удобно и красиво. Значит у автора есть вкус и понимание. Реализация тоже неплоха, значит у автора еще есть и умение.
              А ваша негативная реакиция показывает, что вам этого не понять. Мне не понять вас. Но я же не накидываюсь на вас, а просто разжевываю.
              Вобщем «фе» вам моё не за то, что вы не поддерживаете подход автора, а за то что так бурно накинулись на человека, который мыслит не как вы.
              • –7
                Какая нахрен фантазия? Какая в жопу импровизация? Эта фигня 1 в 1 содранна под кальку с одного из гугловских приложений. Очнитесь. Весь androidviews.net засран этой фигней. Зачем??? Остановитесь, рогатые твари! Придумайте уже хоть что нибудь Интересное. Полезное блин. Новое!
                • +2
                  Затем что бы самому не писать «эту фигню» если захочешь использовать?
                • +2
                  так и придумайте, чего кричать и оскорблять тут? Сделайте что-нибудь хорошее, а то я посмотрел ваши посты, а вы там пишете про видеочаты на чужих технологиях, текстовые редактор в вебе и сотый клиент для хабра и оскорбляете всех разбрызгивая своей слюной по всей ветке коментариев.
                  • –3
                    Друг. А ты наверно видеочаты на каких то своих языках пишешь? Текстовые редакторы для собственных браузеров? Что за бред? И да, я сделал достаточно хорошего. Успокойся уже.
                    • 0
                      я вам не друг, это меня радует.
                      Успокаивать меня не нужно — я и так спокоен.
                      Меня возмущает ваше хамство, а что вы там делаете — лично ваше дело.
                      И кстати можете не верить, но я сделал в своей жизни layout Engine, и браузер на его основе, но текстового редактора для него не делал, практически все остальные работали.
                      Про собственные языки я ничего не писал — ваши фантазии.
                      • –4
                        Интересно, сколько человек пользуются твоим layout enginом? А какая была твоя дипломная работа? Расскажи как ты крут, мне правда интересно.
                        • +3
                          около миллиона пользуются, насколько я знаю. Может больше, я больше к тому проекту не причастен.
                          Дипломная работа была другая. Удовлетворил любопытство?
                          • –4
                            Нет. Теперь мне интересно как называется твой браузер. Я тебе уже говорил что ты нереально крут? Продолжай рассказывать, это крайне интересно! Я даже спать не лягу — читать буду. Это был браузер для N900 да ведь? Ну расскажи же, не томи! Лучше отдельным постом. Назови его, например, Житие Мое.
                            • +2
                              ты реально больной :-), N900 — это хобби было.
          • +4
            Полностью поддерживаю, элемент очень красивый и удобный. Автору «респект» за труды.
  • 0
    Спасибо! Удобная вещь.
  • 0
    А если экшен бар будет «заспличен», то есть часть его будет внизу, будет работать? )
  • +2
    Лови пулл реквест с совместимостью со старыми андроидами)
    • 0
      Спасибо, взял в новую ветку)
  • 0
    У вас получилось исправить проблемы, которые возникли у ManuelPeinado.
    Но в вашем случае обязательно надо наследоваться от Activity и Fragment, что является плохим тоном в разработке библиотек под android.
    • 0
      Это да. Убрал зависимость от Activity.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.