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