美文网首页
Leanback UI简介

Leanback UI简介

作者: 程序猿想吃肉 | 来源:发表于2022-05-23 19:31 被阅读0次

    一、 常见的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来定义行的展示。

    相关文章

      网友评论

          本文标题:Leanback UI简介

          本文链接:https://www.haomeiwen.com/subject/tfcsdrtx.html