Компания
196,42
рейтинг
28 ноября 2014 в 16:04

Разработка → Внедряем материальный дизайн

Настало время переходить на Lollipop, друзья. Как бы смешно это не звучало.

image

Буквально вчера мы в Surfingbird обновили дизайн приложения и сегодня, по свежим следам, хотелось бы поделиться впечатлениями от перехода на material design.


Подготовительный этап.

Чтобы минимизировать количество проблем, лучше обновить все)

  • Устанавливаем образ lollipop на свой нексус телефон
  • Обновляем Java до 7 версии, если еще нет
  • Обновляем IDE, мы используем Intellij Idea
  • Обновляем SDK, не забудьте обновить Tools, Platform-tools, Build-tools, Sdk и Support library


Внедряем RecyclerView

RecyclerView это новый ViewGroup компонент, который пришел на замену List/GridView. Но он не является их потомком, скорее это альтернативная ветвь эволюции. С одной стороны, это гораздо более гибкий и более эффективно работающий компонент, с другой — в нем из коробки отсутствуют, либо делаются по другому некоторые вещи, к которым мы привыкли в List/GridView (разделители, быстрый скролл, селекторы, хидеры и т.п.).
Во-первых, по субъективным ощущениям, скроллинг стал более плавным, чем при использовании listview+viewholder, во-вторых, появилось множество прекрасных штук, так что игра несомненно стоит свеч.

Перейти на этот компонент очень просто. Закидываем в библиотеки соответствующий sdk ▸ extras ▸ android ▸ support ▸ v7 ▸ recyclerview ▸ libs▸ android-support-v7-recyclerview.jar/подключаем в богомерзком gradle или чем вы пользуетесь.

1. Обновляем адаптер, если вы уже использовали view-holder паттерн, то все привычно

Заменяем BaseAdapter(или что там у вас было) на RecyclerView.Adapter<AdapterMain.ViewHolder>

В onCreateViewHolder — парсим layout

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View view = layoutInflater.inflate(R.layout.main_adapter_griditem, null);
        return new ViewHolder(view);
    }


где, собственно ViewHolder – привычная заглушка

    class ViewHolder extends RecyclerView.ViewHolder{
        private ImageView stgvImageView;

        public ViewHolder(View holderView) {
            super(holderView);
            stgvImageView = (ImageView) holderView.findViewById(R.id.stgvImageView);
        }
    }


и переносим логику наполнения view из getView в onBindViewHolder (обращаясь к холдеру — holder.stgvImageView и т.п.)

Удаляем ставшие ненужными методы типа getItem

2. Заменяем ListView на RecyclerView

    public RecyclerView gridView;//здесь был Grid/ListView
    public AdapterMain adapterMain;
    public ArrayList<Site> rows;
    //Это способ отображения recycleview. Кроме сетки с столбцами переменной высоты есть более канонические
    //GridLayoutManager (Grid) и LinearLayoutManager (List)
    public StaggeredGridLayoutManager mLayoutManager;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        aq = new AQuery(getActivity());
        //Не пугайтесь это просто контейнер
        final LinearLayout linearLayout = new LinearLayout(getActivity());
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        linearLayout.setGravity(Gravity.CENTER);
        gridView = new RecyclerView(getActivity());
        gridView.setHasFixedSize(true);
        mLayoutManager = new StaggeredGridLayoutManager(UtilsScreen.getDisplayColumns(getActivity()),StaggeredGridLayoutManager.VERTICAL);
        //можно задать горизонтальную ориентацию. Будет свежо и необычно. Наверное
        gridView.setLayoutManager(mLayoutManager);
        gridView.setItemAnimator(new DefaultItemAnimator());
        //Это новый метод для задания divider
        //gridView.addItemDecoration(new DividerItemDecoration(getActivity()));
        //Этих методов больше нет
        //gridView.setSmoothScrollbarEnabled(true);
        //gridView.setDivider(new ColorDrawable(this.getResources().getColor(R.color.gray_divider)));
        //gridView.setDividerHeight(UtilsScreen.dpToPx(8));
        rows = new ArrayList<Site>();
        linearLayout.addView(gridView);
        return linearLayout;
    }


3. Продолжаем разговор.

Работа с адаптером практически не изменилась.

    @Override
    public void onViewCreated(View view,Bundle savedInstanceState) {
        super.onViewCreated(view,savedInstanceState);
        adapterMain = new AdapterMain(getActivity(),rows);
        gridView.setAdapter(adapterMain);
        gridView.setOnScrollListener(onScroll);
    }


Стал ненужным метод отключения адаптера на момент изменения (DataSetInvalidated), нотификация об изменении осталась без изменения

            adapterMain.notifyDataSetChanged();
            if (page == 1) gridView.scrollToPosition(0);//точно не помню как раньше назывался этот метод


Изменился метод вычисления последнего видимого элемента (для автоматической подгрузки следующей порции). Предполагаю, что эту логику лучше перенести в адаптер, но, если очень некогда, то можно так, например:

    gridView.setOnScrollListener(onScroll);
    //---
    private RecyclerView.OnScrollListener onScroll = new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            int[] visibleItems = ((StaggeredGridLayoutManager) gridView.getLayoutManager()).findLastVisibleItemPositions(null);
            int lastitem=0;
            for (int i:visibleItems) {
                lastitem = Math.max(lastitem,i);
            }
            if (lastitem>0 && lastitem>adapterMain.data.size()-5 && !isRunning) {
                if (!internetIsOver) {
                    refresh();
                }
            }
        }
    };


Вообще, скролинг стал более низкоуровневым, теперь можно прямо в этом методе получать информацию куда и насколько проскролено (простите мой английский)

На этом месте у вас все должно заработать. Если, например, нужно добавить разделители, то их можно добавить перекрыв класс DividerItemDecoration, например так: (вертикальные разделители)
(Ахтунг, копипаста сами знаете с какого сайта)

    public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };

    private Drawable mDivider;
    private int offset = 0;

    public DividerItemDecoration(Context context) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        offset = UtilsScreen.dpToPx(16);
        a.recycle();
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent) {
        drawVertical(c, parent);
    }

    public void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + offset;//mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
        outRect.set(0, 0, 0, offset);//mDivider.getIntrinsicHeight());
    }
    }


Но не спешите с этим! Потому что появились прекрасные Карточки!

Внедряем CardView

Помню, когда я был еще совсем молодым android-разработчиком, вышел пинтерест и все офигели. Мы часами разглядывали, как они реализовали карточки переменной высоты, плавающие кнопки (или это было в Path?), не суть важно. Сейчас можно получить неплохо выглядящие карточки (в том числе, переменной высоты и прямо как в пинтерест) буквально в пару строк кода.

Подключаем cardview как library project/прописываем магическую строку в систему сборки, закидываем jar, не забыв обновить версию саппорт лайбрари.

По сути, карточки — это фрейм вокруг вашего лейаута с тенюшками и скругляшками, поэтому просто обрамляем ими ваш лэйаут:

    <android.support.v7.widget.CardView
            xmlns:card_view="http://schemas.android.com/apk/res-auto"
            android:id="@+id/card_view"
            android:layout_gravity="center"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            card_view:contentPadding="8dp"
            card_view:cardBackgroundColor="@color/primary_bgr"
            card_view:cardUseCompatPadding="true"
            card_view:cardCornerRadius="4dp">
            <RelativeLayout 
                android:id="@+id/articleLayout"
                android:background="@color/primary_bgr"
              android:layout_width="match_parent"
              android:layout_height="wrap_content">
              //---


Готово, Милорд!

Теперь, допустим, для планшетной версии задаем отображение в две колонки, а для телефонов в одну:
(Ахтунг, копипаста сами знаете с какого сайта)

    public static boolean isTablet(Context context) {
        boolean xlarge = ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == 4);
        boolean large = ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_LARGE);
        return (xlarge || large);
    }

    public static int getDisplayColumns(Activity activity) {
        int columnCount = 1;
        if (isTablet(activity)) {
            columnCount = 2;
        }
        return columnCount;
    }


И задаем для разных устройств разный формат отображения:

     mLayoutManager = new StaggeredGridLayoutManager(UtilsScreen.getDisplayColumns(getActivity()),StaggeredGridLayoutManager.VERTICAL);


Должно получиться примерно так:


Некоторые нюансы:

  • После того, как мы выложили приложение в стор, на некоторых устройствах (почему-то на нексусах) и почему-то в том числе на 4.4.4 – приложение странным образом начало падать в районе саппорт лайбрари (причем на наших телефонах (включая нексусы) все работало). Пришлось отключить proguard, это помогло но осадок остался.
  • Нам не очень понравился цвет шрифта в дефолтной светлой теме. Он очееень нежен, учитывая то, что на всех андроид устройствах цветопередача нарушена разная, поэтму мы решили перекрыть цвет шрифта на чуть более темный.
  • Отключить тень у акшенбара теперь можно, например, так: getSupportActionBar().setElevation(0);
  • Приложение не будет работать на бете лоллипоп так же, как не работают на ней и все остальные приложения в лоллипоп дизайне (gmail, пресса)
  • Иконки акшенбара стали меньше. Мы просто перенесли их в папку (xxhdpi)
  • Мы пока решили забить на анимации. Перед глазами гугл-пресса и все вроде дико красиво крутится/вертится/плавает/мигает, но мы еще не готовы к такой решительной анимации.


Результат

Глаз разработчика «замылен», сложно сказать получилось хорошо или так себе. Я почему-то ожидал большего, если честно. Динамически падающих тенюшек при скролинге, например, больше магии. А в целом, все получилось чуть свежее. Хотя, конечно, мы еще не до конца ололлипопились. Посмотреть результат можно в маркете.

Я наверняка что-то забыл. Делитесь нюансами, рецептами и советами перехода на лоллипоп в комментариях. Тема актуальная, всем нам будет полезно и интересно.
Автор: @recompileme
Surfingbird
рейтинг 196,42

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

  • +4
    За что? За что, вы так поиздевались над кнопкой меню?
    • +1
      Кстати почему вы решили использовать именно AQuery? почему бы не использовать Retrofit + OkHttp + Picasso?
      • 0
        Учитывая ещё то, что последний релиз AQuery был год назад. А тот же Retrofit обновляется по сей день.
        • 0
          Не в частоте релизов счастье) Aquery, обладает бОльшим набором функционала, при меньшем размере и бОльшем удобстве. Я уже устал её рекламировать.
    • +2
      Да, мы немного наколхозили с ней, в ближайшем релизе поменяем на стандартную. Спасибо что заметили.
      • +1
        и ButterKnife в помощь.
      • +1
        Не используйте RelativeLayout, вместо него старайтесь писать CustomView.
        • –1
          чем вам не угодил RelativeLayout?
          • +1
            Слишком тяжелый вычисления, Chris Banes(или Roman Nurik) в одной из своих презентация просил крайне осторожно и редко их использовать, а лучше вообще создавать свои View.
          • 0
            Я так полагаю Озик хотел сказать что RelativeLayout штука коварная и можете наткнуться на проблемы с производительностью с ней. Как говорится: лучше уж пару вложенных LinearLayout, чем один RelativeLayout.
            • +1
              в официальной документации как раз есть разобранный контраргумент на ваш совет
              developer.android.com/training/improving-layouts/optimizing-layout.html

              тут же главное ко всему подходить с умом и понимать, что потом будет внутри происходить. и тогда можно написать RelativeLayout оптимальнее, чем получилось бы с другими стандартными ViewGroup.

              для каждой конкретной ситуации можно написать CustomView, который будет работать оптимальнее, чем тот же вариант на RelativeLayout. если посмотреть на элементы списков в приложении Gmail в режиме отображения границ лэйаутов, то можно увидеть, что это как раз один CustomView. но этот вариант не всегда выиграет по соотношению производительность/затраченное время программиста.
              • +1
                Поймали меня за слово: )
                Эта статья что вы привели слишком поверхностная. Там написано что, производительность может быть улучшена из-за выравнивания (flatten) иерархии и как предложение использовать RelativeLayout. Но это далеко не всегда так, из-за того, что RelativeLayout меряет (measure) своих детей в несколько проходов, применяя различные ограничения.

                Понятное дело, что нужно сначала мерить, убедиться что есть проблема и затем уже оптимизировать.
                • 0
                  Я не собирался ни на чем ловить)
                  LinearLayout тоже сначала меряет (measure) своих детей, чтобы узнать сколько они бы «хотели» занять, а потом с уже примененной логикой весов. Именно из-за этого в приведенной статье Relative и выиграл, потому что был один источник сложных вычислений RelativeLayout вместо двух LinearLayout.

                  Ну и с помощью RelativeLayout можно избавиться от проблемы излишних вложенностей (nested views) из-за которой на андроидах 2.x можно было наткнутся и на StackOverflow при отрисовке на довольно сложном экране
  • +2
    Рано леденец выпустили. Проблем достаточно много. Один расход аккумулятора и калькутор чего стоит.
    • 0
      Зато сейчас, благодаря фидбеку комьюнити, они всё это исправят и выпустят обновление.
      • 0
        Не знаю, но мой телефон стал жить на 1-1.5 часа меньше, чем жил на 4.4.4
        На xda довольно активно обсуждают не только тему аккумуляторов, но и систему в целом.
      • 0
        Подождите, сейчас то уже поздно — 5.0 ушел в прод. Было бы здорово если они это исправили во время L Developer Preview.
        • 0
          Обновление для nexus 7 приостановили, видимо, правят что-то.
    • 0
      А что не так с аккумулятором? На моем нексусе 9 нет проблем, держится как второй айпад. Только заряжается от штатной зарядки вдвое дольше айпада.
      • +1
        Сейчас мой нексус держит заряд на 1-1,2 часа меньше чем на 4.4.4 и это не только у меня.
        При это большая проблема с вейклоками.
  • –1
    Тот же Mark Murphy (автор The Busy Coder's Guide to Android Development) сказал, что лучше на 4.0+ версиях оставить Holo. А material design только для новых версий делать.

    Но вы, как я понимаю, используете material design для всех версий Android?
    • 0
      Да, всё верно. Нам очень понравилось как приложение смотрится на версиях 4.x и мы решили подарить всем нашим пользователям немного нового юзер экспириенса.
      • –5
        А гайдлайны 4.0+ для прикола?)
        • +11
          Если бы в гугле не хотели видеть материальный дизайн на 4.х устройствах, зачем было делать бэкпорт стилей и новых вьюх?
          По-моему, наоборот хорошо, что не надо менять устройство, которое застряло на 4.х, чтобы наслаждаться новым дизайном в приложениях.
    • +1
      Я не согласен с Марком в этом плане.
      Стоит хотя бы учесть то, что большинство трендмейкеров, таких как Google, Facebook и иже с ними уже переходят на Material во все поля. Таким образом на андроиде вы практически всегда будете инконсистенты с экосистемой, вопрос с какой стороны.
      Так же, стоит учесть AppCompat, которая по умолчанию подкрашивает вашу аппу в Material.

      Мне кажется Марк своим мнением просто разделил людей на два лагеря.
      • 0
        А я с ним согласен. На своём Android 2.3 хочу видеть все приложения в одном стиле. На своих 4.0-4.4 Android'ах я хочу видеть Holo. Если пара приложений сделают Material, то на фоне всех остальных приложений они будут выделяться, ибо многие приложения не обновились до Material.

        Так что, не всё так однозначно, всё же.

        • +1
          Это все справедливо, как Вы говорите. Но проблема в том, что Google с Вами не согласен и первым начинает вводить свой Material и говорить что это хорошо. Когда эти «остальные приложения» — это вся линейка Google апп, то с этим уже труднее спорить, не так ли?
  • 0
    Предлагаю на будущее некоторые термины оставлять на английском :)
    А вообще статья хорошая. Но я пока бы воздержался от перехода, ибо в саппорте очень много багов еще, да и Build Tools новый на студии тоже порой ведет себя странновато.
  • 0
    А что за ViewPagerIndicator Вы используете? Или это стандартный?
    • 0
      Наверное, это PagerSlidingTabStrip
    • 0
      Это гугловский, версия для 7+ апи
  • –11
    Надо запасаться достаточно старыми телефонами, чтобы на них не обновились приложения до такого дизайна. Что за повсеместная мода(

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

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