BrowseFragment.java revision 79ea84679035f18acc581896ff028a4866361b04
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        mRowsFragment.getVerticalGridView().setAnimateChildLayout(true);
253        mRowsFragment.getVerticalGridView().setFocusSearchDisabled(false);
254        mHeadersFragment.getVerticalGridView().setFocusSearchDisabled(false);
255    }
256
257    private boolean isVerticalScrolling() {
258        // don't run transition
259        return mHeadersFragment.getVerticalGridView().getScrollState()
260                != HorizontalGridView.SCROLL_STATE_IDLE
261                || mRowsFragment.getVerticalGridView().getScrollState()
262                != HorizontalGridView.SCROLL_STATE_IDLE;
263    }
264
265    private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
266            new BrowseFrameLayout.OnFocusSearchListener() {
267        @Override
268        public View onFocusSearch(View focused, int direction) {
269            // If fastlane is disabled, just return null.
270            if (!mCanShowHeaders) return null;
271
272            // if fast lane is running transition,  focus stays
273            if (mHeadersTransitionRunning) return focused;
274            if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
275            if (!mShowingHeaders && direction == View.FOCUS_LEFT) {
276                if (isVerticalScrolling()) {
277                    return focused;
278                }
279                onHeadersTransitionStart();
280                mHeadersFragment.attachGridView();
281                mBrowseFrame.postOnAnimationDelayed(new Runnable() {
282                    @Override
283                    public void run() {
284                        mHeadersFragment.detachGridView();
285                        mTransitionHelper.runTransition(mSceneWithHeaders, mHeadersTransition);
286                    }
287                }, 0);
288                mShowingHeaders = true;
289                return mHeadersFragment.getVerticalGridView();
290            } else if (mShowingHeaders && direction == View.FOCUS_RIGHT) {
291                if (isVerticalScrolling()) {
292                    return focused;
293                }
294                onHeadersTransitionStart();
295                mHeadersFragment.attachGridView();
296                mBrowseFrame.postOnAnimationDelayed(new Runnable() {
297                    @Override
298                    public void run() {
299                        mTransitionHelper.runTransition(mSceneWithoutHeaders, mHeadersTransition);
300                    }
301                }, 0);
302                mShowingHeaders = false;
303                return mRowsFragment.getVerticalGridView();
304            } else if (focused == mSearchOrbView && direction == View.FOCUS_DOWN) {
305                return mShowingHeaders ? mHeadersFragment.getVerticalGridView() :
306                    mRowsFragment.getVerticalGridView();
307
308            } else if (focused != mSearchOrbView && mSearchOrbView.getVisibility() == View.VISIBLE
309                    && direction == View.FOCUS_UP) {
310                return mSearchOrbView;
311
312            } else {
313                return null;
314            }
315        }
316    };
317
318    @Override
319    public void onCreate(Bundle savedInstanceState) {
320        super.onCreate(savedInstanceState);
321        TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme);
322        mContainerListMarginLeft = (int) ta.getDimension(
323                R.styleable.LeanbackTheme_browseRowsMarginStart, 0);
324        mContainerListAlignTop = (int) ta.getDimension(
325                R.styleable.LeanbackTheme_browseRowsMarginTop, 0);
326        ta.recycle();
327    }
328
329    @Override
330    public View onCreateView(LayoutInflater inflater, ViewGroup container,
331            Bundle savedInstanceState) {
332        if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) {
333            mRowsFragment = new RowsFragment();
334            mHeadersFragment = new HeadersFragment();
335            getChildFragmentManager().beginTransaction()
336                    .replace(R.id.browse_headers_dock, mHeadersFragment)
337                    .replace(R.id.browse_container_dock, mRowsFragment).commit();
338        } else {
339            mHeadersFragment = (HeadersFragment) getChildFragmentManager()
340                    .findFragmentById(R.id.browse_headers_dock);
341            mRowsFragment = (RowsFragment) getChildFragmentManager()
342                    .findFragmentById(R.id.browse_container_dock);
343        }
344        mRowsFragment.setOnItemSelectedListener(mRowSelectedListener);
345        mHeadersFragment.setOnItemSelectedListener(mHeaderSelectedListener);
346        mHeadersFragment.setOnHeaderClickListener(mHeaderClickListener);
347        mRowsFragment.setOnItemClickedListener(mOnItemClickedListener);
348        mRowsFragment.setAdapter(mAdapter);
349        mHeadersFragment.setAdapter(mAdapter);
350
351        View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
352
353        mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
354        mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
355
356        mBrowseTitle = (ViewGroup) root.findViewById(R.id.browse_title_group);
357        mBadgeView = (ImageView) mBrowseTitle.findViewById(R.id.browse_badge);
358        mTitleView = (TextView) mBrowseTitle.findViewById(R.id.browse_title);
359        mSearchOrbView = (SearchOrbView) mBrowseTitle.findViewById(R.id.browse_orb);
360        if (mExternalOnSearchClickedListener != null) {
361            mSearchOrbView.setOnOrbClickedListener(mExternalOnSearchClickedListener);
362        }
363
364        readArguments(getArguments());
365        if (mParams != null) {
366            setBadgeDrawable(mParams.mBadgeDrawable);
367            setTitle(mParams.mTitle);
368            setHeadersState(mParams.mHeadersState);
369        }
370
371        mTransitionHelper = new TransitionHelper(getActivity());
372        mSceneWithTitle = mTransitionHelper.createScene(mBrowseFrame, new Runnable() {
373            @Override
374            public void run() {
375                showTitle(true);
376            }
377        });
378        mSceneWithoutTitle = mTransitionHelper.createScene(mBrowseFrame, new Runnable() {
379            @Override
380            public void run() {
381                showTitle(false);
382            }
383        });
384        mSceneWithHeaders = mTransitionHelper.createScene(mBrowseFrame, new Runnable() {
385            @Override
386            public void run() {
387                showHeaders(true);
388            }
389        });
390        mSceneWithoutHeaders =  mTransitionHelper.createScene(mBrowseFrame, new Runnable() {
391            @Override
392            public void run() {
393                showHeaders(false);
394            }
395        });
396        mTitleTransition = mTransitionHelper.createAutoTransition();
397        mHeadersTransition = mTransitionHelper.createAutoTransition();
398        mTransitionHelper.excludeChildren(mHeadersTransition, R.id.browse_title_group, true);
399        mTransitionHelper.excludeChildren(mTitleTransition, R.id.browse_headers, true);
400        mTransitionHelper.excludeChildren(mTitleTransition, R.id.container_list, true);
401        mTransitionHelper.setTransitionCompleteListener(mHeadersTransition, new Runnable() {
402            @Override
403            public void run() {
404                onHeadersTransitionComplete();
405            }
406        });
407        return root;
408    }
409
410    private void showTitle(boolean show) {
411        mBrowseTitle.setVisibility(show ? View.VISIBLE : View.GONE);
412    }
413
414    private void showHeaders(boolean show) {
415        if (DEBUG) Log.v(TAG, "showHeaders " + show);
416        View headerList = mHeadersFragment.getView();
417        View containerList = mRowsFragment.getView();
418        MarginLayoutParams lp;
419
420        if (show) {
421            mHeadersFragment.attachGridView();
422            mHeadersFragment.getView().requestFocus();
423        } else {
424            mHeadersFragment.detachGridView();
425        }
426        lp = (MarginLayoutParams) containerList.getLayoutParams();
427        lp.leftMargin = show ? mContainerListMarginLeft : 0;
428        containerList.setLayoutParams(lp);
429
430        mRowsFragment.setExpand(!show);
431    }
432
433    private HeaderPresenter.OnHeaderClickListener mHeaderClickListener =
434        new HeaderPresenter.OnHeaderClickListener() {
435            @Override
436            public void onHeaderClicked() {
437                if (!mCanShowHeaders || !mShowingHeaders) return;
438
439                if (mHeadersTransitionRunning) {
440                    return;
441                }
442                onHeadersTransitionStart();
443                mTransitionHelper.runTransition(mSceneWithoutHeaders, mHeadersTransition);
444                mShowingHeaders = false;
445                mRowsFragment.getVerticalGridView().requestFocus();
446            }
447        };
448
449    private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() {
450        @Override
451        public void onItemSelected(Object item, Row row) {
452            int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
453            if (DEBUG) Log.v(TAG, "row selected position " + position);
454            onRowSelected(position);
455            if (mExternalOnItemSelectedListener != null) {
456                mExternalOnItemSelectedListener.onItemSelected(item, row);
457            }
458        }
459    };
460
461    private OnItemSelectedListener mHeaderSelectedListener = new OnItemSelectedListener() {
462        @Override
463        public void onItemSelected(Object item, Row row) {
464            int position = mHeadersFragment.getVerticalGridView().getSelectedPosition();
465            if (DEBUG) Log.v(TAG, "header selected position " + position);
466            onRowSelected(position);
467        }
468    };
469
470    private void onRowSelected(int position) {
471        if (position != mSelectedPosition) {
472            mSetSelectionRunnable.mPosition = position;
473            mBrowseFrame.getHandler().post(mSetSelectionRunnable);
474
475            if (position == 0) {
476                if (!mShowingTitle) {
477                    mTransitionHelper.runTransition(mSceneWithTitle, mTitleTransition);
478                    mShowingTitle = true;
479                }
480            } else if (mShowingTitle) {
481                mTransitionHelper.runTransition(mSceneWithoutTitle, mTitleTransition);
482                mShowingTitle = false;
483            }
484        }
485    }
486
487    private class SetSelectionRunnable implements Runnable {
488        int mPosition;
489        @Override
490        public void run() {
491            setSelection(mPosition);
492        }
493    }
494
495    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
496
497    private void setSelection(int position) {
498        if (position != NO_POSITION) {
499            mRowsFragment.setSelectedPosition(position);
500            mHeadersFragment.setSelectedPosition(position);
501        }
502        mSelectedPosition = position;
503    }
504
505    private void setVerticalVerticalGridViewLayout(VerticalGridView listview) {
506        // align the top edge of item to a fixed position
507        listview.setItemAlignmentOffset(0);
508        listview.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
509        listview.setWindowAlignmentOffset(mContainerListAlignTop);
510        listview.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
511        listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
512    }
513
514    /**
515     * Setup dimensions that are only meaningful when the child Fragments are inside
516     * BrowseFragment.
517     */
518    private void setupChildFragmentsLayout() {
519        VerticalGridView headerList = mHeadersFragment.getVerticalGridView();
520        VerticalGridView containerList = mRowsFragment.getVerticalGridView();
521
522        // Both fragments list view has the same alignment
523        setVerticalVerticalGridViewLayout(headerList);
524        setVerticalVerticalGridViewLayout(containerList);
525    }
526
527    @Override
528    public void onStart() {
529        super.onStart();
530        setupChildFragmentsLayout();
531        if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) {
532            mHeadersFragment.getView().requestFocus();
533        } else if ((!mCanShowHeaders || !mShowingHeaders)
534                && mRowsFragment.getView() != null) {
535            mRowsFragment.getView().requestFocus();
536        }
537        showHeaders(mCanShowHeaders && mShowingHeaders);
538    }
539
540    private void readArguments(Bundle args) {
541        if (args == null) {
542            return;
543        }
544        if (args.containsKey(ARG_TITLE)) {
545            setTitle(args.getString(ARG_TITLE));
546        }
547
548        if (args.containsKey(ARG_BADGE_URI)) {
549            setBadgeUri(args.getString(ARG_BADGE_URI));
550        }
551
552        if (args.containsKey(ARG_HEADERS_STATE)) {
553            setHeadersState(args.getInt(ARG_HEADERS_STATE));
554        }
555    }
556
557    private void setBadgeUri(String badgeUri) {
558        // TODO - need a drawable downloader
559    }
560
561    private void setBadgeDrawable(Drawable drawable) {
562        if (mBadgeView == null) {
563            return;
564        }
565        mBadgeView.setImageDrawable(drawable);
566        if (drawable != null) {
567            mBadgeView.setVisibility(View.VISIBLE);
568        } else {
569            mBadgeView.setVisibility(View.GONE);
570        }
571    }
572
573    private void setTitle(String title) {
574        if (mTitleView != null) {
575            mTitleView.setText(title);
576        }
577    }
578
579    private void setHeadersState(int headersState) {
580        if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
581        switch (headersState) {
582            case HEADERS_ENABLED:
583                mCanShowHeaders = true;
584                mShowingHeaders = true;
585                break;
586            case HEADERS_HIDDEN:
587                mCanShowHeaders = true;
588                mShowingHeaders = false;
589                break;
590            case HEADERS_DISABLED:
591                mCanShowHeaders = false;
592                mShowingHeaders = false;
593                break;
594            default:
595                Log.w(TAG, "Unknown headers state: " + headersState);
596                break;
597        }
598    }
599}
600