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