作者簡介
羅鐵錘,六年安卓踩坑經驗,致力于底層平臺、上層應用等多領域開發。文能靜坐彈吉他,武能通宵寫代碼
這是《從 Android 開發到讀懂源碼》系列文章最后一篇,感謝你的陪伴。
無論你是對 Android 感興趣還是對系列文件有建議,都歡迎加入 Android 交流群(文末有進群方式)。
最后這一節內容,讓我一起聊聊 Leanback。
BaseGridView 繼承 RecyclerView ,重寫所有焦點邏輯,Leanback 頁面根布局容器
HorizontalGridView 繼承 BaseGridView ,提供水平布局能力
VerticalGridView 繼承 BaseGridView ,提供垂直布局能力
ArrayObjectAdapter 數據適配器,繼承 ObjectAdapter ,內部可包含數據和視圖結構內容信息
ArrayObjectAdapter 的兩個構造方法:
// 一般用于每行或列數據的創建 /** * Constructs an adapter with the given {@link PresenterSelector}. */ public ArrayObjectAdapter(PresenterSelector presenterSelector) { super(presenterSelector); } // 一般為 ItemBridgeAdapter 創建構造參數時使用 /** * Constructs an adapter that uses the given {@link Presenter} for all items. */ public ArrayObjectAdapter(Presenter presenter) { super(presenter); }
ListRow 行視圖提供者,Android 原生封裝好了,支持子視圖焦點動效及行標題展示
Presenter 提供視圖創建及數據綁定,類似 RecyclerView.Adapter 的功能,注意是類似,下面的 ItemBridgeAdapter 才是填充到BaseGridView 中真正的 Adapter 。暫且稱之為視圖加載器
/** * Creates a new {@link View}. */ public abstract ViewHolder onCreateViewHolder(ViewGroup parent); /** * Binds a {@link View} to an item. */ public abstract void onBindViewHolder(ViewHolder viewHolder, Object item);
PresenterSelector 根據不同的 Item Object 類型提供不同的 Presenter 對象,進行不同的布局視圖創建和數據綁定,暫且稱之為視圖構造篩選器
/** * Returns a presenter for the given item. */ public abstract Presenter getPresenter(Object item); // 這個方法需要重寫,根據 item 返回對應的 presenter /** * Returns an array of all possible presenters. The returned array should * not be modified. */ public Presenter[] getPresenters() { return null; } // 這個方法需要返回所有的 presenters 數組,中途不可改變數據
ItemBridgeAdapter 填充至 BaseGridView 的適配器,繼承 RecyclerView.Adapter
主要有兩個構造方法,需要傳遞一個 ObjectAdapter
public ItemBridgeAdapter(ObjectAdapter adapter, PresenterSelector presenterSelector) { setAdapter(adapter); mPresenterSelector = presenterSelector; } public ItemBridgeAdapter(ObjectAdapter adapter) { this(adapter, null); }
public class PresenterSample extends Presenter { // 創建 BaseGridView 中每一個 Item 的視圖,如果使用 ListRow 則是創建每一行中的每一個 Item 視圖 @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { return new HolderSample(...ItemView...); } // 數據綁定,item 即是外層 ArrayObjectAdapter 中包含的數據類型 @Override public void onBindViewHolder(ViewHolder viewHolder, Object item) { if (viewHolder instanceof HolderSample) { HolderSample holder = (HolderSample) viewHolder; if (item instanceof YourDataType) { bindDataWithViews()... } } } @Override public void onUnbindViewHolder(ViewHolder viewHolder) { releaseSource()... } // 自定義 Holder,處理視圖點擊,焦點事件等 static class HolderSample extends ViewHolder { HolderSample(View view) { super(view); } }}
ListRow 方式構造
// 構造一個 ArrayObjectAdapter,填充一個 PresenterArrayObjectAdapter rowAdapter = new ArrayObjectAdapter(new PresenterSample());// 填充數據rowAdapter.add(...Data...);// 構造一個 ListRowListRow listRow = new ListRow(rowAdapter);
普通方式構造
// 構造一個指定數據類型對象CustomData data = new CustomData();
public class PresenterSelectorSample extends PresenterSelector { // 此處 item 就是外層 ArrayObjectAdapter 添加進來的數據,按添加索引排序 @Override public Presenter getPresenter(Object item) { if (item.getClass == ListRow.class) { return new ListRowPresenter(); } else if (item.getClass == CustomData.class) { return new PresenterCustom(); } else { return ... } } @Override public Presenter[] getPresenters() { return mPresenters.toArray(new Presenter[]{}); }}
// 構建一個自定義的 PresenterSelectorSample PresenterSelectorSample presenterSelector = new PresenterSelectorSample(); // 構建一個裝載每行數據的 ArrayObjectAdapter ArrayObjectAdapter verticalAdapter = new ArrayObjectAdapter(presenterSelector); // 填充數據 verticalAdapter.add(listRow); verticalAdapter.add(CustomData);
該 Adapter 只是作為一個橋梁作用,將每一行結構對應的 presenter 和 data 進行關聯
ItemBridgeAdapter bridgeAdapter = new ItemBridgeAdapter(verticalAdapter);VerticalGridView.setAdapter(bridgeAdapter);
至此,頁面展示效果如下:
首先看下 ArrayObjectAdapter ,它內部有個 ArrayList<Object> 保存數據,同時其父類 ObjectAdapter 中有個 mPresenterSelector 變量保存當前 adapter 對應的 presenterSelector 。
<ArrayObjectAdapter.java> public class ArrayObjectAdapter extends ObjectAdapter { ... // 當 ArrayObjectAdapter 作為行/列的數據提供者時 (ListRow),緩存每行/列的每個子 Item 的數據 // 當 ArrayObjectAdapter 作為 ItemBridgeAdapter 的構造參數時,緩存每行/列的數據對象 private final List mItems = new ArrayList<Object>(); public ArrayObjectAdapter(PresenterSelector presenterSelector) { super(presenterSelector); } public ArrayObjectAdapter(Presenter presenter) { super(presenter); } // 包含數據添加,刪除,修改,查詢方法 public void add(Object item) { add(mItems.size(), item); } remove(...) move(...) replace(...) get(...) ... } <ObjectAdapter.java> // 當 ArrayObjectAdapter 作為行/列的數據提供者時,緩存每行/列的視圖數據提供者 private PresenterSelector mPresenterSelector;// 緩存 presenter // 提供 get 方法獲取當前的 presenter public final Presenter getPresenter(Object item) { if (mPresenterSelector == null) { throw new IllegalStateException("Presenter selector must not be null"); } return mPresenterSelector.getPresenter(item); }
主適配器 ItemBridgeAdapter ,看 android 命名應該是一個橋接的適配器,這也是整個模塊中核心類之一
<ItemBridgeAdapter.java> // 緩存了構造傳進來的 ArrayObjectAdapter private ObjectAdapter mAdapter; // 緩存了 PresenterSelector 選擇器,根據不同 ViewType 獲取不同的 Presenter 進行不同的視圖加載 private PresenterSelector mPresenterSelector; // 焦點動效輔助類 FocusHighlightHandler mFocusHighlight; // 緩存了根據 PresenterSelector 創建出來的各個不同的 Presenter 視圖加載器 private ArrayList<Presenter> mPresenters = new ArrayList<Presenter>();
接著就按照正常使用 RecyclerView 的流程去分析 ItemBridgeAdapter ,首先是 getItemViewType() 方法。
<ItemBridgeAdapter.java> @Override public int getItemViewType(int position) { // mPresenterSelector 可以直接調用 setter 主動賦值,如果沒有賦值過,則會通過構造方法中的 // ArrayObjectAdapter.getPresenterSelector 進行獲取視圖構造篩選器 PresenterSelector presenterSelector = mPresenterSelector != null ? mPresenterSelector : mAdapter.getPresenterSelector(); // 這個 Object 就是構造傳進來的 ArrayObjectAdapter 中的數據 Object item = mAdapter.get(position); // 根據 Object 對象獲取對應的 presenter,這里是在自定義的 PresenterSelector 中進行分支判斷處理 Presenter presenter = presenterSelector.getPresenter(item); // 根據索引判斷緩存中該 Object 是否有 presenter 對象 int type = mPresenters.indexOf(presenter); if (type < 0) { // 不存在,將 presenter 加入緩存 mPresenters.add(presenter); // 將索引值賦值給 viewType type = mPresenters.indexOf(presenter); if (DEBUG) Log.v(TAG, "getItemViewType added presenter " + presenter + " type " + type); // 回調通知外部當前添加了 presenter onAddPresenter(presenter, type); if (mAdapterListener != null) { mAdapterListener.onAddPresenter(presenter, type); } } return type; }
在這里我們先暫停,去看下 PresenterSelector 中創建 Presenter 對象部分
<PresenterSelector.java> // PresenterSelector 是一個抽象類,需要我們自己實現以下兩個方法 public abstract class PresenterSelector { public abstract Presenter getPresenter(Object item); public Presenter[] getPresenters() { return null; } }// 這里以我們自己定義的一個 Sample 為例<PresenterSelectorSample.java>public class PresenterSelectorSample extends PresenterSelector { private List<Presenter> mPresenters = new ArrayList<>(); private Map<Class<?>, Presenter> mPresenterCache = new HashMap<>(); public void addPresenter(Class<?> cls, Presenter presenter) { mPresenterCache.put(cls, presenter); if (!mPresenters.contains(presenter)) { mPresenters.add(presenter); } } @Override public Presenter getPresenter(Object item) { // 我們會調用 addPresenter 方法進行 setter 操作,此處通過 map 進行緩存 // 注意:實際中還要進行 class 的重復沖突處理,如有多個 ListRow,每個 ListRow 中的 Presenter 視圖展示效果不一樣 Class<?> cls = item.getClass(); // 然后通過 class 進行 getter 取操作 Presenter presenter = mPresenterCache.get(cls); if (presenter != null) { return presenter; } else { return null; } } @Override public Presenter[] getPresenters() { // 返回所有的 Presenters return mPresenters.toArray(new Presenter[]{}); }}// sample codePresenterSelectorSample presenterSelector = new PresenterSelectorSample();presenterSelector.addPresenter(ListRow.class, new ListRowPresenter());presenterSelector.addPresenter(CustomDataObject.class, new CustomPresenter());ArrayObjectAdapter adapter = new ArrayObjectAdapter(presenterSelector);adapter.add(new ListRow);adapter.add(new CustomDataObject());ItemBridgeAdapter bridgeAdapter = new ItemBridgeAdapter(adapter);// 1.這樣 ItemBridgeAdapter.getItemViewType 中 mAdapter.getPresenterSelector() 取出來的就是我們構建的PresenterSelectorSample。// 2.然后 mAdapter.get(position) 就是我們上面 adapter 添加進去的數據。例如 position = 1 時,取出來的就是一個 ListRow 對象。// 3.接著調用我們 PresenterSelectorSample 中的 getPresenter(item) 方法,會根據 ListRow.class 返回一個ListRowPresenter。同時緩存到 ItemBridgeAdapter 的 mPresenters 變量中。并且將 ViewType 用 presenter 在緩存池中的索引與之對應起來,方便后面 onCreateViewHolder 中的獲取。
此時,我們就可以理解了 Presenter,PresenterSelector,ArrayObjectAdapter,ItemBridgeAdapter 之間的關系。
回到 ItemBridgeAdapter ,分析其 onCreateViewHolder 方法
<ItemBridgeAdapter.java> /** * {@link View.OnFocusChangeListener} that assigned in * {@link Presenter#onCreateViewHolder(ViewGroup)} may be chained, user should never change * {@link View.OnFocusChangeListener} after that. */ @Override public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { // 首先取出 presenter,前面也說了 viewType 已經通過緩存池中的索引關聯了 presenter Presenter presenter = mPresenters.get(viewType); // 我們熟悉的 ViewHolder,注意,這個是我們 presenter 中自定義的 viewHolder Presenter.ViewHolder presenterVh; // viewHolder 中的 view View view; if (mWrapper != null) { view = mWrapper.createWrapper(parent); presenterVh = presenter.onCreateViewHolder(parent); mWrapper.wrap(view, presenterVh.view); } else {// 一般走這里 // 這里會去調用我們自定義 Presenter 中的 onCreateViewHolder 進行 holder 和 view 的創建 presenterVh = presenter.onCreateViewHolder(parent); // 將試圖賦值給 viewHolder 中的 view view = presenterVh.view; } // 將我們 presenter 的 viewHolder,presenter,view 一起打包進行封裝 ViewHolder viewHolder = new ViewHolder(presenter, view, presenterVh); // 回調通知 onCreate(viewHolder); if (mAdapterListener != null) { mAdapterListener.onCreate(viewHolder); } // 這個 view 就是我們 presenter 中的創建的視圖 View presenterView = viewHolder.mHolder.view; if (presenterView != null) { // 為我們 presenter 中的 view 設置 focus 監聽,焦點變化時如果設置了 FocusHighlight 則會自動執行動效 viewHolder.mFocusChangeListener.mChainedListener = presenterView.getOnFocusChangeListener(); presenterView.setOnFocusChangeListener(viewHolder.mFocusChangeListener); } if (mFocusHighlight != null) { // 如果設置了 FocusHighlight,在此焦點動效初始化 mFocusHighlight.onInitializeView(view); } // 返回創建好的 viewHolder(里面包含 presenter,holder,view 信息) return viewHolder; }
接下去就是 ItemBridgeAdapter 的 onCreateViewHolder 方法
<ItemBridgeAdapter.java> @Override public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (DEBUG) Log.v(TAG, "onBindViewHolder position " + position); // 這個 holder 包含了 presenter,該 presenter 中對應的 holder,view ViewHolder viewHolder = (ViewHolder) holder; // mItem 是一個 Object 對象,也就是上面 getItemViewType 所說的 ArrayObjectAdapter 中的數據,例如 sample 中的 CustomDataObject 和 ListRow viewHolder.mItem = mAdapter.get(position); // 調用對應 presenter 中的 onBindViewHolder 方法 viewHolder.mPresenter.onBindViewHolder(viewHolder.mHolder, viewHolder.mItem); // 回調通知 onBind(viewHolder); if (mAdapterListener != null) { mAdapterListener.onBind(viewHolder); } }
拋開 RecyclerView 視圖部分原理,此時視圖創建和數據綁定都已經完成了,界面上已經可以展示了。
ListRow 繼承 Row 是 android 封裝好的行數據展示的一種抽象(并不是實際 View 的展示,leanback 系統中 view 的創建都是在 presenter 層,對應 ListRowPresenter ),其結構如下:
<ListRow.java>public class ListRow extends Row { // 這個 adapter 包含了該行中所有子 Item 的數據 private final ObjectAdapter mAdapter; // 行標題文字,對應父類 Row 中的 HeaderItem private CharSequence mContentDescription; ...}<Row.java>public class Row { // 行標題的結構體 private HeaderItem mHeaderItem; ...}<HeaderItem.java>// 也是一種抽象,對應視圖也是在 RowHeaderPresenter 中創建public class HeaderItem { private final long mId; private final String mName; private CharSequence mDescription; private CharSequence mContentDescription; ...}
接下來看下視圖部分 ListRowPresenter ,其繼承 RowPresenter ,而 RowPresenter 繼承 Presenter , Presenter 在上面已經介紹過了,主要有這幾個抽象方法:onCreateViewHolder , onBindViewHolder , onUnbindViewHolder
首先看下 ListRowPresenter 內部的幾個內部類:
<RowPresenter.java>public abstract class RowPresenter extends Presenter { ... // RowPresenter 這里不作重點分析,主要是對 ViewHolder 的抽象封裝 // 1.ContainerViewHolder,它內部持有一個 ViewHolder // 2.ViewHolder}<ListRowPresenter.java>public class ListRowPresenter extends RowPresenter { // 行視圖的 ViewHolder public static class ViewHolder extends RowPresenter.ViewHolder { // 持有 presenter final ListRowPresenter mListRowPresenter; // 這個其實就是展示水平列表視圖的 HorizontalGridView final HorizontalGridView mGridView; // 可以類比上面垂直視圖案例中的 ItemBridgeAdapter,作為橋梁關聯 mGridView 中每個 item 視圖創建者 presenter ItemBridgeAdapter mItemBridgeAdapter; // 暫不分析 final HorizontalHoverCardSwitcher mHoverCardViewSwitcher = new HorizontalHoverCardSwitcher(); // layout 參數 final int mPaddingTop; ... public ViewHolder(View rootView, HorizontalGridView gridView, ListRowPresenter p) { super(rootView); mGridView = gridView; mListRowPresenter = p; mPaddingTop = mGridView.getPaddingTop(); ... } // 下面就是一些 getter 屬性方法 ... } // 選中的 Position 變化回調監聽 /** * A task on the ListRowPresenter.ViewHolder that can select an item by position in the * HorizontalGridView and perform an optional item task on it. */ public static class SelectItemViewHolderTask extends Presenter.ViewHolderTask { private int mItemPosition;// 選中的 position private boolean mSmoothScroll = true; Presenter.ViewHolderTask mItemTask;// 緩存 task /** * Sets task to run when the item is selected, null for no task. * @param itemTask Optional task to run when the item is selected, null for no task. */ public void setItemTask(Presenter.ViewHolderTask itemTask) { // 設置 task,等待時機執行 run 方法,如果沒設置,run 方法無效。本質上只是給外部提供一個監聽選中 position 變化的回調 mItemTask = itemTask; } // 主要是提供一些設置選中 position,是否平滑滾動之類的方法 ... // 關鍵是 run 方法 @Override public void run(Presenter.ViewHolder holder) { if (holder instanceof ListRowPresenter.ViewHolder) { // 獲取到列表視圖 HorizontalGridView HorizontalGridView gridView = ((ListRowPresenter.ViewHolder) holder).getGridView(); androidx.leanback.widget.ViewHolderTask task = null; // 如果之前設置過 task,則新創建 if (mItemTask != null) { task = new androidx.leanback.widget.ViewHolderTask() { final Presenter.ViewHolderTask itemTask = mItemTask; @Override public void run(RecyclerView.ViewHolder rvh) { ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) rvh; itemTask.run(ibvh.getViewHolder()); } }; } // 設置選中的 position,并將 task 和 postition 傳遞給 BaseGridView,里面會執行 task 的 run 方法,再執行我們外部 setter 進來的 Presenter.ViewHolderTask 的 run 方法 if (isSmoothScroll()) { gridView.setSelectedPositionSmooth(mItemPosition, task); } else { gridView.setSelectedPosition(mItemPosition, task); } } } } // 對交互事件的處理 class ListRowPresenterItemBridgeAdapter extends ItemBridgeAdapter { ListRowPresenter.ViewHolder mRowViewHolder; ListRowPresenterItemBridgeAdapter(ListRowPresenter.ViewHolder rowViewHolder) { mRowViewHolder = rowViewHolder; } @Override protected void onCreate(ItemBridgeAdapter.ViewHolder viewHolder) { if (viewHolder.itemView instanceof ViewGroup) { TransitionHelper.setTransitionGroup((ViewGroup) viewHolder.itemView, true); } if (mShadowOverlayHelper != null) { mShadowOverlayHelper.onViewCreated(viewHolder.itemView); } } @Override public void onBind(final ItemBridgeAdapter.ViewHolder viewHolder) { // 監聽點擊事件 // Only when having an OnItemClickListener, we will attach the OnClickListener. if (mRowViewHolder.getOnItemViewClickedListener() != null) { viewHolder.mHolder.view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder) mRowViewHolder.mGridView.getChildViewHolder(viewHolder.itemView); if (mRowViewHolder.getOnItemViewClickedListener() != null) { mRowViewHolder.getOnItemViewClickedListener().onItemClicked(viewHolder.mHolder, ibh.mItem, mRowViewHolder, (ListRow) mRowViewHolder.mRow); } } }); } } @Override public void onUnbind(ItemBridgeAdapter.ViewHolder viewHolder) { // 移除點擊事件 if (mRowViewHolder.getOnItemViewClickedListener() != null) { viewHolder.mHolder.view.setOnClickListener(null); } } @Override public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) { applySelectLevelToChild(mRowViewHolder, viewHolder.itemView); // 設置是否持久化狀態顯示 mRowViewHolder.syncActivatedStatus(viewHolder.itemView); } @Override public void onAddPresenter(Presenter presenter, int type) { // 默認緩存池大小為 24 個,針對每種不同 presenter 可以設置不同的緩存空間 mRowViewHolder.getGridView().getRecycledViewPool().setMaxRecycledViews( type, getRecycledPoolSize(presenter)); } }}
接下來就是真正的 ListRowPresenter
<ListRowPresenter.java>public class ListRowPresenter extends RowPresenter { // 行數,這個行數不是 ListRow 的行數,ListRow 抽象上的單行,這個行數是指其內部 HorizontalGridView 的行數 private int mNumRows = 1; // 行高度 private int mRowHeight; // 展開行高度 private int mExpandedRowHeight; private PresenterSelector mHoverCardPresenterSelector; // 縮放比例:FocusHighlight 中定義的常量 private int mFocusZoomFactor; // 是否使用聚焦高亮那種效果 private boolean mUseFocusDimmer; // 是否支持陰影 private boolean mShadowEnabled = true; private int mBrowseRowsFadingEdgeLength = -1; private boolean mRoundedCornersEnabled = true; private boolean mKeepChildForeground = true; // 緩存池 private HashMap<Presenter, Integer> mRecycledPoolSize = new HashMap<Presenter, Integer>(); ShadowOverlayHelper mShadowOverlayHelper; // 主要功能就是通過一層 FrameLayout(ShadowOverlayContainer) 包裹當前 View 實現陰影,高亮昏暗,圓角效果,api21 以上才能使用 private ItemBridgeAdapter.Wrapper mShadowOverlayWrapper; private static int sSelectedRowTopPadding; private static int sExpandedSelectedRowTopPadding; private static int sExpandedRowNoHovercardBottomPadding; // 3個構造方法,主要是設置聚焦縮放等級和陰影效果 public ListRowPresenter() { this(FocusHighlight.ZOOM_FACTOR_MEDIUM); } public ListRowPresenter(int focusZoomFactor) { this(focusZoomFactor, false); } public ListRowPresenter(int focusZoomFactor, boolean useFocusDimmer) { if (!FocusHighlightHelper.isValidZoomIndex(focusZoomFactor)) { throw new IllegalArgumentException("Unhandled zoom factor"); } mFocusZoomFactor = focusZoomFactor; mUseFocusDimmer = useFocusDimmer; }<RowPresenter.java> // 接下來直接看 onCreateViewHolder 方法,在父類 RowPresenter 中 @Override public final Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) { // 創建臨時 ViewHolder,這個 holder 只包含列表視圖 HorizontalGridView,不包含頭部視圖 ViewHolder vh = createRowViewHolder(parent); vh.mInitialzed = false;// 標記未初始化過 // 最終真正的 ViewHolder Presenter.ViewHolder result; // 如果設置了 Header 標題視圖,需要在外部添加布局和頭部視圖 if (needsRowContainerView()) { RowContainerView containerView = new RowContainerView(parent.getContext()); if (mHeaderPresenter != null) { // 創建頭部視圖 vh.mHeaderViewHolder = (RowHeaderPresenter.ViewHolder) mHeaderPresenter.onCreateViewHolder((ViewGroup) vh.view); } result = new ContainerViewHolder(containerView, vh); } else {// 沒有設置頭部標題 result = vh; } // 初始化 holder initializeRowViewHolder(vh); if (!vh.mInitialzed) { throw new RuntimeException("super.initializeRowViewHolder() must be called"); } return result; }<ListRowPresenter.java> // 創建 ViewHolder @Override protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) { initStatics(parent.getContext()); // ListRowView 其實就是一個布局封裝,里面包含一個 HorizontalGridView ListRowView rowView = new ListRowView(parent.getContext()); setupFadingEffect(rowView); if (mRowHeight != 0) { // 設置行高度 rowView.getGridView().setRowHeight(mRowHeight); } return new ViewHolder(rowView, rowView.getGridView(), this); } @Override protected void initializeRowViewHolder(RowPresenter.ViewHolder holder) { // 父類 RowPresenter 中只是設置 clipChildren 屬性為 false,因為設置 true 的話會影響縮放動效 super.initializeRowViewHolder(holder); // 獲取 holder final ViewHolder rowViewHolder = (ViewHolder) holder; // ItemView 的 context Context context = holder.view.getContext(); // 陰影效果相關,暫不分析,內部就是通過一層 FrameLayout 包裹當前的 View 實現陰影等效果 if (mShadowOverlayHelper == null) { mShadowOverlayHelper = new ShadowOverlayHelper.Builder() .needsOverlay(needsDefaultListSelectEffect()) .needsShadow(needsDefaultShadow()) .needsRoundedCorner(isUsingOutlineClipping(context) && areChildRoundedCornersEnabled()) .preferZOrder(isUsingZOrder(context)) .keepForegroundDrawable(mKeepChildForeground) .options(createShadowOverlayOptions()) .build(context); if (mShadowOverlayHelper.needsWrapper()) { mShadowOverlayWrapper = new ItemBridgeAdapterShadowOverlayWrapper( mShadowOverlayHelper); } } // 構造橋接 ItemBridgeAdapter rowViewHolder.mItemBridgeAdapter = new ListRowPresenterItemBridgeAdapter(rowViewHolder); // set wrapper if needed rowViewHolder.mItemBridgeAdapter.setWrapper(mShadowOverlayWrapper); mShadowOverlayHelper.prepareParentForShadow(rowViewHolder.mGridView); // ListRow 默認會給設置 Item 的焦點縮放動效,下面動效部分單獨分析 FocusHighlightHelper.setupBrowseItemFocusHighlight(rowViewHolder.mItemBridgeAdapter, mFocusZoomFactor, mUseFocusDimmer); rowViewHolder.mGridView.setFocusDrawingOrderEnabled(mShadowOverlayHelper.getShadowType() != ShadowOverlayHelper.SHADOW_DYNAMIC); // 通過 BaseGridView 監聽焦點選中回調 rowViewHolder.mGridView.setOnChildSelectedListener( new OnChildSelectedListener() { @Override public void onChildSelected(ViewGroup parent, View view, int position, long id) { selectChildView(rowViewHolder, view, true); } }); // 通過 BaseGridView 監聽按鍵事件 rowViewHolder.mGridView.setOnUnhandledKeyListener( new BaseGridView.OnUnhandledKeyListener() { @Override public boolean onUnhandledKey(KeyEvent event) { return rowViewHolder.getOnKeyListener() != null && rowViewHolder.getOnKeyListener().onKey( rowViewHolder.view, event.getKeyCode(), event); } }); // 設置 HorizontalGridView 的行數 rowViewHolder.mGridView.setNumRows(mNumRows); }<RowPresenter.java> // 父類 RowPresenter 的初始化 holder 方法 protected void initializeRowViewHolder(ViewHolder vh) { vh.mInitialzed = true;// 標記已經初始化過 if (!isClippingChildren()) { // set clip children to false for slide transition if (vh.view instanceof ViewGroup) { ((ViewGroup) vh.view).setClipChildren(false); } if (vh.mContainerViewHolder != null) { ((ViewGroup) vh.mContainerViewHolder.view).setClipChildren(false); } } }<ListRowPresenter.java> // onBindRowViewHolder 方法 @Override protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) { super.onBindRowViewHolder(holder, item); // 獲取 holder ViewHolder vh = (ViewHolder) holder; // 獲取到 ListRow ListRow rowItem = (ListRow) item; // ListRow 中的 ObjectAdapter,設置到橋接的 ItemBridgeAdapter 中 vh.mItemBridgeAdapter.setAdapter(rowItem.getAdapter()); // 設置 HorizontalGridView 的 adapter,而 ItemBridgeAdapter 的 createRowViewHolder 會調用我們 ListRow 中 ObjectAdapter 的自定義 Presenter 創建每一個子 Item 的視圖,onBindRowViewHolder 會將數據綁定 vh.mGridView.setAdapter(vh.mItemBridgeAdapter); // 設置 row 描述信息 vh.mGridView.setContentDescription(rowItem.getContentDescription()); }}
至此, ListRow 的視圖創建和數據綁定已經分析完了,其實內部子 Item 的視圖創建和數據綁定是沿用 ItemBridgeAdapter 方式。
在 Leanback 中的橫豎列表展現形式都是通過這種 Presenter 與 BaseGridView 之間的嵌套關系進行剝離。例如在多 ViewType 的形式下,一般我們寫 RecyclerView.Adapter 是這樣的:
public class CutstomAdapter extends RecyclerView.Adapter<VH> { @NonNull @Override public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == Type1) { return VH1;// 不同行為對象 } else if (viewType == Type2) { return VH2;// 不同行為對象 } else ... ...// 不同行為對象... } @Override public void onBindViewHolder(@NonNull VH holder, int position) { if (holder instance VH1) { bind1(); } else if (holder instance VH2) { bind2(); } else ... ... } class VH1 extends Vh {} class VH2 extends Vh {} ...}
在 Leanback 結構中對應的是 ItemBridgeAdapter ,其僅充當一個橋接作用,結構中增加定義了一個 Presenter 抽象,將 onCreateViewHolder 和 onBindViewHolder 等行為抽離出去,讓每個有不同樣式的 CustomPresenter 自身去實現具體視圖和數據行為,這樣當需要增加新的樣式和數據時,只需要往橋接類中添加對應的 Presenter 實現即可(往 ArrayObjectAdapter 中添加)。
對于 Leanback 中使用原生展示控件,比如 ListRow 這種,其默認是會實現焦點縮放動效。上面分析 ListRowPresenter 時可以看到,其內部默認幫我們調用了 FocusHighlightHelper.setupBrowseItemFocusHighlight() 方法,在 Item 發生焦點變化時,焦點的監聽回調中會通過 Helper 的方法實現縮放效果。
首先看下 FocusHighlightHelper 這個類
<FocusHighlightHelper.java>public class FocusHighlightHelper { // 是否可縮放 static boolean isValidZoomIndex(int zoomIndex) { return zoomIndex == ZOOM_FACTOR_NONE || getResId(zoomIndex) > 0; } // 獲取縮放比例 static int getResId(int zoomIndex) { switch (zoomIndex) { case ZOOM_FACTOR_SMALL: return R.fraction.lb_focus_zoom_factor_small; case ZOOM_FACTOR_XSMALL: return R.fraction.lb_focus_zoom_factor_xsmall; ... // 具體值在 res 的 value 文件中定義縮放比例 <item name="lb_focus_zoom_factor_large" type="fraction">118%</item> <item name="lb_focus_zoom_factor_medium" type="fraction">114%</item> <item name="lb_focus_zoom_factor_small" type="fraction">110%</item> <item name="lb_focus_zoom_factor_xsmall" type="fraction">106%</item> } // 綁定焦點動效 public static void setupBrowseItemFocusHighlight(ItemBridgeAdapter adapter, int zoomIndex, boolean useDimmer) { // 這里我們只關注 BrowseItemFocusHighlight,HeaderItemFocusHighlight 類似 adapter.setFocusHighlight(new BrowseItemFocusHighlight(zoomIndex, useDimmer)); } static class BrowseItemFocusHighlight implements FocusHighlightHandler { // 時長 private static final int DURATION_MS = 150; // 縮放等級 private int mScaleIndex; // 是否使用陰影 private final boolean mUseDimmer; BrowseItemFocusHighlight(int zoomIndex, boolean useDimmer) { if (!isValidZoomIndex(zoomIndex)) { throw new IllegalArgumentException("Unhandled zoom index"); } mScaleIndex = zoomIndex; mUseDimmer = useDimmer; } ... // 焦點變化監聽回調 @Override public void onItemFocused(View view, boolean hasFocus) { view.setSelected(hasFocus); // 第一個參數是否聚焦,第二個參數表示是否跳過動畫執行過程直接展示結果 getOrCreateAnimator(view).animateFocus(hasFocus, false); } // 初始化,如果綁定了動畫,ItemBridgeAdapter 的 onCreateViewHolder 中會調用 @Override public void onInitializeView(View view) { getOrCreateAnimator(view).animateFocus(false, true); } // 創建或者獲取動畫對象 private FocusAnimator getOrCreateAnimator(View view) { // 此處通過 view 的 tag 進行緩存,避免了頻繁創建動畫對象的開銷,這種對性能敏感度的思想非常值得學習 FocusAnimator animator = (FocusAnimator) view.getTag(R.id.lb_focus_animator); if (animator == null) { animator = new FocusAnimator( view, getScale(view.getResources()), mUseDimmer, DURATION_MS); view.setTag(R.id.lb_focus_animator, animator); } return animator; } } // 動畫對象 static class FocusAnimator implements TimeAnimator.TimeListener { private final View mView; private final int mDuration; // 支持陰影的 FrameLayout,SDK_INT >= 21 以上才支持,此處不詳細分析了 private final ShadowOverlayContainer mWrapper; private final float mScaleDiff; private float mFocusLevel = 0f; private float mFocusLevelStart; private float mFocusLevelDelta; private final TimeAnimator mAnimator = new TimeAnimator(); private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator(); // 顏色遮罩,就是那種聚焦高亮,非聚焦昏暗的效果層,內部通過 canvas 的 drawRect 方式實現,此處不詳細分析了 private final ColorOverlayDimmer mDimmer; void animateFocus(boolean select, boolean immediate) { // 先結束上一次動畫 endAnimation(); final float end = select ? 1 : 0; if (immediate) { // 不需要過程,直接 setScale 設置最終效果,結束 setFocusLevel(end); } else if (mFocusLevel != end) { // 需要動畫過程,開始執行動畫 mFocusLevelStart = mFocusLevel; mFocusLevelDelta = end - mFocusLevelStart; mAnimator.start(); } } FocusAnimator(View view, float scale, boolean useDimmer, int duration) { // 動畫執行的 view mView = view; // 動畫時長 mDuration = duration; // 動畫縮放的比例差值 mScaleDiff = scale - 1f; // 陰影和高亮效果 if (view instanceof ShadowOverlayContainer) { mWrapper = (ShadowOverlayContainer) view; } else { mWrapper = null; } mAnimator.setTimeListener(this); if (useDimmer) { mDimmer = ColorOverlayDimmer.createDefault(view.getContext()); } else { mDimmer = null; } } // 改變當前動畫值 void setFocusLevel(float level) { mFocusLevel = level; float scale = 1f + mScaleDiff * level; // 縮放 mView.setScaleX(scale); mView.setScaleY(scale); // 陰影和高亮效果 if (mWrapper != null) { mWrapper.setShadowFocusLevel(level); } else { ShadowOverlayHelper.setNoneWrapperShadowFocusLevel(mView, level); } if (mDimmer != null) { // 改變高亮或者昏暗的透明度值 mDimmer.setActiveLevel(level); int color = mDimmer.getPaint().getColor(); if (mWrapper != null) { // 設置陰影 mWrapper.setOverlayColor(color); } else { // 取消陰影 ShadowOverlayHelper.setNoneWrapperOverlayColor(mView, color); } } } ... void endAnimation() { mAnimator.end(); } // 估值器 @Override public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) { float fraction; if (totalTime >= mDuration) { // 動畫結束 fraction = 1; mAnimator.end(); } else { // 計算當前動畫執行進度 fraction = (float) (totalTime / (double) mDuration); } if (mInterpolator != null) { // 有插值器的情況下計算的動畫執行進度 fraction = mInterpolator.getInterpolation(fraction); } // 改變當前動畫的值 setFocusLevel(mFocusLevelStart + fraction * mFocusLevelDelta); } }}
下面我們看下是如何監聽 Item 的焦點變化的
<ItemBridgeAdapter.java> @Override public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { ... if (presenterView != null) { // 設置焦點變化監聽,這個 Listener 是每個 ViewHolder 中對應的,監聽的是 ViewHolder 的 ItemView viewHolder.mFocusChangeListener.mChainedListener = presenterView.getOnFocusChangeListener(); presenterView.setOnFocusChangeListener(viewHolder.mFocusChangeListener); } if (mFocusHighlight != null) { // 這里會創建動畫對象,并且緩存到 view 的 tag 中 mFocusHighlight.onInitializeView(view); } return viewHolder; } // 焦點監聽回調 final class OnFocusChangeListener implements View.OnFocusChangeListener { // 這個內部的 listener 實在沒搞懂干嘛用的,可能是為以后擴展準備的吧 View.OnFocusChangeListener mChainedListener; @Override public void onFocusChange(View view, boolean hasFocus) { if (DEBUG) { Log.v(TAG, "onFocusChange " + hasFocus + " " + view + " mFocusHighlight" + mFocusHighlight); } if (mWrapper != null) { view = (View) view.getParent(); } if (mFocusHighlight != null) { // 看到了,這里就會執行 BrowseItemFocusHighlight 的 onItemFocused 方法 mFocusHighlight.onItemFocused(view, hasFocus); } if (mChainedListener != null) { mChainedListener.onFocusChange(view, hasFocus); } } }
至此, Leanback 中焦點縮放動效也分析完了,里面其實就是監聽焦點變化,執行相應的 scale 動畫而已。不過里面為了節省頻繁創建動畫對象的性能開銷,通過 View.Tag 緩存思想的確值得學習借鑒。
專欄《從 Android 開發到讀懂源碼》系列文章推薦
第01期:requestFocus 源碼分析
第02期:NestScroll 機制源碼解析
第03期:View.post 源碼解析
第04期:LiveData 源碼解析
本文由 貴州做網站公司 整理發布,部分圖文來源于互聯網,如有侵權,請聯系我們刪除,謝謝!
網絡推廣與網站優化公司(網絡優化與推廣專家)作為數字營銷領域的核心服務提供方,其價值在于通過技術手段與策略規劃幫助企業提升線上曝光度、用戶轉化率及品牌影響力。這...
在當今數字化時代,公司網站已成為企業展示形象、傳遞信息和開展業務的重要平臺。然而,對于許多公司來說,網站建設的價格是一個關鍵考量因素。本文將圍繞“公司網站建設價...
在當今的數字化時代,企業網站已成為企業展示形象、吸引客戶和開展業務的重要平臺。然而,對于許多中小企業來說,高昂的網站建設費用可能會成為其發展的瓶頸。幸運的是,隨...
手機UC可以看的電影網站?用uc瀏覽器看電影的步驟如下:1.首先雙擊電腦上的UC瀏覽器打開。2、進入主頁面,點擊左側視頻。3.打開電影后,我們可以在搜索框中搜索想要觀看的電影名稱。這里我們以尋找鋼鐵俠2為例。4.搜索后,主頁面左側有一個可播放的視頻網站。下面是視頻。點擊觀看。這就解決了用uc瀏覽器看電影的問題。uc手機瀏覽器搜索引擎是多少?uc瀏覽器的默認搜索引擎是 "神馬搜索 ",可以設置。瀏覽...
dnf浴血之怒如何獲得?血怒是DNF狂暴者的一大技能。你可以在一次睡眠后學習,然后升級你的等級DNF狂戰如何無限浴血之怒?DNF紅眼無限獄浴狂暴的裝備條件有:無雙護胸的魔法戰、無雙腿甲的魔法戰、無雙腰帶的魔法戰、時間控制者的斗篷、明天的洗禮春花或怪物圍攻buff。DNF紅眼無限監獄浴憤怒藥達到的條件:精神刺激長生不老藥。注意:紅眼的無限血怒需要時間才能達到,需要血怒等級。無限爆血派最好的武器就是三...
保定東站到深圳多長時間?保定東站應該是高鐵站,所以從保定東站到深圳北高鐵站需要九個小時三十分鐘。北京到深圳的高鐵經過河北、河南、湖北、湖南、廣東。如果是快車,到深圳要25個小時。所以高鐵時代為大家節省了大量的時間,提高了工作效率,為的發展點贊 美國鐵路行業。保定東站到深圳多長時間? 71次,保定東-深圳北用時9小時23分;D927,保定東-深圳北用時10小時13分。(以上情況下,D927次列車為動...