Leanback UI简介

一、 常见的UI

1. BrowserFragment

image.png

整体内容被对齐在一个网格布局里。左侧的每一个标题header,都有右侧对应的一个内容行row,他们是一一对应的。header+content row由一个类 ListRow来表示。页面的整体其实是ListRow的集合
整体是一个大的ArrayObjectAdapter 由一系列的ListRow来填充。view的呈现方式由ListRowPresenter来定义。
一个ListRow 由HeaderItem 和一个小的ArrayObjectAdapter组成,这个一行中的ArrayObjectAdapter中放置我们定义的view,呈现方式由CardPresenter来定义。
典型的代码如下:

List list = MovieList.setupMovies();
       mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
       CardPresenter cardPresenter = new CardPresenter();
       for (int i = 0; i < NUM_ROWS; i++) {
           if (i != 0) {
               Collections.shuffle(list);
           }
           ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(cardPresenter);
           for (int j = 0; j < NUM_COLS; j++) {
               listRowAdapter.add(list.get(j % 5));
           }
           HeaderItem header = new HeaderItem(i, MovieList.MOVIE_CATEGORY[i]);
           mRowsAdapter.add(new ListRow(header, listRowAdapter));
       }
        setAdapter(mRowsAdapter);

基本关系:

ArrayObjectAdapter (RowsAdapter) ← A set of ListRow
ListRow = HeaderItem + ArrayObjectAdapter (RowAdapter)
ArrayObjectAdapter (RowAdapter) ← A set of Object (CardInfo/Item)

一般来说,谷歌的leanback 是(如上图)左边的菜单对应后面的一行。但是其实实际在开发中应用中,是左边的一个菜单对应右边一整个页面。第一种情况基本上上面已经说了。下面来说说第二种情况:

public class MainFragment extends BaseBrowseFragment {
    private static final String TAG = "MainFragment";
    private static final long HEADER_ID_DISCOVERY = 0;
    private static final long HEADER_ID_TV_SHOW = 1;
    private static final long HEADER_ID_FAV = 2;
    private static final long HEADER_ID_MEDIA = 3;
    private static final long HEADER_ID_SETTINGS = 4;  // 这些都是对应的左边菜单的id

    @Override
    protected int getHeaderTitleArrayRes() {
        return R.array.main_header_title_array;
    }

    @Override
    protected int getHeaderIconArrayRes() {
        return R.array.main_header_icon;
    }

    @Override
    protected FragmentFactory getBrowseFragmentFactory() {
        return new PageRowFragmentFactory(mBackgroundManager);
    }

    protected void onEntranceTransitionEnd() {
        Log.d(TAG, "onEntranceTransitionEnd: ");
    }

    private static class PageRowFragmentFactory extends BrowseFragment.FragmentFactory {
        private final BackgroundManager mBackgroundManager;

        PageRowFragmentFactory(BackgroundManager backgroundManager) {
            this.mBackgroundManager = backgroundManager;
        }

        @Override
        public Fragment createFragment(Object rowObj) {
            Row row = (Row) rowObj;
            long id = row.getHeaderItem().getId();         //每当点击后就会显示出对应的fragment
            mBackgroundManager.setDrawable(null);
            if (id == HEADER_ID_TV_SHOW) {
                return new TvShowFragment();
            } else if (id == HEADER_ID_DISCOVERY) {
                return new DiscoveryFragment();
            } else if (id == HEADER_ID_MEDIA) {
                return new MediaFragment();
            }else if (id == HEADER_ID_FAV) {
                return new FavoriteFragment();
            }
            else if (id == HEADER_ID_SETTINGS) {
                return new SettingsFragment();
            }

            throw new IllegalArgumentException(String.format("Invalid row %s", rowObj));
        }
    }

}

在leanback 中,右边页面显示的种类往往不同,例如,有视频列表,图片列表,音乐列表。那么这些在leanback中都是怎么处理的呢?其实在那些列表中的每一个item都是一个Card。然后其实就是在recycleview 中设置不同类型的item。
通过如下的selector 去选择不同的card

public class CardPresenterSelector extends PresenterSelector {

    private final Context mContext;
    private final HashMap presenters = new HashMap();

    public CardPresenterSelector(Context context) {
        mContext = context;
    }

    @Override
    public Presenter getPresenter(Object item) {
        if (!(item instanceof Card)) throw new RuntimeException(
                String.format("The PresenterSelector only supports data items of type '%s'",
                        Card.class.getName()));
        Card card = (Card) item;
        Presenter presenter = presenters.get(card.getCardType());

        if (presenter == null) {
            switch (card.getCardType()) {
                case SINGLE_LINE:
                    presenter = new SingleLineCardPresenter(mContext);
                    break;
                case VIDEO_GRID:
                    presenter = new VideoCardViewPresenter(mContext, R.style.VideoGridCardTheme);
                    break;
                case MOVIE:
                    presenter = new CardPresenter();
                    break;
                case MOVIE_COMPLETE:
                    /**
                     * {@link com.smartdevice.multimediaplayer.utils.Constants.ITEM_TYPE_MUSIC}
                     * add for music which use in search and discovery model ,ordinal  =  0
                     */
                    presenter = new MusicCardPresenter(mContext, false);
                    break;
                case MUSIC_SMALL:
                    presenter = new MusicCardPresenter(mContext, true);
                    break;
                case MOVIE_BASE:
                case SQUARE_BIG:
                case ICON:
                    presenter = new GridItemPresenter(mContext);
                    break;
                case GRID_SQUARE:
                    presenter = new GridItemPresenter(mContext);
                    break;
                case MUSIC_ARTIST:
                    presenter = new ArtistsCirclePresenter(mContext);
                    break;
                case MUSIC_ALBUM:
                    presenter = new AlbumCardPresenter(mContext);
                    break;
                case CIRCLE_ICON:
                    presenter = new CircleIconPresenter(mContext);
                    break;
                case TV_SHOW_CURRENT:
                case TV_SHOW_UPCOMING:
                case TV_SHOW_RECOMMENDATION:
                case TV_SHOW_APP:
                case SPOTIFY_SEARCH:
                    presenter = new TVShowPresenter(mContext);
                    break;
                case POPULAR_MUSIC:
                    presenter = new PopularMusicPresenter(mContext);
                    break;
                case RECOMMEND_VIDEO:
                case CHILD_CHANNEL:
                    presenter = new RecommendVideoPresenter(mContext);
                    break;
                case FAVOITE_TVSHOW:
                    presenter = new FavoriteTVShowPresenter(mContext);
                    break;
                case DEVICE:
                    presenter = new DeviceItemPresenter(mContext);
                    break;
                case SETTINGS:
                    presenter = new SettingsItemPresenter(mContext);
                    break;
                case LOCALDEVICE:
                    presenter = new LocalDeviceItemPresenter(mContext);
                    break;

                case DLNA_DEVICE:
                    presenter = new DlnaDeviceItemPresenter(mContext);
                    break;
                case LOADING_ICON:
                    presenter = new LoadingCardPresenter(false);
                    break;
                case LOADING_ICON_ERROR:
                    presenter = new LoadingCardPresenter(true);
                    break;
                case YOUTUBE_VIDEO_ICON:
                    presenter = new DiscoveryCardViewPresent(mContext);
                    break;
                default:
                    presenter = new ImageCardViewPresenter(mContext);
                    break;
            }
        }
        presenters.put(card.getCardType(), presenter);
        return presenter;
    }

选择好card后,然后对应的card再去往里面填充数据,下面是一种card 类型。

public class MusicCardPresenter extends MusicAbstractCardPresenter {
    public static final String TAG = MusicCardPresenter.class.getSimpleName();

    public MusicCardPresenter(Context context, int cardThemeResId) {
        super(new ContextThemeWrapper(context, cardThemeResId));
    }

    public MusicCardPresenter(Context context, boolean isSmall) {
        this(context, isSmall ? R.style.MusicSmallCardStyle : R.style.MusicCardStyle);
    }

    @Override
    protected ImageCardView onCreateView() {
        ImageCardView imageCardView = new ImageCardView(getContext());
        imageCardView.setFocusable(true);
        imageCardView.setFocusableInTouchMode(true);
        imageCardView.setMainImageScaleType(ImageView.ScaleType.CENTER_CROP);
        return imageCardView;
    }

    @Override
    public void onBindViewHolder(Card card, ImageCardView cardView) {
        cardView.setTitleText(card.getTitle());
        if (TextUtils.isEmpty(card.getFilePath())) {//spotify music
            Glide.with(getContext())
                    .load(card.getImageUrl())
                    .into(cardView.getMainImageView());   这里使用Glide 框架来填充图片

        } else {//local music files
            mImageFetcher.loadImage(card.getFilePath(), cardView.getMainImageView());
        }

    }

    @Override
    public void onUnbindViewHolder(ImageCardView cardView) {
        super.onUnbindViewHolder(cardView);
        ImageWorker.cancelWork(cardView.getMainImageView());
        cardView.setBadgeImage(null);
        cardView.setMainImage(null);
    }

2. Card View

使用BaseCardView和它的子类显示与媒体项相关的数据。使用ImageCardView显示显示图片和标题。
创建一个Card Presenter
Presenter根据需求生成视图并将数据对象与之绑定。
如下:

@Override
public void onLoadFinished(Loader>> arg0,
                       HashMap> data) {

    mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
    CardPresenter cardPresenter = new CardPresenter();

    int i = 0;

    for (Map.Entry> entry : data.entrySet()) {
        ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(cardPresenter);
        List list = entry.getValue();

        for (int j = 0; j < list.size(); j++) {
            listRowAdapter.add(list.get(j));
        }
        HeaderItem header = new HeaderItem(i, entry.getKey(), null);
        i++;
        mRowsAdapter.add(new ListRow(header, listRowAdapter));
    }

    HeaderItem gridHeader = new HeaderItem(i, getString(R.string.more_samples),null);

    GridItemPresenter gridPresenter = new GridItemPresenter();
    ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(gridPresenter);
    gridRowAdapter.add(getString(R.string.grid_view));
    gridRowAdapter.add(getString(R.string.error_fragment));
    gridRowAdapter.add(getString(R.string.personal_settings));
    mRowsAdapter.add(new ListRow(gridHeader, gridRowAdapter));

    setAdapter(mRowsAdapter);
    updateRecommendations();
}

注:每一个presenter只能创建一种视图类型,如果有多种不同视图类型就需要创建多种presenter。
创建Presenter需要实现onCreatViewHolder()方法:

@Override
public class CardPresenter extends Presenter {

    private Context mContext;
    private static int CARD_WIDTH = 313;
    private static int CARD_HEIGHT = 176;
    private Drawable mDefaultCardImage;

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent) {
        mContext = parent.getContext();
        mDefaultCardImage = mContext.getResources().getDrawable(R.drawable.movie);

如果卡片选中,你可以做各种操作,默认放大:

...
ImageCardView cardView = new ImageCardView(mContext) {
    @Override
    public void setSelected(boolean selected) {
        int selected_background = mContext.getResources().getColor(R.color.detail_background);
        int default_background = mContext.getResources().getColor(R.color.default_background);
        int color = selected ? selected_background : default_background;
        findViewById(R.id.info_field).setBackgroundColor(color);
        super.setSelected(selected);
    }
};
...

为了实现遥控操作需要设置

setFocusable(true),setFocusableInTouchMode(true);
或者是在.xml里加入android:focusable="true"

3. Details Fragment

创建一个详情presenter
Leanback library提供了视频浏览框架,你可以使用presenter控制数据在屏幕上的显示,包括视频详情。这个框架为止提供了AbstractDetailsDescriptionPresenter,你需要实现onBindDescription(),将数据与视图绑定。如下:

public class DetailsDescriptionPresenterextends AbstractDetailsDescriptionPresenter {
    @Override
    protected void onBindDescription(ViewHolder viewHolder, Object itemData) {
        MyMediaItemDetails details = (MyMediaItemDetails) itemData;

        // itemData包含视频的详细信息
        //需要显示视频的详细信息
        // viewHolder.getTitle().setText(details.getShortTitle());

        // 使用静态数据测试:
        viewHolder.getTitle().setText(itemData.toString());
        viewHolder.getSubtitle().setText("2014   Drama   TV-14");
        viewHolder.getBody().setText("Lorem ipsum dolor sit amet, consectetur "
            + "adipisicing elit, sed do eiusmod tempor incididunt ut labore "
            + " et dolore magna aliqua. Ut enim ad minim veniam, quis "
            + "nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
            + "commodo consequat.");
    }
}

继承DetailsFragment
使用DetailsFragment来显示视频的详细信息,它提供额外的内容,比如:预览图片,关于视频的操作项(购买、播放、关注等)。您还可以提供额外的内容,如相关视频或演员的列表。如下:

public class MediaItemDetailsFragment extends DetailsFragment {

    private static final String TAG = "MediaItemDetailsFragment";
    private ArrayObjectAdapter mRowsAdapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "onCreate");
        super.onCreate(savedInstanceState);
        buildDetails();
    }

    private void buildDetails() {
        ClassPresenterSelector selector = new ClassPresenterSelector();

        // 将视频详细信息的presenter附加到rowPresenter上
        FullWidthDetailsOverviewRowPresenter rowPresenter =
            new FullWidthDetailsOverviewRowPresenter(
                new DetailsDescriptionPresenter());

        selector.addClassPresenter(DetailsOverviewRow.class, rowPresenter);
        selector.addClassPresenter(ListRow.class,
                new ListRowPresenter());
        mRowsAdapter = new ArrayObjectAdapter(selector);

        Resources res = getActivity().getResources();
        DetailsOverviewRow detailsOverview = new DetailsOverviewRow(
                "Media Item Details");

        // 给详情视图添加图片和操作
        detailsOverview.setImageDrawable(res.getDrawable(R.drawable.jelly_beans));
        detailsOverview.addAction(new Action(1, "Buy $9.99"));
        detailsOverview.addAction(new Action(2, "Rent $2.99"));
        mRowsAdapter.add(detailsOverview);


        // 添加相关项
        ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(
            new StringPresenter());
        listRowAdapter.add("Media Item 1");
        listRowAdapter.add("Media Item 2");
        listRowAdapter.add("Media Item 3");
        HeaderItem header = new HeaderItem(0, "Related Items", null);
        mRowsAdapter.add(new ListRow(header, listRowAdapter));
        setAdapter(mRowsAdapter);
    }
}

创建详情Activity
创建一个activity包含DetailsFragment来显示详情。

public class DetailsActivity extends Activity{

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

定义一个Listener监听每项的点击

public class BrowseMediaActivity extends Activity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        // create the media item rows
        buildRowsAdapter();

        // add a listener for selected items
        mBrowseFragment.OnItemViewClickedListener(
            new OnItemViewClickedListener() {
                @Override
                public void onItemClicked(Object item, Row row) {
                    System.out.println("Media Item clicked: " + item.toString());
                    Intent intent = new Intent(BrowseMediaActivity.this,
                        DetailsActivity.class);
                    // pass the item information
                    intent.getExtras().putLong("id", item.getId());
                    startActivity(intent);
                }
            });
    }
}

二、 MVP的构建模式

Leanback 提供了model-view-presenter mvp的方式来构建应用。

•   model 是由应用开发者来提供,leanback对于model的实现没有加额外的限制,任何对象都是可以的。
•   view 还是由原来的android.view包下的类来实现。
•   Presenter 是基于现在的Adapter的该  概念,并扩充为更具的灵活性和组合性。特别的是,绑定数据到view上的操作已经将adapter中分离出去,这部分逻辑由presenter去承担。

Presenter
Presenter class 是用来做数据和视图的桥梁的
每一行的视图展示,每一个卡片的视图展示都是由Presenter来定义。Presenter是一个抽象类,需要自己来继承该类。
需要实现下面的三个方法:

  1. onCreateViewHolder(ViewGroup parent);
  2. OnBindViewHolder(ViewHolder ViewHolder,Ojbect item);
  3. onUnBindViewHolder(Viewholder viewhlder);可以看到这些方法跟RecyclerView 的Apdater的实现方法很像,实际上这些方法就是借鉴了recyclerview的实现。
    不同的是多了一个onUnBindViewHolder的方法,在这个方法里,可以做一下释放资源的操作,主要包括图片资源。

View

  1. 数据model的容器 ObjectAdapter,类似于RecyclerView.Adapter,但是将迭代展示每个item对应的view的任务分离了出去。实现类有ArrayObjectAdapter和CursorOjbectAdapter,前者持有列表数据。我们可以是实现自己的ObjectAdapter的子类。
  2. Preseter 负责将数据绑定到view上,并呈现view;presener和ObjectAdapter合起来相当于现在的Android里的Adapter.这种分离的优势在于,我们可以在Adaper的范围之外去控制view的创建。例如一个view是从单个对象的数据中产生的,另外的view是有ObjectAdapter来提供数据。比如我们现在页面的构成是由一个header 加一个vip行,再加多个相同模式的行。
  3. PresenterSelector类,用来选择用哪一个Presenter去对于ObjectAdapter提供的数据适配。通常是根据不同的item类型选择Presenter去适配.现在的页面中有多行相同视图展示的,也有其他少数几行展示的模式,这中情况下,例如聚好看的有头部的一行,和进入vip的一行,还有其他的列表行,这种情况下就可以用。
  4. leanback 提供的基本界面是纵向的列表,每个行元素是一个横向的列表,纵向列表和横向列表都用ObjectAdapter来提供数据。
  5. Row 是leanback中定义的一个抽象类,包含一个header和一个ListRow ,ListRow是Row的实现类,代表一个横行.用ListRowPresenter来展示view.我们也可以定义自己的RowPresenter来定义行的展示。

你可能感兴趣的:(Leanback UI简介)