BrowseFragment.java revision 9e8e482b545c9c7d8db82c05993850d2f9038c5b
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.app;
15
16import android.support.v17.leanback.R;
17import android.support.v17.leanback.widget.Presenter;
18import android.support.v17.leanback.widget.VerticalGridView;
19import android.support.v17.leanback.widget.Row;
20import android.support.v17.leanback.widget.ObjectAdapter;
21import android.support.v17.leanback.widget.OnItemSelectedListener;
22import android.support.v17.leanback.widget.OnItemClickedListener;
23import android.support.v17.leanback.widget.SearchOrbView;
24import android.util.Log;
25import android.app.Fragment;
26import android.os.Bundle;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.view.ViewGroup;
30import android.view.ViewGroup.MarginLayoutParams;
31import android.widget.ImageView;
32import android.widget.TextView;
33import android.graphics.drawable.Drawable;
34
35import static android.support.v7.widget.RecyclerView.NO_POSITION;
36
37/**
38 * Wrapper fragment for leanback browse screens. Composed of a
39 * RowsFragment and a HeadersFragment.
40 *
41 */
42public class BrowseFragment extends Fragment {
43    private static final String TAG = "BrowseFragment";
44    private static boolean DEBUG = false;
45
46    /** The fastlane navigation panel is enabled and shown by default. */
47    public static final int HEADERS_ENABLED = 1;
48
49    /** The fastlane navigation panel is enabled and hidden by default. */
50    public static final int HEADERS_HIDDEN = 2;
51
52    /** The fastlane navigation panel is disabled and will never be shown. */
53    public static final int HEADERS_DISABLED = 3;
54
55    private final RowsFragment mRowsFragment = new RowsFragment();
56    private final HeadersFragment mHeadersFragment = new HeadersFragment();
57
58    private Params mParams;
59    private BrowseFrameLayout mBrowseFrame;
60    private ImageView mBadgeView;
61    private TextView mTitleView;
62    private ViewGroup mBrowseTitle;
63    private SearchOrbView mSearchOrbView;
64    private boolean mShowingTitle = true;
65    private boolean mShowingHeaders = true;
66    private boolean mCanShowHeaders = true;
67    private int mContainerListMarginLeft;
68    private int mContainerListWidth;
69    private int mContainerListAlignTop;
70    private TransitionHelper mTransitionHelper;
71    private OnItemSelectedListener mExternalOnItemSelectedListener;
72    private int mSelectedPosition = -1;
73
74    private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title";
75    private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge";
76    private static final String ARG_HEADERS_STATE =
77        BrowseFragment.class.getCanonicalName() + ".headersState";
78
79    /**
80     * @param args Bundle to use for the arguments, if null a new Bundle will be created.
81     */
82    public static Bundle createArgs(Bundle args, String title, String badgeUri) {
83        return createArgs(args, title, badgeUri, HEADERS_ENABLED);
84    }
85
86    public static Bundle createArgs(Bundle args, String title, String badgeUri, int headersState) {
87        if (args == null) {
88            args = new Bundle();
89        }
90        args.putString(ARG_TITLE, title);
91        args.putString(ARG_BADGE_URI, badgeUri);
92        args.putInt(ARG_HEADERS_STATE, headersState);
93        return args;
94    }
95
96    public static class Params {
97        private String mTitle;
98        private Drawable mBadgeDrawable;
99        private int mHeadersState;
100
101        /**
102         * Sets the badge image.
103         */
104        public void setBadgeImage(Drawable drawable) {
105            mBadgeDrawable = drawable;
106        }
107
108        /**
109         * Returns the badge image.
110         */
111        public Drawable getBadgeImage() {
112            return mBadgeDrawable;
113        }
114
115        /**
116         * Sets a title for the browse fragment.
117         */
118        public void setTitle(String title) {
119            mTitle = title;
120        }
121
122        /**
123         * Returns the title for the browse fragment.
124         */
125        public String getTitle() {
126            return mTitle;
127        }
128
129        /**
130         * Sets the state for the headers column in the browse fragment.
131         */
132        public void setHeadersState(int headersState) {
133            if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
134                Log.e(TAG, "Invalid headers state: " + headersState
135                        + ", default to enabled and shown.");
136                mHeadersState = HEADERS_ENABLED;
137            } else {
138                mHeadersState = headersState;
139            }
140        }
141
142        /**
143         * Returns the state for the headers column in the browse fragment.
144         */
145        public int getHeadersState() {
146            return mHeadersState;
147        }
148    }
149
150    /**
151     * Set browse parameters.
152     */
153    public void setBrowseParams(Params params) {
154        mParams = params;
155        setBadgeDrawable(mParams.mBadgeDrawable);
156        setTitle(mParams.mTitle);
157        setHeadersState(mParams.mHeadersState);
158    }
159
160    /**
161     * Set background parameters.
162     * @deprecated Use BackgroundManager instead
163     */
164    @Deprecated
165    public void setBackgroundParams(BackgroundParams params) {
166    }
167
168    /**
169     * Returns browse parameters.
170     */
171    public Params getBrowseParams() {
172        return mParams;
173    }
174
175    /**
176     * Returns the background parameters.
177     * @deprecated Use BackgroundManager instead
178     */
179    @Deprecated
180    public BackgroundParams getBackgroundParams() {
181        return new BackgroundParams();
182    }
183
184    /**
185     * Sets the list of rows for the fragment.
186     */
187    public void setAdapter(ObjectAdapter adapter) {
188        mRowsFragment.setAdapter(adapter);
189        mHeadersFragment.setAdapter(adapter);
190    }
191
192    /**
193     * Returns the list of rows.
194     */
195    public ObjectAdapter getAdapter() {
196        return mRowsFragment.getAdapter();
197    }
198
199    /**
200     * Sets an item selection listener.
201     */
202    public void setOnItemSelectedListener(OnItemSelectedListener listener) {
203        mExternalOnItemSelectedListener = listener;
204    }
205
206    /**
207     * Sets an item clicked listener on the fragment.
208     * OnItemClickedListener will override {@link View.OnClickListener} that
209     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
210     * So in general,  developer should choose one of the listeners but not both.
211     */
212    public void setOnItemClickedListener(OnItemClickedListener listener) {
213        mRowsFragment.setOnItemClickedListener(listener);
214    }
215
216    /**
217     * Returns the item Clicked listener.
218     */
219    public OnItemClickedListener getOnItemClickedListener() {
220        return mRowsFragment.getOnItemClickedListener();
221    }
222
223    /**
224     * Sets a click listener for the search "affordance".
225     *
226     * The presence of a listener will change the visibility of the search "affordance" in the
227     * title area. When set to non null the title area will contain a call to search action.
228     *
229     * The listener onClick method will be invoked when the user click on the search action.
230     *
231     * @param listener The listener.
232     */
233    public void setOnSearchClickedListener(View.OnClickListener listener) {
234        mSearchOrbView.setOnOrbClickedListener(listener);
235    }
236
237    private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
238            new BrowseFrameLayout.OnFocusSearchListener() {
239        @Override
240        public View onFocusSearch(View focused, int direction) {
241            // If fastlane is disabled, just return null.
242            if (!mCanShowHeaders) return null;
243
244            if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
245            if (!mShowingHeaders && direction == View.FOCUS_LEFT) {
246                mTransitionHelper.runTransition(TransitionHelper.SCENE_WITH_HEADERS);
247                mShowingHeaders = true;
248                return mHeadersFragment.getVerticalGridView();
249
250            } else if (mShowingHeaders && direction == View.FOCUS_RIGHT) {
251                mTransitionHelper.runTransition(TransitionHelper.SCENE_WITHOUT_HEADERS);
252                mShowingHeaders = false;
253                return mRowsFragment.getVerticalGridView();
254            } else if (mSearchOrbView.getVisibility() == View.VISIBLE
255                    && direction == View.FOCUS_DOWN) {
256                return mRowsFragment.getVerticalGridView();
257            } else if (mSearchOrbView.getVisibility() == View.VISIBLE
258                    && direction == View.FOCUS_UP) {
259                return mSearchOrbView;
260            } else {
261                return null;
262            }
263        }
264    };
265
266    @Override
267    public void onCreate(Bundle savedInstanceState) {
268        super.onCreate(savedInstanceState);
269
270        mHeadersFragment.setOnHeaderClickListener(mHeaderClickListener);
271
272        mContainerListMarginLeft = (int) getResources().getDimension(
273                R.dimen.lb_browse_rows_margin_left);
274        mContainerListWidth =  getResources().getDimensionPixelSize(R.dimen.lb_browse_rows_width);
275        mContainerListAlignTop =
276            getResources().getDimensionPixelSize(R.dimen.lb_browse_rows_align_top);
277    }
278
279    @Override
280    public View onCreateView(LayoutInflater inflater, ViewGroup container,
281            Bundle savedInstanceState) {
282        View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
283
284        mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
285        mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
286
287        mBrowseTitle = (ViewGroup) root.findViewById(R.id.browse_title_group);
288        mBadgeView = (ImageView) mBrowseTitle.findViewById(R.id.browse_badge);
289        mTitleView = (TextView) mBrowseTitle.findViewById(R.id.browse_title);
290        mSearchOrbView = (SearchOrbView) mBrowseTitle.findViewById(R.id.browse_orb);
291
292        readArguments(getArguments());
293        if (mParams != null) {
294            setBadgeDrawable(mParams.mBadgeDrawable);
295            setTitle(mParams.mTitle);
296            setHeadersState(mParams.mHeadersState);
297        }
298
299        mTransitionHelper = new TransitionHelper(getActivity());
300        mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITH_TITLE, mBrowseFrame,
301                new Runnable() {
302            @Override
303            public void run() {
304                showTitle(true);
305            }
306        });
307        mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITHOUT_TITLE, mBrowseFrame,
308                new Runnable() {
309            @Override
310            public void run() {
311                showTitle(false);
312            }
313        });
314        mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITH_HEADERS, mBrowseFrame,
315                new Runnable() {
316            @Override
317            public void run() {
318                showHeaders(true);
319            }
320        });
321        mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITHOUT_HEADERS, mBrowseFrame,
322                new Runnable() {
323            @Override
324            public void run() {
325                showHeaders(false);
326            }
327        });
328
329        return root;
330    }
331
332    private void showTitle(boolean show) {
333        mBrowseTitle.setVisibility(show ? View.VISIBLE : View.GONE);
334    }
335
336    private void showHeaders(boolean show) {
337        if (DEBUG) Log.v(TAG, "showHeaders " + show);
338        View headerList = mHeadersFragment.getView();
339        View containerList = mRowsFragment.getView();
340        MarginLayoutParams lp;
341
342        headerList.setVisibility(show ? View.VISIBLE : View.GONE);
343        lp = (MarginLayoutParams) containerList.getLayoutParams();
344        lp.leftMargin = show ? mContainerListMarginLeft : 0;
345        containerList.setLayoutParams(lp);
346
347        mRowsFragment.setExpand(!show);
348    }
349
350    private HeaderPresenter.OnHeaderClickListener mHeaderClickListener =
351        new HeaderPresenter.OnHeaderClickListener() {
352            @Override
353            public void onHeaderClicked() {
354                if (!mCanShowHeaders || !mShowingHeaders) return;
355
356                mTransitionHelper.runTransition(TransitionHelper.SCENE_WITHOUT_HEADERS);
357                mShowingHeaders = false;
358                mRowsFragment.getVerticalGridView().requestFocus();
359            }
360        };
361
362    private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() {
363        @Override
364        public void onItemSelected(Object item, Row row) {
365            int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
366            if (DEBUG) Log.v(TAG, "row selected position " + position);
367            onRowSelected(position);
368            if (mExternalOnItemSelectedListener != null) {
369                mExternalOnItemSelectedListener.onItemSelected(item, row);
370            }
371        }
372    };
373
374    private OnItemSelectedListener mHeaderSelectedListener = new OnItemSelectedListener() {
375        @Override
376        public void onItemSelected(Object item, Row row) {
377            int position = mHeadersFragment.getVerticalGridView().getSelectedPosition();
378            if (DEBUG) Log.v(TAG, "header selected position " + position);
379            onRowSelected(position);
380        }
381    };
382
383    private void onRowSelected(int position) {
384        if (position != mSelectedPosition) {
385            mSetSelectionRunnable.mPosition = position;
386            mBrowseFrame.getHandler().post(mSetSelectionRunnable);
387
388            if (position == 0) {
389                if (!mShowingTitle) {
390                    mTransitionHelper.runTransition(TransitionHelper.SCENE_WITH_TITLE);
391                    mShowingTitle = true;
392                }
393            } else if (mShowingTitle) {
394                mTransitionHelper.runTransition(TransitionHelper.SCENE_WITHOUT_TITLE);
395                mShowingTitle = false;
396            }
397        }
398    }
399
400    private class SetSelectionRunnable implements Runnable {
401        int mPosition;
402        @Override
403        public void run() {
404            setSelection(mPosition);
405        }
406    }
407
408    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
409
410    private void setSelection(int position) {
411        if (position != NO_POSITION) {
412            mRowsFragment.setSelectedPosition(position);
413            mHeadersFragment.setSelectedPosition(position);
414        }
415        mSelectedPosition = position;
416    }
417
418    @Override
419    public void onActivityCreated(Bundle savedInstanceState) {
420        super.onActivityCreated(savedInstanceState);
421
422        if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) {
423            getChildFragmentManager().beginTransaction()
424                    .replace(R.id.browse_headers_dock, mHeadersFragment)
425                    .replace(R.id.browse_container_dock, mRowsFragment).commit();
426            mRowsFragment.setOnItemSelectedListener(mRowSelectedListener);
427            mHeadersFragment.setOnItemSelectedListener(mHeaderSelectedListener);
428        }
429    }
430
431    private void setVerticalVerticalGridViewLayout(VerticalGridView listview) {
432        // align the top edge of item to a fixed position
433        listview.setItemAlignmentOffset(0);
434        listview.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
435        listview.setWindowAlignmentOffset(mContainerListAlignTop);
436        listview.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
437        listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
438    }
439
440    /**
441     * Setup dimensions that are only meaningful when the child Fragments are inside
442     * BrowseFragment.
443     */
444    private void setupChildFragmentsLayout() {
445        VerticalGridView headerList = mHeadersFragment.getVerticalGridView();
446        VerticalGridView containerList = mRowsFragment.getVerticalGridView();
447
448        // Both fragments list view has the same alignment
449        setVerticalVerticalGridViewLayout(headerList);
450        setVerticalVerticalGridViewLayout(containerList);
451
452        mRowsFragment.getVerticalGridView().getLayoutParams().width = mContainerListWidth;
453        mRowsFragment.getVerticalGridView().requestLayout();
454    }
455
456    @Override
457    public void onStart() {
458        super.onStart();
459        setupChildFragmentsLayout();
460        if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) {
461            mHeadersFragment.getView().requestFocus();
462        } else if ((!mCanShowHeaders || !mShowingHeaders)
463                && mRowsFragment.getView() != null) {
464            mRowsFragment.getView().requestFocus();
465        }
466        showHeaders(mCanShowHeaders && mShowingHeaders);
467    }
468
469    private void readArguments(Bundle args) {
470        if (args == null) {
471            return;
472        }
473        if (args.containsKey(ARG_TITLE)) {
474            setTitle(args.getString(ARG_TITLE));
475        }
476
477        if (args.containsKey(ARG_BADGE_URI)) {
478            setBadgeUri(args.getString(ARG_BADGE_URI));
479        }
480
481        if (args.containsKey(ARG_HEADERS_STATE)) {
482            setHeadersState(args.getInt(ARG_HEADERS_STATE));
483        }
484    }
485
486    private void setBadgeUri(String badgeUri) {
487        // TODO - need a drawable downloader
488    }
489
490    private void setBadgeDrawable(Drawable drawable) {
491        if (mBadgeView == null) {
492            return;
493        }
494        mBadgeView.setImageDrawable(drawable);
495        if (drawable != null) {
496            mBadgeView.setVisibility(View.VISIBLE);
497        } else {
498            mBadgeView.setVisibility(View.GONE);
499        }
500    }
501
502    private void setTitle(String title) {
503        if (mTitleView != null) {
504            mTitleView.setText(title);
505        }
506    }
507
508    private void setHeadersState(int headersState) {
509        if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
510        switch (headersState) {
511            case HEADERS_ENABLED:
512                mCanShowHeaders = true;
513                mShowingHeaders = true;
514                break;
515            case HEADERS_HIDDEN:
516                mCanShowHeaders = true;
517                mShowingHeaders = false;
518                break;
519            case HEADERS_DISABLED:
520                mCanShowHeaders = false;
521                mShowingHeaders = false;
522                break;
523            default:
524                Log.w(TAG, "Unknown headers state: " + headersState);
525                break;
526        }
527    }
528}
529