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