Android TV:使用Leanback寫國內(nèi)UI風(fēng)格的TV應(yīng)用

Demo地址:https://github.com/iSuperRed/LeanbackTvSample.git

圖一 LeanbackTvSample_1.png

背景

??現(xiàn)在國內(nèi)主流的TV端視頻播放軟件碉咆、TV端桌面的UI風(fēng)格都差不多了阀趴。這個(gè)“差不多”不僅是說版式排布“差不多”染服,也是在說交互邏輯的“差不多”窑眯。
??從版式排布上來說氏淑,主頁(如圖一所示)的最上面會(huì)有一排比較重要的功能按鈕腕巡,比如搜索、歷史嫩挤、登錄害幅、引導(dǎo)開通VIP消恍、廣告岂昭、網(wǎng)絡(luò)狀態(tài)、時(shí)間狠怨、Logo等等约啊,這一排按鈕下面是視頻分類的標(biāo)題,標(biāo)題下面是流式布局的視頻內(nèi)容頁佣赖,通過切換標(biāo)題能夠切換視頻內(nèi)容頁恰矩,在視頻內(nèi)容頁按遙控器下鍵能夠加載多頁內(nèi)容,當(dāng)前視頻內(nèi)容頁沒有更多內(nèi)容時(shí)憎蛤,會(huì)有文字提示“到底部了”或者會(huì)有個(gè)按鈕外傅,點(diǎn)擊該按鈕后會(huì)回到頂部纪吮;而焦點(diǎn)態(tài)的標(biāo)記是通過放大、加邊框和加陰影的方式來實(shí)現(xiàn)的萎胰。
??從交互邏輯上來說碾盟,進(jìn)到主頁后,會(huì)有一個(gè)默認(rèn)焦點(diǎn)技竟,很多App的默認(rèn)焦點(diǎn)都是標(biāo)題上的“精選”頁冰肴,然后通過遙控器的上、下榔组、左熙尉、右、返回搓扯、OK检痰、Home等按鍵進(jìn)行人機(jī)交互;如果你想按返回鍵退出當(dāng)前App擅编,焦點(diǎn)會(huì)先回到當(dāng)前頁的標(biāo)題攀细,如果當(dāng)前標(biāo)題不是進(jìn)入主頁的默認(rèn)標(biāo)題,再按返回鍵會(huì)先回到默認(rèn)標(biāo)題爱态,回到默認(rèn)標(biāo)題后再按返回鍵會(huì)給個(gè)對話框問一下“是否要退出App”谭贪,確定退出后才會(huì)真的退出;很多App都會(huì)加邊界抖動(dòng)锦担,當(dāng)按遙控器方向鍵時(shí)俭识,查找該方向的下一個(gè)焦點(diǎn)View為null時(shí),會(huì)給當(dāng)前焦點(diǎn)View一個(gè)抖動(dòng)動(dòng)畫來提示用戶當(dāng)前已經(jīng)到邊界了洞渔。
??上面這些就是當(dāng)前國內(nèi)視頻播放軟件的一些基本特點(diǎn)了套媚。

分析

??下面我來說道說道我是怎么通過Leanback實(shí)現(xiàn)這些功能的。
??如圖二所示磁椒,我要實(shí)現(xiàn)一個(gè)這樣的頁面堤瘤。標(biāo)題我使用的是HorizontalGridView(一個(gè)水平方向的RecyclerView);標(biāo)題下面的內(nèi)容頁是ViewPager嵌套Fragment浆熔,ViewPager實(shí)現(xiàn)切頁本辐,每一個(gè)頁都是一個(gè)Fragment;Fragment的布局是一個(gè)VerticalGridView(一個(gè)是垂直方向上的RecyclerView)医增,VerticalGridView的每一個(gè)item又是一個(gè)HorizontalGridView慎皱,相當(dāng)于垂直方向上的RecyclerView嵌套了水平方向的RecyclerView。

圖二 LeanbackTvSample_2.png

實(shí)現(xiàn)

實(shí)現(xiàn)一 切換標(biāo)題叶骨,ViewPager聯(lián)動(dòng)

??關(guān)于標(biāo)題的實(shí)現(xiàn)可以看這篇 聊一聊 Leanback 中的 HorizontalGridView茫多,或者看本篇Demo的源碼也可以。標(biāo)題的一個(gè)重要的作用是切換頁面忽刽,所以O(shè)nChildViewHolderSelectedListener這個(gè)監(jiān)聽很重要天揖,HorizontalGridView添加這個(gè)監(jiān)聽后夺欲,在切換標(biāo)題Item時(shí),會(huì)回調(diào)這個(gè)監(jiān)聽的onChildViewHolderSelected方法今膊,而我就是在這個(gè)方法里調(diào)用了mViewPager.setCurrentItem來實(shí)現(xiàn)標(biāo)題和ViewPager的聯(lián)動(dòng)的洁闰。
??順便提一下,OnChildViewHolderSelectedListener這個(gè)接口有兩個(gè)方法万细,分別為onChildViewHolderSelected和onChildViewHolderSelectedAndPositioned扑眉。那有什么區(qū)別呢?注釋里說赖钞,這個(gè)監(jiān)聽可能會(huì)改變子View的大小和位置腰素,所以如果想要獲取子View的布局位置的話,要重寫onChildViewHolderSelectedAndPositioned這個(gè)方法雪营,不過我這里暫時(shí)沒用到它弓千。

    private final OnChildViewHolderSelectedListener onChildViewHolderSelectedListener
            = new OnChildViewHolderSelectedListener() {
        @Override
        public void onChildViewHolderSelected(RecyclerView parent, RecyclerView.ViewHolder child,
            int position, int subposition) {
            super.onChildViewHolderSelected(parent, child, position, subposition);

            if (mOldTitle != null) {
                Paint paint = mOldTitle.getPaint();
                if (paint != null) {
                    paint.setFakeBoldText(false);
                    //viewpager切頁標(biāo)題不刷新,調(diào)用invalidate刷新
                    mOldTitle.invalidate();
                }
            }
            if (child != null) {

                TextView view = child.itemView.findViewById(R.id.tv_main_title);
                Paint paint = view.getPaint();
                if (paint != null) {
                    paint.setFakeBoldText(true);
                    //viewpager切頁標(biāo)題不刷新献起,調(diào)用invalidate刷新
                    view.invalidate();
                }
                mOldTitle = view;
            }
            Log.e(TAG, "onChildViewHolderSelected mViewPager != null: " + (mViewPager != null)
                    + " position:" + position);
            setCurrentItemPosition(position);

        }
    };

    private void setCurrentItemPosition(int position) {
        if (mViewPager != null && position != mCurrentPageIndex) {
            mViewPager.setCurrentItem(position);
            mCurrentPageIndex = position;
        }
    }

實(shí)現(xiàn)二 切換ViewPager洋访,標(biāo)題聯(lián)動(dòng)

??那如果想要切換ViewPager,讓標(biāo)題聯(lián)動(dòng)該如何實(shí)現(xiàn)呢谴餐?
??ViewPager有個(gè)切頁監(jiān)聽addOnPageChangeListener姻政,添加這個(gè)監(jiān)聽后,在切頁時(shí)岂嗓,會(huì)回調(diào)onPageSelected這個(gè)方法汁展,我就是在這個(gè)方法里調(diào)用了mHorizontalGridView.setSelectedPosition(position);進(jìn)行標(biāo)題的切換的,調(diào)用mHorizontalGridView.setSelectedPosition(position);這個(gè)方法后厌殉,標(biāo)題的這個(gè)監(jiān)聽onChildViewHolderSelectedListener就會(huì)執(zhí)行回調(diào)食绿。

   private void initViewPager(List<Title.DataBean> dataBeans) {
       mViewPagerAdapter = new ContentViewPagerAdapter(getSupportFragmentManager());
       mViewPagerAdapter.setData(dataBeans);
       mViewPager.setAdapter(mViewPagerAdapter);
       mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
           @Override
           public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

           }

           @Override
           public void onPageSelected(int position) {
               Log.e(TAG, "onPageSelected position: " + position);
               if (position != mCurrentPageIndex) {
                   mCurrentPageIndex = position;
                   mHorizontalGridView.setSelectedPosition(position);
               }
           }

           @Override
           public void onPageScrollStateChanged(int state) {
               Log.e(TAG, "onPageScrollStateChanged state: " + state);

           }
       });
   }
實(shí)現(xiàn)三 流式布局實(shí)現(xiàn)

??那每一個(gè)內(nèi)容頁的流式布局是如何實(shí)現(xiàn)的呢?
??每一個(gè)內(nèi)容頁都是一個(gè)Fragment公罕,而流式布局是Fragment里的VerticalGridView通過添加一個(gè)個(gè)ListRowPresenter實(shí)現(xiàn)的器紧。
??流式布局的每一行都是一個(gè)ListRowPresenter,也就是標(biāo)題TextView + 內(nèi)容HorizontalGridView的樣式楼眷。ListRowPresenter封裝了一個(gè)RowContainerView铲汪,RowContainerView是一個(gè)繼承自LinearLayout(垂直方向)的自定義View。如下所示摩桶,這個(gè)垂直方向的LinearLayout調(diào)用了addHeaderView和addRowView桥状,HeaderView就是標(biāo)題TextView帽揪,RowView就是內(nèi)容HorizontalGridView硝清。

RowPresenter.java

    static class ContainerViewHolder extends Presenter.ViewHolder {
        /**
         * wrapped row view holder
         */
        final ViewHolder mRowViewHolder;

        public ContainerViewHolder(RowContainerView containerView, ViewHolder rowViewHolder) {
            super(containerView);
            containerView.addRowView(rowViewHolder.view);
            if (rowViewHolder.mHeaderViewHolder != null) {
                containerView.addHeaderView(rowViewHolder.mHeaderViewHolder.view);
            }
            mRowViewHolder = rowViewHolder;
            mRowViewHolder.mContainerViewHolder = this;
        }
    }

??那怎么把一個(gè)個(gè)ListRowPresenter添加到VerticalGridView中呢?

??第一步 初始化

        mVerticalGridView = mRootView.findViewById(R.id.hg_content);   
        //設(shè)置垂直方向上的間距
        mVerticalGridView.setVerticalSpacing((int) getResources().getDimension(R.dimen.px48));
        //PresenterSelector使用ArrayMap存儲(chǔ)對象和Presenter转晰,使mAdapter添加的對象能夠找到與之對應(yīng)的布局芦拿,這使得數(shù)據(jù)層和表現(xiàn)層分離
        ContentPresenterSelector presenterSelector = new ContentPresenterSelector();
        mAdapter = new ArrayObjectAdapter(presenterSelector);
        //ItemBridgeAdapter是Presenter和RecyclerView.Adapter之間溝通的橋梁
        ItemBridgeAdapter itemBridgeAdapter = new ItemBridgeAdapter(mAdapter);
        mVerticalGridView.setAdapter(itemBridgeAdapter);

??第二步 創(chuàng)建HorizontalGridView的item的布局

    public class TypeOneContentPresenter extends Presenter {
        private Context mContext;
        private static final String TAG = "TypeOneContentPresenter";

        @Override
        public Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) {
            if (mContext == null) {
               mContext = parent.getContext();
            }
            View view = LayoutInflater.from(mContext).inflate(R.layout.item_type_one_layout, parent, false);
            return new ViewHolder(view);
        }

        @Override
        public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
            if (item instanceof Content.DataBean.WidgetsBean) {
                ViewHolder vh = (ViewHolder) viewHolder;
                Glide.with(mContext)
                        .load(((Content.DataBean.WidgetsBean) item).getUrl())
                        .apply(new RequestOptions()
                        .centerCrop()
                        .override((int) mContext.getResources().getDimension(R.dimen.px400),
                                    (int) mContext.getResources().getDimension(R.dimen.px222))
                            .placeholder(R.drawable.shape_default))
                        .into(vh.mIvTypeTwoPoster);
            }
        }

        @Override
        public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
        }

        public static class ViewHolder extends Presenter.ViewHolder {
            private final ImageView mIvTypeTwoPoster;
            public ViewHolder(View view) {
                super(view);
                mIvTypeTwoPoster = view.findViewById(R.id.iv_type_two_poster);
            }
        }
    }

??第三步 將ListRowPresenter添加到VerticalGridView
??下面以添加一個(gè)ListRowPresenter的代碼舉例士飒,添加多個(gè)ListRowPresenter就是多次執(zhí)行下面這些代碼而已。

        //TypeOneContentPresenter是水平的HorizontalGridView的Item的布局蔗崎,在里面進(jìn)行數(shù)據(jù)綁定酵幕,
        //有onCreateViewHolder、onBindViewHolder和onUnbindViewHolder三個(gè)方法缓苛,和RecyclerView的Adapter的三個(gè)方法的作用一樣

        //arrayObjectAdapterOne是水平的HorizontalGridView的數(shù)據(jù)    
        ArrayObjectAdapter arrayObjectAdapterOne = new ArrayObjectAdapter(new TypeOneContentPresenter());
        List<Content.DataBean.WidgetsBean> listOne = dataBean.getWidgets();
        if (listOne == null) {
            return;
        }        
        arrayObjectAdapterOne.addAll(0, listOne);
        //HeaderItem是標(biāo)題的對象芳撒,headerItem為null的話,不顯示標(biāo)題
        HeaderItem headerItem = null;
        if (dataBean.getShowTitle()) {
            //dataBean.getTitle()就是標(biāo)題顯示的字符串
            headerItem = new HeaderItem(dataBean.getTitle());
        }
        ListRow listRowOne = new ListRow(headerItem, arrayObjectAdapterOne);
        //mAdapter是VerticalGridView的ArrayObjectAdapter對象
        mAdapter.add(listRowOne);

??看完還覺得不清楚的話未桥,可以看看Demo里的ContentFragment這個(gè)類笔刹。

實(shí)現(xiàn)四 ViewPager禁止切頁

??ViewPager繼承自ViewGroup,所以根據(jù)事件分發(fā)機(jī)制我們知道冬耿,重寫ViewPager的dispatchKeyEvent就能夠攔截ViewPager的Key事件舌菜,那我們看看ViewPager的dispatchKeyEvent是怎么實(shí)現(xiàn)的。
??看ViewPager的源碼亦镶,ViewPager的dispatchKeyEvent返回了super.dispatchKeyEvent(event) || executeKeyEvent(event)日月,那我們看看super.dispatchKeyEvent(event)executeKeyEvent(event)都做了什么操作$凸牵看過executeKeyEvent(event)后爱咬,知道了按遙控器的左右鍵時(shí),pageLeft()pageRight()執(zhí)行setCurrentItem進(jìn)行切頁绊起,知道這個(gè)就好辦了台颠。我們自定義ViewPager,重寫pageLeft()pageRight()這兩個(gè)方法勒庄,讓他們不執(zhí)行setCurrentItem而是直接返回True就可以了串前。(注:我的Demo里是可以切頁的,Demo沒有攔截切頁)
??ViewPager禁止切頁修改方式如下所示实蔽。

    public class TabViewPager extends ViewPager {
    public static final String TAG = "TabViewPager";
    private final Rect mTempRect = new Rect();

    public TabViewPager(@NonNull Context context) {
        super(context);
    }

    public TabViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return super.dispatchKeyEvent(event) || executeKeyEvent(event);
    }

    public boolean executeKeyEvent(@NonNull KeyEvent event) {
        boolean handled = false;
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            switch (event.getKeyCode()) {
                case KeyEvent.KEYCODE_DPAD_LEFT:
                    handled = arrowScroll(FOCUS_LEFT);
                    break;
                case KeyEvent.KEYCODE_DPAD_RIGHT:
                    handled = arrowScroll(FOCUS_RIGHT);
                    break;
            }
        }
        return handled;
    }

    public boolean arrowScroll(int direction) {
        View currentFocused = findFocus();
        if (currentFocused == this) {
            currentFocused = null;
        } else if (currentFocused != null) {
            boolean isChild = false;
            for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
                 parent = parent.getParent()) {
                if (parent == this) {
                    isChild = true;
                    break;
                }
            }
            if (!isChild) {
                // This would cause the focus search down below to fail in fun ways.
                final StringBuilder sb = new StringBuilder();
                sb.append(currentFocused.getClass().getSimpleName());
                for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
                     parent = parent.getParent()) {
                    sb.append(" => ").append(parent.getClass().getSimpleName());
                }
                Log.e(TAG, "arrowScroll tried to find focus based on non-child "
                        + "current focused view " + sb.toString());
                currentFocused = null;
            }
        }

        boolean handled = false;

        View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
                direction);
        if (nextFocused != null && nextFocused != currentFocused) {
            if (direction == View.FOCUS_LEFT) {
                // If there is nothing to the left, or this is causing us to
                // jump to the right, then what we really want to do is page left.
                final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
                final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
                if (currentFocused != null && nextLeft >= currLeft) {
                    handled = pageLeft();
                } else {
                    handled = nextFocused.requestFocus();
                }
            } else if (direction == View.FOCUS_RIGHT) {
                // If there is nothing to the right, or this is causing us to
                // jump to the left, then what we really want to do is page right.
                final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
                final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
                if (currentFocused != null && nextLeft <= currLeft) {
                    handled = pageRight();
                } else {
                    handled = nextFocused.requestFocus();
                }
            }
        } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {
            // Trying to move left and nothing there; try to page.
            handled = pageLeft();
        } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {
            // Trying to move right and nothing there; try to page.
            handled = pageRight();
        }
        if (handled) {
            playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
        }
        return handled;
    }

    private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {
        if (outRect == null) {
            outRect = new Rect();
        }
        if (child == null) {
            outRect.set(0, 0, 0, 0);
            return outRect;
        }
        outRect.left = child.getLeft();
        outRect.right = child.getRight();
        outRect.top = child.getTop();
        outRect.bottom = child.getBottom();

        ViewParent parent = child.getParent();
        while (parent instanceof ViewGroup && parent != this) {
            final ViewGroup group = (ViewGroup) parent;
            outRect.left += group.getLeft();
            outRect.right += group.getRight();
            outRect.top += group.getTop();
            outRect.bottom += group.getBottom();

            parent = group.getParent();
        }
        return outRect;
    }

    boolean pageLeft() {
        return true;
    }

    boolean pageRight() {
        return true;
    }
}
實(shí)現(xiàn)五 邊界抖動(dòng)

??邊界抖動(dòng)的實(shí)現(xiàn)方式是:當(dāng)按某個(gè)方向鍵的時(shí)候荡碾,發(fā)現(xiàn)該方向上找到的的下一個(gè)View為null,那這時(shí)候給當(dāng)前焦點(diǎn)View執(zhí)行抖動(dòng)動(dòng)畫就行了局装。所以只要重寫dispatchKeyEvent就行了坛吁。具體代碼可以看Demo中的TabHorizontalGridView、TabVerticalGridView和TabViewPager這三個(gè)類铐尚。目前Demo中只在第一個(gè)頁面按左鍵和最后一個(gè)頁面按右鍵時(shí)添加了抖動(dòng)效果拨脉。

邊界抖動(dòng).gif
實(shí)現(xiàn)六 焦點(diǎn)View上劃過一道光

??其實(shí)這個(gè)效果就是一張圖片執(zhí)行了水平方向上的移動(dòng)動(dòng)畫。
??我的實(shí)現(xiàn)方式是自定義了ConstraintLayout宣增,當(dāng)ConstraintLayout獲取焦點(diǎn)時(shí)玫膀,將閃光圖片執(zhí)行一個(gè)屬性動(dòng)畫。所以爹脾,我所有的能獲取焦點(diǎn)的Item的根布局都是這個(gè)自定義的ConstraintLayout帖旨。具體代碼可以看Demo中的ImgConstraintLayout這個(gè)類箕昭。

焦點(diǎn)View上劃過一道光.gif
實(shí)現(xiàn)七 自定義標(biāo)題
自定義標(biāo)題樣式.png

??默認(rèn)的標(biāo)題RowHeaderPresenter只有文本樣式,如果想要在文本前加上圖片或者變成其它的樣式解阅,需要自定義RowHeaderPresenter落竹。下面說一下自定義的步驟。
??第一步货抄,新建一個(gè)ImageRowHeaderPresenter類述召,繼承自RowHeaderPresenter。

public class ImageRowHeaderPresenter extends RowHeaderPresenter {

    private final int mLayoutResourceId;
    private final boolean mAnimateSelect;

    public ImageRowHeaderPresenter() {
        this(R.layout.lb_img_row_header);
    }

    /**
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP_PREFIX)
    public ImageRowHeaderPresenter(int layoutResourceId) {
        this(layoutResourceId, true);
    }

    /**
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP_PREFIX)
    public ImageRowHeaderPresenter(int layoutResourceId, boolean animateSelect) {
        mLayoutResourceId = layoutResourceId;
        mAnimateSelect = animateSelect;
    }

    @Override
    public Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) {
        View root = LayoutInflater.from(parent.getContext())
                .inflate(mLayoutResourceId, parent, false);
        HeadViewHolder viewHolder =  new HeadViewHolder(root);
        if (mAnimateSelect) {
            setSelectLevel(viewHolder, 0);
        }
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
        HeaderItem headerItem = item == null ? null : ((Row) item).getHeaderItem();
        if (headerItem == null) {
            if ( viewHolder.view.findViewById(R.id.row_header) != null) {
                ((TextView)viewHolder.view.findViewById(R.id.row_header)).setText(null);
            }
            viewHolder.view.setContentDescription(null);
            viewHolder.view.setVisibility(View.GONE);

        } else {
            if (viewHolder.view.findViewById(R.id.row_header) != null) {
                ((TextView)viewHolder.view.findViewById(R.id.row_header)).setText(headerItem.getName());
            }
            viewHolder.view.setContentDescription(headerItem.getContentDescription());
            viewHolder.view.setVisibility(View.VISIBLE);
        }
    }

    public static class HeadViewHolder extends ViewHolder {

        public HeadViewHolder(View view) {
            super(view);

        }
    }
}

??第二步蟹地,調(diào)用setHeaderPresenter設(shè)置自定義標(biāo)題樣式桨武。

        TypeFiveListRowPresenter listRowPresenterFive = new TypeFiveListRowPresenter();
        listRowPresenterFive.setShadowEnabled(false);
        listRowPresenterFive.setSelectEffectEnabled(false);
        listRowPresenterFive.setKeepChildForeground(false);
        listRowPresenterFive.setHeaderPresenter(new ImageRowHeaderPresenter());
        addClassPresenter(ListRow.class, listRowPresenterFive, TypeFiveContentPresenter.class);
實(shí)現(xiàn)八 已安裝應(yīng)用

?? 已安裝應(yīng)用列表展示已經(jīng)安裝的所有應(yīng)用并能點(diǎn)擊打開應(yīng)用。獲取焦點(diǎn)時(shí)锈津,圖片上添加了焦點(diǎn)框呀酸,應(yīng)用名過長時(shí),跑馬燈滾動(dòng)展示琼梆。學(xué)習(xí)VerticalGridView的基本使用時(shí)可以參考這個(gè)頁面性誉。

已安裝應(yīng)用.jpg
實(shí)現(xiàn)九 一張大圖多張小圖樣式

?? 有人問怎么實(shí)現(xiàn)一張大圖多張小圖樣式,我就花時(shí)間寫了一種實(shí)現(xiàn)方式茎杂。其實(shí)也就是模擬ListRowPresenter的思想寫的错览,但沒有進(jìn)行封裝,寫的不優(yōu)雅煌往。如果大家有更好的實(shí)現(xiàn)方式倾哺,望指教。
?? 新類型的主要實(shí)現(xiàn)類請參考:TypeSevenPresenter.java

一張大圖多張小圖.png

結(jié)束語

??寫代碼的修行路刽脖,道阻且長羞海。
??如果我有寫的不好的地方或者錯(cuò)誤的地方,還請批評指正曲管。
??如果此文對您稍微有點(diǎn)用却邓,希望您能在簡書點(diǎn)個(gè)贊,并在Github點(diǎn)個(gè)星院水。謝謝腊徙!
??Demo地址:https://github.com/iSuperRed/LeanbackTvSample.git

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市檬某,隨后出現(xiàn)的幾起案子撬腾,更是在濱河造成了極大的恐慌,老刑警劉巖恢恼,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件民傻,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)饰潜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來和簸,“玉大人彭雾,你說我怎么就攤上這事∷#” “怎么了薯酝?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長爽柒。 經(jīng)常有香客問我吴菠,道長,這世上最難降的妖魔是什么浩村? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任做葵,我火速辦了婚禮,結(jié)果婚禮上心墅,老公的妹妹穿的比我還像新娘酿矢。我一直安慰自己,他們只是感情好怎燥,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布瘫筐。 她就那樣靜靜地躺著,像睡著了一般铐姚。 火紅的嫁衣襯著肌膚如雪策肝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天隐绵,我揣著相機(jī)與錄音之众,去河邊找鬼。 笑死依许,一個(gè)胖子當(dāng)著我的面吹牛酝枢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播悍手,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼帘睦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了坦康?” 一聲冷哼從身側(cè)響起竣付,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎滞欠,沒想到半個(gè)月后古胆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年逸绎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了惹恃。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡棺牧,死狀恐怖巫糙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情颊乘,我是刑警寧澤参淹,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站乏悄,受9級(jí)特大地震影響浙值,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望序臂。 院中可真熱鬧,春花似錦负蚊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至冕茅,卻和暖如春伤极,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背姨伤。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工哨坪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人乍楚。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓当编,卻偏偏與公主長得像,于是被迫代替她去往敵國和親徒溪。 傳聞我的和親對象是個(gè)殘疾皇子忿偷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容