Библиотека 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'ом


    Я заранее закрываю свое лицо руками и прошу прощения за свой код и английский. :(
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 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.

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