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<Movie> 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<Card.Type, Presenter> presenters = new HashMap<Card.Type, Presenter>();

    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<ImageCardView> {
    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<HashMap<String, List<Movie>>> arg0,
                       HashMap<String, List<Movie>> data) {

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

    int i = 0;

    for (Map.Entry<String, List<Movie>> entry : data.entrySet()) {
        ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(cardPresenter);
        List<Movie> 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来定义行的展示。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容