Android TV开发

前言

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

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

样例

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

Android TV开发_第1张图片
Android TV开发_第2张图片

开发过程

       虽然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样例程序直接导入是运行不起来的,需要修改很多东西,但是实质内容与新创建的工程没有什么区别,因此也可以不用导入样例程序进行学习了。

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

"1.0" encoding="utf-8"?>
"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">

    "match_parent"
        android:layout_height="@dimen/gap_86_dp"
        android:clickable="true">

        "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"/>

        "@+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"/>

        "@+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"/>

        "@+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>

    "
        android:background="@color/gray1"/>

    @+id/tab_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

       界面的代码如下:

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 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 getContactList() {
        buildContact();
        return contactList;
    }

    public static List buildContact() {
        if (null != contactList) {
            return contactList;
        }
        contactList = new ArrayList();
        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 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属性,比如拨号键盘选中后会出现一个圆形的选中背景框, 如下:
Android TV开发_第3张图片

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


<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

<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,插入的布局代码如下:

"http://schemas.android.com/apk/res/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" />
    .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" />

       该布局插入了两帧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内容未占满整个屏幕

 
<item name="browsePaddingStart">@dimen/lb_browse_padding_startitem>

<item name="browsePaddingEnd">@dimen/lb_browse_padding_enditem>

<item name="browsePaddingTop">@dimen/lb_browse_padding_topitem>

<item name="browsePaddingBottom">@dimen/lb_browse_padding_bottomitem>

<item name="browseRowsMarginStart">@dimen/lb_browse_rows_margin_startitem>

<item name="browseRowsMarginTop">@dimen/lb_browse_rows_margin_topitem>

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

样式调整

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














总结

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

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

你可能感兴趣的:(TV)