前言

这里主要记录几个TV问题的解决方案,如果对这个不感兴趣的其实就不用往下看了。传送门

这几天有一个需求就是要求出一个TV版本的app,之前没有具体的了解Tv版的app有手机端的app到底有什么区别,因此就做了一下研究,写了些Demo,在做的过程中确实出现了好几个问题。一开始碰到这些问题时,浅尝辄止的试了试,发现很多都没有解决方案,本着外事问google的,search了一把,也没有结果,可能是TV做的人比较少,网上搜出来的都是照着谷歌官方的样例实现了一把而已,因此就仔细的研究了一下这些问题,这里把解决这些问题的方案描述出来,希望其他人能少走弯路,如果有更好的解决方案也希望大家探讨分享。

样例

这里我们做了一个demo,demo界面如下,以下的图都是最终运行后截出的图,由于是在模拟器上截图,导致了控件加载不完全,但是在真是的机顶盒上是没有问题的,以下的图将就着看吧:


开发过程

虽然google官方写的是手机app不用做太多改动就可以运行在Tv上,但是终究两种还是有部分区别的,这里还是要针对TV版做部分设置。
       首先是配置文件的改动,需要在AndroidManifest中配置如下属性:

<uses-feature
    android:name="android.hardware.touchscreen"android:required="false"/>
<uses-feature
    android:name="android.software.leanback"android:required="true"/>

同时还需要配置一个action为android.intent.action.MAIN,category为android.intent.category.LEANBACK_LAUNCHER的Activity,类似如下:

<activity
    android:name="im.yixin.home.HomeActivity"android:label="@string/app_name"android:screenOrientation="landscape"><intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LEANBACK_LAUNCHER"/></intent-filter>
</activity>

如果记不住上面需要配置的内容其实也没有关系,可以新创建一个TV工程,默认创建的TV工程就已经包含了上述的配置,并且该工程就相当于一个demo了,是可以直接运行的一个工程,里面包含了Tv开发的很多控件,如果你要学习这也是很好的学习资料了,其实后续的内容也是根据这里的内容进行参照学习的。

这里附带一句,Android的sdk中的samples中的tv样例程序直接导入是运行不起来的,需要修改很多东西,但是实质内容与新创建的工程没有什么区别,因此也可以不用导入样例程序进行学习了。

根据前面的样例图,主界面配置页面如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@drawable/global_bg"android:orientation="vertical"android:paddingLeft="42dp"android:paddingRight="42dp"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="@dimen/gap_86_dp"android:clickable="true"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerVertical="true"android:drawableLeft="@drawable/tv_logo"android:drawablePadding="@dimen/gap_8_dp"android:gravity="center"android:text="@string/itv_name"android:textColor="@color/white"android:textSize="@dimen/text_size_20"/><TextViewandroid:id="@+id/settings_tab"android:layout_width="wrap_content"android:layout_height="match_parent"android:layout_alignParentRight="true"android:layout_marginRight="@dimen/gap_45_dp"android:background="@drawable/navigation_tab_bar_selector"android:focusable="true"android:gravity="center"android:text="@string/setting"android:textColor="@color/navigation_text_selector"android:textSize="@dimen/text_size_20"/><TextViewandroid:id="@+id/contact_tab"android:layout_width="wrap_content"android:layout_height="match_parent"android:layout_marginRight="@dimen/gap_45_dp"android:layout_toLeftOf="@id/settings_tab"android:background="@drawable/navigation_tab_bar_selector"android:focusable="true"android:gravity="center"android:text="@string/contact"android:textColor="@color/navigation_text_selector"android:textSize="@dimen/text_size_20"/><TextViewandroid:id="@+id/dial_tab"android:layout_width="wrap_content"android:layout_height="match_parent"android:layout_marginRight="@dimen/gap_65_dp"android:layout_toLeftOf="@id/contact_tab"android:background="@drawable/navigation_tab_bar_selector"android:focusable="true"android:gravity="center"android:text="@string/dial"android:textColor="@color/navigation_text_selector"android:textSize="@dimen/text_size_20"/></RelativeLayout><Viewandroid:layout_width="match_parent"android:layout_height="1px"android:layout_marginBottom="@dimen/gap_50_dp"android:background="@color/gray1"/><FrameLayoutandroid:id="@+id/tab_container"android:layout_width="match_parent"android:layout_height="match_parent"></FrameLayout>
</LinearLayout>

界面的代码如下:

public class HomeActivity extends Activity implements View.OnClickListener {public static void start(Context context) {Intent intent = new Intent(context, HomeActivity.class);context.startActivity(intent);}private static final String[] TAGS = {"dial", "contact", "my"};private FragmentManager manager;private int showTabIndex = -1;private TextView dialTab;private TextView contactTab;private TextView myTab;@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_home);findViews();setViewsListener();init();selectTab(0);}private void findViews() {dialTab = (TextView) findViewById(R.id.dial_tab);contactTab = (TextView) findViewById(R.id.contact_tab);myTab = (TextView) findViewById(R.id.settings_tab);}private void setViewsListener() {dialTab.setOnClickListener(this);contactTab.setOnClickListener(this);myTab.setOnClickListener(this);}private void init() {manager = getFragmentManager();}private void selectTab(int index) {if (index == showTabIndex) {return;}dialTab.setSelected(index == 0);contactTab.setSelected(index == 1);myTab.setSelected(index == 2);FragmentTransaction transaction = manager.beginTransaction();hideFragment(showTabIndex, transaction);showTabIndex = index;showFragment(showTabIndex, transaction);transaction.commit();}private void hideFragment(int tabIndex, FragmentTransaction transaction) {Fragment fragment = getFragmentByIndex(tabIndex);if (fragment != null) {transaction.hide(fragment);}}private Fragment getFragmentByIndex(int index) {if (index >= 0 && index < TAGS.length) {return manager.findFragmentByTag(TAGS[index]);}return null;}private void showFragment(int tabIndex, FragmentTransaction transaction) {Fragment fragment = getFragmentByIndex(tabIndex);if (fragment == null) {switch (tabIndex) {case 0:fragment = new DialFragment();break;case 1:/*  fragment = new ContactFragment();*/fragment = new VerticalGridFragment();break;case 2:fragment = new MyFragment();break;}transaction.add(R.id.tab_container, fragment, TAGS[tabIndex]);//transaction.addToBackStack(TAGS[tabIndex]);} else {transaction.show(fragment);}}@Overridepublic void onClick(View v) {switch (v.getId()) {case R.id.dial_tab:selectTab(0);return;case R.id.contact_tab:selectTab(1);return;case R.id.settings_tab:selectTab(2);//                VerticalGridActivity.start(this);return;}}}

该界面主要采用Fragment来实现三个界面,分别为拨号页面,好友,设置界面,其中拨号界面又包含两个子的Fragment,我们来继续看看拨号界面与好友界面,设置界面是一个充数的界面啥都没有做。

首先来看看拨号界面的配置代码:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/transparent"android:orientation="horizontal"tools:context="im.yixin.home.dial.DialFragment"><FrameLayout
        android:id="@+id/dial_pan"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"></FrameLayout><FrameLayout
        android:id="@+id/contact_pan"android:layout_width="0dp"android:layout_height="match_parent"android:layout_marginLeft="@dimen/gap_12_dp"android:layout_weight="3"></FrameLayout>
</LinearLayout>

对应的界面代码如下:

public class DialFragment extends Fragment{public DialFragment() {// Required empty public constructor}/*** Use this factory method to create a new instance of* this fragment using the provided parameters.** @return A new instance of fragment DialFragment.*/public static DialFragment newInstance() {DialFragment fragment = new DialFragment();return fragment;}@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);}@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {return inflater.inflate(R.layout.fragment_dial, container, false);}@Overridepublic void onActivityCreated(Bundle savedInstanceState) {super.onActivityCreated(savedInstanceState);addFragments();;}private void addFragments() {FragmentTransaction transaction = getChildFragmentManager().beginTransaction();transaction.replace(R.id.dial_pan, new DialPanFragment());VerticalGridFragment fragment = new VerticalGridFragment();Bundle args = new Bundle();args.putInt(Extra.COLUMNS, Extra.DIAL_COLUMNS);fragment.setArguments(args);transaction.replace(R.id.contact_pan, fragment);transaction.commit();}}

拨号界面被分成了两部分,一部分为拨号盘,一部分为联系人,分别占据了屏幕一份和三份,右边的联系人与主界面的好用共用了同一个Fragment,因此这里我们再看看接下来的两个界面,首先我们看看拨号盘的界面代码。

由于只做展示,因此代码写的很粗糙,界面直接写了N个按钮的代码,配置界面如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/white_35_transparent"android:clickable="true"android:contextClickable="true"android:orientation="vertical"tools:context="im.yixin.home.dial.DialFragment"><ImageView
        android:id="@+id/dial_icon"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentBottom="true"android:layout_centerHorizontal="true"android:layout_marginBottom="@dimen/gap_20_dp"android:focusable="true"android:padding="@dimen/gap_20_dp"android:src="@drawable/tv_call_btn_selector"/><LinearLayout
        android:id="@+id/input_num_line_1"android:layout_width="fill_parent"android:layout_height="wrap_content"android:layout_above="@id/dial_icon"android:baselineAligned="false"android:orientation="horizontal"><RelativeLayout
            android:layout_width="0dp"android:layout_height="80dp"android:layout_weight="1"android:background="@drawable/keyboard_item_selector"><ImageView
                android:id="@+id/input_key_number_null"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerInParent="true"android:contentDescription="@string/empty"/></RelativeLayout><RelativeLayout
            android:layout_width="0dp"android:layout_height="80dp"android:layout_weight="1"><TextView
                android:id="@+id/input_key_number_0"android:layout_width="60dp"android:layout_height="60dp"android:layout_centerInParent="true"android:background="@drawable/keyboard_item_selector"android:focusable="true"android:gravity="center"android:text="0"android:textColor="#ffffff"android:textSize="30sp"/></RelativeLayout><RelativeLayout
            android:layout_width="0dp"android:layout_height="80dp"android:layout_weight="1"><ImageView
                android:id="@+id/input_key_number_del"android:layout_width="60dp"android:layout_height="60dp"android:layout_centerInParent="true"android:background="@drawable/keyboard_item_selector"android:contentDescription="@string/empty"android:focusable="true"android:scaleType="center"android:src="@drawable/tv_del"/></RelativeLayout></LinearLayout><LinearLayout
        android:id="@+id/input_num_line_2"android:layout_width="fill_parent"android:layout_height="wrap_content"android:layout_above="@+id/input_num_line_1"android:baselineAligned="false"android:orientation="horizontal"><RelativeLayout
            android:layout_width="0dp"android:layout_height="80dp"android:layout_weight="1"><TextView
                android:id="@+id/input_key_number_7"android:layout_width="60dp"android:layout_height="60dp"android:layout_centerInParent="true"android:background="@drawable/keyboard_item_selector"android:focusable="true"android:gravity="center"android:text="7"android:textColor="#ffffff"android:textSize="30sp"/></RelativeLayout><RelativeLayout
            android:layout_width="0dp"android:layout_height="80dp"android:layout_weight="1"><TextView
                android:id="@+id/input_key_number_8"android:layout_width="60dp"android:layout_height="60dp"android:layout_centerInParent="true"android:background="@drawable/keyboard_item_selector"android:focusable="true"android:gravity="center"android:text="8"android:textColor="#ffffff"android:textSize="30sp"/></RelativeLayout><RelativeLayout
            android:layout_width="0dp"android:layout_height="80dp"android:layout_weight="1"><TextView
                android:id="@+id/input_key_number_9"android:layout_width="60dp"android:layout_height="60dp"android:layout_centerInParent="true"android:background="@drawable/keyboard_item_selector"android:focusable="true"android:gravity="center"android:text="9"android:textColor="#ffffff"android:textSize="30sp"/></RelativeLayout></LinearLayout><LinearLayout
        android:id="@+id/input_num_line_3"android:layout_width="fill_parent"android:layout_height="wrap_content"android:layout_above="@+id/input_num_line_2"android:baselineAligned="false"android:orientation="horizontal"><RelativeLayout
            android:layout_width="0dp"android:layout_height="80dp"android:layout_weight="1"><TextView
                android:id="@+id/input_key_number_4"android:layout_width="60dp"android:layout_height="60dp"android:layout_centerInParent="true"android:background="@drawable/keyboard_item_selector"android:focusable="true"android:gravity="center"android:text="4"android:textColor="#ffffff"android:textSize="30sp"/></RelativeLayout><RelativeLayout
            android:layout_width="0dp"android:layout_height="80dp"android:layout_weight="1"><TextView
                android:id="@+id/input_key_number_5"android:layout_width="60dp"android:layout_height="60dp"android:layout_centerInParent="true"android:background="@drawable/keyboard_item_selector"android:focusable="true"android:gravity="center"android:text="5"android:textColor="#ffffff"android:textSize="30sp"/></RelativeLayout><RelativeLayout
            android:layout_width="0dp"android:layout_height="80dp"android:layout_weight="1"><TextView
                android:id="@+id/input_key_number_6"android:layout_width="60dp"android:layout_height="60dp"android:layout_centerInParent="true"android:background="@drawable/keyboard_item_selector"android:focusable="true"android:gravity="center"android:text="6"android:textColor="#ffffff"android:textSize="30sp"/></RelativeLayout></LinearLayout><LinearLayout
        android:id="@+id/input_num_line_4"android:layout_width="fill_parent"android:layout_height="wrap_content"android:layout_above="@+id/input_num_line_3"android:baselineAligned="false"android:orientation="horizontal"><RelativeLayout
            android:layout_width="0dp"android:layout_height="80dp"android:layout_weight="1"><TextView
                android:id="@+id/input_key_number_1"android:layout_width="60dp"android:layout_height="60dp"android:layout_centerInParent="true"android:background="@drawable/keyboard_item_selector"android:focusable="true"android:gravity="center"android:text="1"android:textColor="#ffffff"android:textSize="30sp"/></RelativeLayout><RelativeLayout
            android:layout_width="0dp"android:layout_height="80dp"android:layout_weight="1"><TextView
                android:id="@+id/input_key_number_2"android:layout_width="60dp"android:layout_height="60dp"android:layout_centerInParent="true"android:background="@drawable/keyboard_item_selector"android:focusable="true"android:gravity="center"android:text="2"android:textColor="#ffffff"android:textSize="30sp"/></RelativeLayout><RelativeLayout
            android:layout_width="0dp"android:layout_height="80dp"android:layout_weight="1"><TextView
                android:id="@+id/input_key_number_3"android:layout_width="60dp"android:layout_height="60dp"android:layout_centerInParent="true"android:background="@drawable/keyboard_item_selector"android:focusable="true"android:gravity="center"android:text="3"android:textColor="#ffffff"android:textSize="30sp"/></RelativeLayout></LinearLayout><TextView
        android:id="@+id/show_phone"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_above="@+id/input_num_line_4"android:layout_centerInParent="true"android:padding="@dimen/gap_20_dp"android:textColor="#ffffff"android:textSize="33sp"/>
</RelativeLayout>

对应的界面代码如下:


public class DialPanFragment extends Fragment implements View.OnClickListener {private TextView showPhone;private ImageView dialBnt;public DialPanFragment() {// Required empty public constructor}@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {return inflater.inflate(R.layout.fragment_dial_pan, container, false);}@Overridepublic void onActivityCreated(Bundle savedInstanceState) {super.onActivityCreated(savedInstanceState);findViews();}private void findViews() {showPhone = (TextView) getView().findViewById(R.id.show_phone);dialBnt = (ImageView) getView().findViewById(R.id.dial_icon);dialBnt.setOnClickListener(this);dialBnt.setTag(-2);dialBnt.setEnabled(false);View view0 = getView().findViewById(R.id.input_key_number_0);view0.setTag(0);view0.setOnClickListener(this);View view1 = getView().findViewById(R.id.input_key_number_1);view1.setTag(1);view1.setOnClickListener(this);view1.setNextFocusUpId(R.id.dial_tab);View view2 = getView().findViewById(R.id.input_key_number_2);view2.setTag(2);view2.setOnClickListener(this);view2.setNextFocusUpId(R.id.dial_tab);View view3 = getView().findViewById(R.id.input_key_number_3);view3.setTag(3);view3.setOnClickListener(this);view3.setNextFocusUpId(R.id.dial_tab);View view4 = getView().findViewById(R.id.input_key_number_4);view4.setTag(4);view4.setOnClickListener(this);View view5 = getView().findViewById(R.id.input_key_number_5);view5.setTag(5);view5.setOnClickListener(this);View view6 = getView().findViewById(R.id.input_key_number_6);view6.setTag(6);view6.setOnClickListener(this);View view7 = getView().findViewById(R.id.input_key_number_7);view7.setTag(7);view7.setOnClickListener(this);View view8 = getView().findViewById(R.id.input_key_number_8);view8.setTag(8);view8.setOnClickListener(this);View view9 = getView().findViewById(R.id.input_key_number_9);view9.setTag(9);view9.setOnClickListener(this);View viewDel = getView().findViewById(R.id.input_key_number_del);viewDel.setTag(-1);viewDel.setOnClickListener(this);}@Overridepublic void onClick(View v) {int tag = (int) v.getTag();if (tag == -2) {dial();} else if (tag == -1) {// DELdelNumber();} else {inputNumber(tag);}}private void delNumber() {String text = showPhone.getText().toString();if (text != null && text.length() > 0) {text = text.substring(0, text.length() - 1);showPhone.setText(text);}dialBtnState(text);}private void inputNumber(int tag) {String text = showPhone.getText().toString();if (text == null) {text = new String(String.valueOf(tag));} else {text = text + tag;}dialBtnState(text);showPhone.setText(text);}private void dial() {String text = showPhone.getText().toString();int len = TextUtils.isEmpty(text) ? 0 : text.length();if (len != 11) {ToastUtil.showToast("你输入的账号不合法!");showPhone.setText("");} else {String uid = ContactProvider.getUidByPhone(text);if (TextUtils.isEmpty(uid)) {ToastUtil.showToast("该账号不存在!");} else {// TODO}}}private void dialBtnState(String text) {dialBnt.setEnabled(!TextUtils.isEmpty(text));}
}

最后我们再来看看好友界面,改界面本地是没有xml的,因此我们直接来看看代码:

这里将使用到数据bean,与数据源的代码也贴出来如下:

public class Contact implements Parcelable {private String phone;private int headResId;private String name;public String getPhone() {return phone;}public void setPhone(String phone) {this.phone = phone;}public int getHeadResId() {return headResId;}public void setHeadResId(int headResId) {this.headResId = headResId;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Contact() {}public Contact(Parcel in) {phone = in.readString();headResId = in.readInt();name = in.readString();}public int describeContents() {return 0;}@Overridepublic void writeToParcel(Parcel dest, int flags) {dest.writeString(phone);dest.writeInt(headResId);dest.writeString(name);}@Overridepublic String toString() {StringBuilder sb = new StringBuilder(200);sb.append("Contact{");sb.append("phone='" + phone + '\'');sb.append(", headResId='" + headResId + '\'');sb.append(", name='" + name + '\'');sb.append('}');return sb.toString();}public static final Creator CREATOR = new Creator() {public Contact createFromParcel(Parcel in) {return new Contact(in);}public Contact[] newArray(int size) {return new Contact[size];}};
}//
public class ContactProvider {private static List<Contact> contactList;private static Context sContext;private static int[] head = {R.drawable.avater1, R.drawable.avater2, R.drawable.avater3, R.drawable.avater4, R.drawable.avater5, R.drawable.avater6, R.drawable.avater7, R.drawable.avater8, R.drawable.avater9, R.drawable.avater10, R.drawable.avater11, R.drawable.avater12};private static String[] names = {"梦洁", "雅静", "韵寒", "莉姿", "沛玲", "欣妍", "歆瑶", "凌菲", "靖瑶", "瑾萱", "芳蕤", "若华"};private static String[] phones = {"18618188630", "18158103936", "18620145337", "15116333186", "18618188630","18158103936", "18620145337", "15116333186", "18618188630", "18158103936", "18620145337", "18767106408"};public static void setContext(Context context) {if (sContext == null)sContext = context;}public static List<Contact> getContactList() {buildContact();return contactList;}public static List<Contact> buildContact() {if (null != contactList) {return contactList;}contactList = new ArrayList<Contact>();for (int i = 0; i < 12; ++i) {contactList.add(buildContactInfo(phones[i], names[i], head[i]));}return contactList;}private static Contact buildContactInfo(String phone, String name, int resId) {Contact contact = new Contact();contact.setPhone(phone);contact.setName(name);contact.setHeadResId(resId);return contact;}
}
/** VerticalGridFragment shows a grid of videos*/
public class VerticalGridFragment extends android.support.v17.leanback.app.VerticalGridFragment {private static final String TAG = "VerticalGridFragment";private static final int DEFAULT_COLUMNS = 4;private int numColumns = DEFAULT_COLUMNS;private ArrayObjectAdapter mAdapter;@Overridepublic void onCreate(Bundle savedInstanceState) {Log.d(TAG, "onCreate");super.onCreate(savedInstanceState);//setTitle(getString(R.string.vertical_grid_title));getParams();setupFragment();}@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {View root = super.onCreateView(inflater, container, savedInstanceState);return root;}private void setupFragment() {VerticalGridPresenter gridPresenter = new VerticalGridPresenter(FocusHighlight.ZOOM_FACTOR_NONE);gridPresenter.setNumberOfColumns(numColumns);//gridPresenter.setShadowEnabled(false);setGridPresenter(gridPresenter);mAdapter = new ArrayObjectAdapter(new ContactPresenter());List<Contact> contacts = ContactProvider.getContactList();mAdapter.addAll(0, contacts);setAdapter(mAdapter);setOnItemViewClickedListener(new ItemViewClickedListener());setOnItemViewSelectedListener(new ItemViewSelectedListener());}public void getParams() {if (getArguments() != null) {numColumns = getArguments().getInt(Extra.COLUMNS);}}private final class ItemViewClickedListener implements OnItemViewClickedListener {@Overridepublic void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolderrowViewHolder, Row row) {if (item instanceof Contact) {Contact contact = (Contact) item;// TODO}}}private final class ItemViewSelectedListener implements OnItemViewSelectedListener {@Overridepublic void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolderrowViewHolder, Row row) {}}}

在Fragment中我们自己实现了一个ContactPresenter,该Presenter是仿照官方的CardPresenter,但是CardPresenter中使用的ImageCardView是系统support包中提供的控件,而ContactPresenter中使用的是自己自定义的控件, 代码如下:

public class ContactPresenter extends Presenter {private static final String TAG = "CardPresenter";private static int sSelectedBackgroundColor;private static int sDefaultBackgroundColor;@Overridepublic ViewHolder onCreateViewHolder(ViewGroup parent) {Log.d(TAG, "onCreateViewHolder");sDefaultBackgroundColor = parent.getResources().getColor(R.color.white_35_transparent);sSelectedBackgroundColor = parent.getResources().getColor(R.color.white_60_transparent);ContactView contactView = new ContactView(parent.getContext()) {@Overridepublic void setSelected(boolean selected) {updateCardBackgroundColor(this, selected);super.setSelected(selected);}};contactView.setFocusable(true);contactView.setFocusableInTouchMode(true);updateCardBackgroundColor(contactView, false);return new ViewHolder(contactView);}private static void updateCardBackgroundColor(ContactView view, boolean selected) {int color = selected ? sSelectedBackgroundColor : sDefaultBackgroundColor;view.setBackgroundColor(color);//view.findViewById(R.id.info_field).setBackgroundColor(color);}@Overridepublic void onBindViewHolder(ViewHolder viewHolder, Object item) {Contact contact = (Contact) item;ContactView contactView = (ContactView) viewHolder.view;Log.d(TAG, "onBindViewHolder");contactView.setHead(contact.getHeadResId());contactView.setName(contact.getName());contactView.setPhone(contact.getPhone());}@Overridepublic void onUnbindViewHolder(ViewHolder viewHolder) {Log.d(TAG, "onUnbindViewHolder");ContactView contactView = (ContactView) viewHolder.view;// Remove references to images so that the garbage collector can free up memorycontactView.setHead(0);}
}

ContactView是一个继承自LinearLayout的自定义控件,包含了一个ImageView和两个TextView。

到此整个界面的代码就完成了,接下来我们来看看遇到的问题。

注意事项

事项1
       在VerticalGridFragment中一定要记得在onViewCreated函数之前调用setGridPresenter函数,因为之后再onViewCreated中使用到了setGridPresenter中传入的VerticalGridPresenter.

事项2
       在构造ArrayObjectAdapter时,如果你的界面只有一种view类型,则调用如下的构造函数:

    public ArrayObjectAdapter(Presenter presenter) {super(presenter);}

但是如果你是多view类型,你就必须要调用传入PresenterSelector的构造函数:

  public ArrayObjectAdapter(PresenterSelector presenterSelector) {super(presenterSelector);}

PresenterSelector是一个抽象类,因此需要你进行实现,在getPresenter跟数据类型,返回不同的Presenter

public abstract class PresenterSelector {/*** Returns a presenter for the given item.*/public abstract Presenter getPresenter(Object item);/*** Returns an array of all possible presenters.  The returned array should* not be modified.*/public Presenter[] getPresenters() {return null;}
}

问题列表

问题1:控件遥控器不能选中,不能导航

出现这种问题往往是控件没有设置android:focusable=”true”属性,只有默认能够选中焦点的才不需要设置改属性,比如Button,EditText。

问题2:控件选中后,看不出选中效果

由于默认选中是没有视觉效果的,因此你需要对控件设置选中效果,比如说背景图片,以前在手机上可能只需要设置selector中的pressed属性,或者selected属性,现在针对TV你必须要设置focused属性,比如拨号键盘选中后会出现一个圆形的选中背景框, 如下:

要实现上述效果,因此对每一键盘输入按钮添加如下的selector。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:drawable="@drawable/key_board_hover" android:state_focused="true"></item><item android:drawable="@drawable/key_board_hover" android:state_pressed="true"></item><item android:drawable="@drawable/key_board_hover" android:state_checked="true"></item><item android:drawable="@color/transparent"></item></selector>key_board_hover.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"android:shape="oval"><solid android:color="@color/white_35_transparent"></solid><size
        android:width="40dp"android:height="40dp"/>
</shape>

问题3:TV launcher中没有入口图标

如果需要出现入口图标,你必须要在AndroidManifest中配置action为android.intent.action.MAIN,category为android.intent.category.LAUNCHER的Activity。该配置与上面的LEANBACK_LAUNCHER不冲突,可以对入口Activity配置LAUNCHER,之后一个页面配置LEANBACK_LAUNCHER,配置如下:

<activity
    android:name=".WelcomeActivity"android:label="@string/app_name"android:screenOrientation="landscape"><intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LEANBACK_LAUNCHER"/></intent-filter>
</activity>

问题4:TV launcher中的图标不清晰,太糊

如果直接将手机app的launcher图标直接使用到TV中,则图标会拉伸,由于TV的图标往往都比较大,拉伸后就会变糊,因此需要重新切launcher图标,手机里面是48*48, 72*72,96*96等,而tv需要更大的尺寸,虽然没有在官方找到建议的尺寸,但是这里推荐一个尺寸180*180,可以多个文件夹都放同一个图标,这样界面加载的图标就会变得清晰。

问题5:遥控器导航下一个不是自己希望导航的控件

系统中如果界面中有多个可focus的控件,上下左右导航,则会找到与当前控件最邻近的控件作为下一个选中的控件,因此如果你确切想指定下一个导航的控件,则可以指定下一个控件的ID,只要该id在当前显示的界面中,比如向上 view1.setNextFocusUpId(R.id.dial_tab);

问题6:官方VerticalGridFragment加载后,默认选中第一个,但是第一个占据了整个界面。

该问题应该是官方的一个bug,如果不是第一次加载VerticalGridFragment,则不会出现该问题,并且我尝试了多个版本的,都会出现该问题,原因是选中后系统会在在选中的控件后插入两帧NonOverlappingView,插入的布局代码如下:

<merge xmlns:android="http://schemas.android.com/apk/res/android"><android.support.v17.leanback.widget.NonOverlappingViewandroid:id="@+id/lb_shadow_normal"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@drawable/lb_card_shadow_normal" /><android.support.v17.leanback.widget.NonOverlappingViewandroid:id="@+id/lb_shadow_focused"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@drawable/lb_card_shadow_focused"android:alpha="0" /></merge>

该布局插入了两帧NonOverlappingView,每一帧都使用了一个.9图标作为背景,而当系统第一次加载时,最终第一个选中的控件宽高计算错误,计算成了一个16777211类似的一个值,远远超出了界面的大小,解决方案如下:

方案1,将布局中的match_parent改为wrap_content
方案2,对VerticalGridFragment中使用的VerticalGridPresenter设置ShadowEnabled,如gridPresenter.setShadowEnabled(false);
方案3,替换掉.9图片

问题7:VerticalGridFragment加载后,选中放大效果不居中

在VerticalGridFragment,如果ArrayObjectAdapter使用的是自己实现的Presenter,而Presenter使用的不是系统提供的ImageCardView,则会导致选中效果不居中,当选中效果放大后会向右向下覆盖,而不是在当前位置放大覆盖四周。

该问题,我查了对应的style、只有针对ImageCardView的style,我也还没有仔细研究怎么调整,不过这里给出一个避免的方案,对VerticalGridPresenter选中后的高亮效果选择为不放大,如new VerticalGridPresenter(FocusHighlight.ZOOM_FACTOR_NONE)。

问题8:VerticalGridFragment顶层控件不能向上导航

比如在联系人列表页第一行时,遥控器向上不能导航,比如不能导航到拨号,好友控件,该问题其实是被系统给拦截了。系统的VerticalGridFragment加载了lb_vertical_grid_fragment布局,该布局包含了一个BrowseFrameLayout,对
BrowseFrameLayout设置了setOnFocusSearchListener。如下:

    private void setupFocusSearchListener() {BrowseFrameLayout browseFrameLayout = (BrowseFrameLayout) getView().findViewById(R.id.grid_frame);browseFrameLayout.setOnFocusSearchListener(getTitleHelper().getOnFocusSearchListener());}

当系统在VerticalGridPresenter最顶层时,向上找最近一个控件时,发现当前布局已经没有控件,则会向父布局查找,代码如下:

public View focusSearch(View focused, int direction) {if (isRootNamespace()) {// root namespace means we should consider ourselves the top of the// tree for focus searching; otherwise we could be focus searching// into other tabs.  see LocalActivityManager and TabHost for more inforeturn FocusFinder.getInstance().findNextFocus(this, focused, direction);} else if (mParent != null) {return mParent.focusSearch(focused, direction);}return null;
}

而VerticalGridPresenter的父布局则是BrowseFrameLayout,因此最终执行的是上面设置的getTitleHelper().getOnFocusSearchListener(),我们去看看改listener:

 private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =new BrowseFrameLayout.OnFocusSearchListener() {@Overridepublic View onFocusSearch(View focused, int direction) {if (focused != mTitleView && direction == View.FOCUS_UP) {return mTitleView;}final boolean isRtl = ViewCompat.getLayoutDirection(focused) ==View.LAYOUT_DIRECTION_RTL;final int forward = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;if (mTitleView.hasFocus() && direction == View.FOCUS_DOWN || direction == forward) {return mSceneRoot;}return null;}
};

发现问题所在没有,当focused != mTitleView && direction == View.FOCUS_UP时,强制指定了mTitleView,就算没有没有显示title,效果也一样。我认为这应该算系统的一个bug,那怎么解决呐?

我们可以重写一个一模一样的lb_vertical_grid_fragment,自己写的布局会覆盖掉系统的布局,再将BrowseFrameLayout重写成我们自己的BrowseFrameLayout。如下

public class BrowseFrameLayout extends android.support.v17.leanback.widget.BrowseFrameLayout {public BrowseFrameLayout(Context context) {super(context);}public BrowseFrameLayout(Context context, AttributeSet attrs) {super(context, attrs);}public BrowseFrameLayout(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}/*** Sets a {@link OnFocusSearchListener}.*/public void setOnFocusSearchListener(OnFocusSearchListener listener) {}
}

这样就可以实现向上导航的功能了。

问题9:VerticalGridFragment内容未占满整个屏幕

 <!--BrowseFragment, RowsFragment, DetailsFragment padding 左边的距离</item>-->
<item name="browsePaddingStart">@dimen/lb_browse_padding_start</item>
<!--BrowseFragment, RowsFragment, DetailsFragment padding 右边的距离</item>-->
<item name="browsePaddingEnd">@dimen/lb_browse_padding_end</item>
<!--BrowseFragment padding 顶部的距离</item>-->
<item name="browsePaddingTop">@dimen/lb_browse_padding_top</item>
<!--BrowseFragment padding 底部的距离</item>-->
<item name="browsePaddingBottom">@dimen/lb_browse_padding_bottom</item>
<!--start margin of RowsFragment inside BrowseFragment when HeadersFragment is visible</item>-->
<item name="browseRowsMarginStart">@dimen/lb_browse_rows_margin_start</item>
<!--top margin of RowsFragment inside BrowseFragment when BrowseFragment title is visible</item>-->
<item name="browseRowsMarginTop">@dimen/lb_browse_rows_margin_top</item>

如果你使用的是BrowseFragment,则控制上述的边距,如果你使用的是VerticalGridFragment, 则复写itemsVerticalGridStyle,他也使用了上述定义的值,也可以直接设置具体的值:

<style name="ItemsVerticalGridStyle" parent="@style/Widget.Leanback.GridItems.VerticalGridView"><item name="horizontalMargin">@dimen/gap_12_dp</item><item name="verticalMargin">@dimen/gap_12_dp</item><item name="android:focusable">true</item><item name="android:focusableInTouchMode">true</item><item name="android:paddingStart">?attr/browsePaddingStart</item><item name="android:paddingEnd">?attr/browsePaddingEnd</item><item name="android:paddingBottom">@dimen/lb_vertical_grid_padding_bottom</item><item name="android:paddingTop">?attr/browseRowsMarginTop</item><item name="android:gravity">center_horizontal</item><item name="focusOutFront">true</item>
</style>

样式调整

如果你需要对VerticalGridFragment的某些样式进行调整,你可以重新定义一个Theme继承自Theme.Leanback,这里我们大致写其中几个效果。可以控制VerticalGridFragment的内容的四周的边距,也可以控制ImageCardView的视觉效果。

<style name="AppTheme" parent="@style/Theme.Leanback"><!--BrowseFragment, RowsFragment, DetailsFragment padding 左边的距离</item>--><item name="browsePaddingStart">@dimen/lb_browse_padding_start</item><!--BrowseFragment, RowsFragment, DetailsFragment padding 右边的距离</item>--><item name="browsePaddingEnd">@dimen/lb_browse_padding_end</item><!--BrowseFragment padding 顶部的距离</item>--><item name="browsePaddingTop">@dimen/lb_browse_padding_top</item><!--BrowseFragment padding 底部的距离</item>--><item name="browsePaddingBottom">@dimen/lb_browse_padding_bottom</item><!--start margin of RowsFragment inside BrowseFragment when HeadersFragment is visible</item>--><item name="browseRowsMarginStart">@dimen/lb_browse_rows_margin_start</item><!--top margin of RowsFragment inside BrowseFragment when BrowseFragment title is visible</item>--><item name="browseRowsMarginTop">@dimen/lb_browse_rows_margin_top</item><!--fading edge length of start of browse row when HeadersFragment is visible</item>--><item name="browseRowsFadingEdgeLength">@dimen/lb_browse_rows_fading_edge</item><item name="baseCardViewStyle">@style/BaseCardViewStyle</item><item name="overlayDimMaskColor">@color/transparent</item><item name="overlayDimActiveLevel">@fraction/lb_view_active_level</item><!--控制每一个item 背景投影</item>--><item name="overlayDimDimmedLevel">0%</item><item name="itemsVerticalGridStyle">@style/ItemsVerticalGridStyle</item>
</style><style name="BaseCardViewStyle" parent="@style/Widget.Leanback.BaseCardViewStyle"><item name="cardForeground">@color/transparent</item><item name="cardBackground">@color/transparent</item>
</style><style name="ItemsVerticalGridStyle" parent="@style/Widget.Leanback.GridItems.VerticalGridView"><item name="horizontalMargin">@dimen/gap_12_dp</item><item name="verticalMargin">@dimen/gap_12_dp</item><item name="android:focusable">true</item><item name="android:focusableInTouchMode">true</item><item name="android:paddingStart">?attr/browsePaddingStart</item><item name="android:paddingEnd">?attr/browsePaddingEnd</item><item name="android:paddingBottom">@dimen/lb_vertical_grid_padding_bottom</item><item name="android:paddingTop">?attr/browseRowsMarginTop</item><item name="android:gravity">center_horizontal</item><item name="focusOutFront">true</item>
</style><style name="ImageCardViewInfoAreaStyle" parent="@style/Widget.Leanback.ImageCardView.InfoAreaStyle"><item name="android:background">@null</item>
</style><style name="ImageCardViewStyle" parent="@style/Widget.Leanback.ImageCardViewStyle"><item name="cardBackground">@color/transparent</item>
</style><style name="ImageCardViewTitleStyle" parent="@style/Widget.Leanback.ImageCardView.TitleStyle"><item name="android:layout_marginBottom">@dimen/lb_basic_card_info_text_margin</item><item name="android:textAlignment">center</item><item name="android:ellipsize">end</item>
</style><style name="ImageCardViewContentStyle" parent="@style/Widget.Leanback.ImageCardView.ContentStyle"><item name="android:layout_alignParentStart">false</item><item name="android:textAlignment">center</item>
</style>

总结

这其中有一个问题是@我是asha查出来的

在开发过程中可能还有这样或者那样的问题这里没有遇到,希望大家指正上面的问题,也可以探讨遇到的新问题。

Android TV开发相关推荐

  1. Android TV开发总结(三)构建一个TV app的焦点控制及遇到的坑

    原文:Android TV开发总结(三)构建一个TV app的焦点控制及遇到的坑 版权声明:我已委托"维权骑士"(rightknights.com)为我的文章进行维权行动.转载务必 ...

  2. 【Android TV 开发】焦点处理 ( 父容器与子组件焦点获取关系处理 | 不同电视设备上的兼容问题 | 触摸获取焦点 | 按键获取焦点 )

    Android TV 开发系列文章目录 [Android TV 开发]安卓电视调试 ( 开启网络远程调试 ) [Android TV 开发]焦点处理 ( 父容器与子组件焦点获取关系处理 | 不同电视设 ...

  3. android tv 菜单键,Android TV开发总结(三)构建一个TV app的焦点控制及遇到的坑

    前言:关于<TV Metro界面(仿泰捷视频TV版)源码解析>由于都是相关代码,就不发公众号了,有兴趣的可以看链接:http://blog.csdn.net/hejjunlin/artic ...

  4. android tv 云播放器,Android TV开发总结(六)构建一个TV app的直播节目实例

    近年来,Android TV的迅速发展,传统的有线电视受到较大的冲击,在TV上用户同样也可以看到各个有线电视的直播频道,相对于手机,这种直播节目,体验效果更佳,尤其是一样赛事节目,大屏幕看得才够痛快, ...

  5. android 仿 tv 菜单,Android TV 开发之仿泰捷视频最新 TV 版 Metro UI 效果

    Some Android TV related Sample 更多TV相关,欢迎关注公众号: Android TV开发交流群:135622564 1.Imitation of tai jie late ...

  6. android 按键分析,Android TV开发按键与焦点深入分析(四)

    8种机械键盘轴体对比 本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选? 前面三篇都是从源码的角度分析按键事件.焦点变换的原理,作为应用层的开发者, 分析源码都是带着实际的开发困惑的,要不然谁没 ...

  7. Android TV开发:APP安装、ICON图标问题

    使用AndroidX版本的Android Studio开发的面向TV的APK,安装后,在电视默认主屏没有显示该APP的ICON,是怎么回事? 一开始没有注意到电视的Android版本,安装APK时出现 ...

  8. 聊聊真实的 Android TV 开发技术栈

    智能电视越来越普及了,华为说四月发布智能电视跳票了,一加也说今后要布局智能电视,在智能电视方向,小米已经算是先驱了.但是还有不少开发把智能电视简单的理解成手机屏幕的放大,其实这两者并不一样. 一.序 ...

  9. Android TV 开发有关PopupWindow的KeyListener(手机也能用)

    转载请标明原地址:Android TV 开发有关PopupWindow的KeyListener(手机也能用)_高磊的专栏-CSDN博客 现在这个公司主要是做智能电视视频方面.有硬件电视盒子,APP开发 ...

  10. Android TV 开发 (1)

    本文来自网易云社区 作者:孙有军 前言 这里主要记录几个TV问题的解决方案,如果对这个不感兴趣的其实就不用往下看了. 这几天有一个需求就是要求出一个TV版本的app,之前没有具体的了解Tv版的app有 ...

最新文章

  1. 简单介绍python连接telnet和ssh的两种方式
  2. Java Web之文件的上传及下载
  3. lamp/lnmp实例
  4. 罗杰斯:做你喜欢的工作,你会变成个有钱人
  5. Beyond Compare 3.3.8 build 16340 + Key
  6. 微服务架构案例(03):数据库选型简介,业务数据规划设计
  7. 封装性的基本使用练习1
  8. 洛谷——P1100 高低位交换
  9. 初学UML之-------用例图
  10. 309. 最佳买卖股票时机含冷冻期
  11. 安卓10不支持qmc解码_你永远都不知道手机中计算器的秘密!安卓IOS都支持!
  12. 大家崇拜凯文.米特尼克吗?
  13. PHP小白之路1--PHP之简易留言板设计
  14. html表格相同合并单元格,根据id相同,table表格动态合并
  15. SPECCPU2006 Spec2006 使用说明
  16. Swing实现五子棋
  17. PC-DIMS测量中遇到了不可解决的问题
  18. 计算机引导原理,计算机启动原理与多重引导.ppt
  19. 微信小程序元素显示隐藏方法总结(原生)
  20. Qt+Win10使用QAxWidget控件实现远程桌面控制

热门文章

  1. mysql文件夹搬家_Mysql 静态文件搬家 注意事项
  2. c盘中的软件怎么转到D盘,将c盘软件转移到d盘
  3. c语言工业设计中的案例,工业设计中CMF为什么这么重要?看看设计案例就知道了!...
  4. 家里宽带网络连接第二台路由器实验二 ----Tenda A6设置无线信号放大模式(WISP)
  5. 安卓手机设置指定文件夹下的图片、视频不被相册读取到
  6. 关于计算机社团的游戏活动计划,社团活动策划方案趣味活动方案大全
  7. Native开发工具之静态库和动态库(二,小码农也有大梦想
  8. Rax.js基础学习
  9. 计算机二级页眉页脚的知识,轻松解决计算机二级之页眉页脚不再是问题-页眉页脚的设置...
  10. 追寻ARM的起源-Acorn电脑简史及FPGA实现