Android TV开发
2016-07-10 23:46
483 查看
前言
这里主要记录几个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"?> <LinearLayout xmlns: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"> <RelativeLayout android:layout_width="match_parent" android:layout_height="@dimen/gap_86_dp" android:clickable="true"> <TextView android: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"/> <TextView android: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"/> <TextView android: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"/> <TextView android: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> <View android:layout_width="match_parent" android:layout_height="1px" android:layout_marginBottom="@dimen/gap_50_dp" android:background="@color/gray1"/> <FrameLayout android: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; @Override public 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); } } @Override public 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; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_dial, container, false); } @Override public 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 } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_dial_pan, container, false); } @Override public 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); } @Override public void onClick(View v) { int tag = (int) v.getTag(); if (tag == -2) { dial(); } else if (tag == -1) {// DEL delNumber(); } 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; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(phone); dest.writeInt(headResId); dest.writeString(name); } @Override public 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; @Override public void onCreate(Bundle savedInstanceState) { Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); //setTitle(getString(R.string.vertical_grid_title)); getParams(); setupFragment(); } @Override public 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 { @Override public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { if (item instanceof Contact) { Contact contact = (Contact) item; // TODO } } } private final class ItemViewSelectedListener implements OnItemViewSelectedListener { @Override public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, 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; @Override public 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()) { @Override public 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); } @Override public 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()); } @Override public 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 memory contactView.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.NonOverlappingView android: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.NonOverlappingView android: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 info return 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() { @Override public 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 Studio 完美修改应用包名
- Android开发艺术探索笔记_第二章 IPC机制
- android查看Logcat
- 根据悍马病毒小结android病毒如何提取root权限
- windows下通过命令行启动android studio自带的安卓模拟器
- Android之制作简单的拨号器
- 基于Android实现保存图片到本地并可以在相册中显示出来
- 谈Android模拟点击的价值和实现
- Android Studio使用JNI和NDK进行开发
- 澄迈NIIT Android 实训 后5天学习android的基础
- android自定义带下拉刷新和Checkbox的ListView
- Android ShapeDrawable学习
- 使用Android Accessibility实现免Root自动批量安装功能
- Android开发中的java循环语句(简单小结)
- Class.getSimpleName()的作用
- Android SurfaceView用法
- Android实现无标题栏全屏的三种方法
- Android学习笔记:按钮类组件实例
- android基础——各个文件夹存放的文件类型
- Handler机制源码分析(异步一)