一、 常见的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是一个抽象类,需要自己来继承该类。
需要实现下面的三个方法:
- onCreateViewHolder(ViewGroup parent);
- OnBindViewHolder(ViewHolder ViewHolder,Ojbect item);
- onUnBindViewHolder(Viewholder viewhlder);可以看到这些方法跟RecyclerView 的Apdater的实现方法很像,实际上这些方法就是借鉴了recyclerview的实现。
不同的是多了一个onUnBindViewHolder的方法,在这个方法里,可以做一下释放资源的操作,主要包括图片资源。
View
- 数据model的容器 ObjectAdapter,类似于RecyclerView.Adapter,但是将迭代展示每个item对应的view的任务分离了出去。实现类有ArrayObjectAdapter和CursorOjbectAdapter,前者持有列表数据。我们可以是实现自己的ObjectAdapter的子类。
- Preseter 负责将数据绑定到view上,并呈现view;presener和ObjectAdapter合起来相当于现在的Android里的Adapter.这种分离的优势在于,我们可以在Adaper的范围之外去控制view的创建。例如一个view是从单个对象的数据中产生的,另外的view是有ObjectAdapter来提供数据。比如我们现在页面的构成是由一个header 加一个vip行,再加多个相同模式的行。
- PresenterSelector类,用来选择用哪一个Presenter去对于ObjectAdapter提供的数据适配。通常是根据不同的item类型选择Presenter去适配.现在的页面中有多行相同视图展示的,也有其他少数几行展示的模式,这中情况下,例如聚好看的有头部的一行,和进入vip的一行,还有其他的列表行,这种情况下就可以用。
- leanback 提供的基本界面是纵向的列表,每个行元素是一个横向的列表,纵向列表和横向列表都用ObjectAdapter来提供数据。
- Row 是leanback中定义的一个抽象类,包含一个header和一个ListRow ,ListRow是Row的实现类,代表一个横行.用ListRowPresenter来展示view.我们也可以定义自己的RowPresenter来定义行的展示。
网友评论