BrowseFragment.java revision 6f03a4e411825283e5fddac2ab29ee8a9de4c42b
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     */
162    public void setBackgroundParams(BackgroundParams params) {
163        mRowsFragment.setBackgroundParams(params);
164    }
165
166    /**
167     * Returns browse parameters.
168     */
169    public Params getBrowseParams() {
170        return mParams;
171    }
172
173    /**
174     * Returns the background parameters.
175     */
176    public BackgroundParams getBackgroundParams() {
177        return mRowsFragment.getBackgroundParams();
178    }
179
180    /**
181     * Sets the list of rows for the fragment.
182     */
183    public void setAdapter(ObjectAdapter adapter) {
184        mRowsFragment.setAdapter(adapter);
185        mHeadersFragment.setAdapter(adapter);
186    }
187
188    /**
189     * Returns the list of rows.
190     */
191    public ObjectAdapter getAdapter() {
192        return mRowsFragment.getAdapter();
193    }
194
195    /**
196     * Sets an item selection listener.
197     */
198    public void setOnItemSelectedListener(OnItemSelectedListener listener) {
199        mExternalOnItemSelectedListener = listener;
200    }
201
202    /**
203     * Sets an item Clicked listener.
204     */
205    public void setOnItemClickedListener(OnItemClickedListener listener) {
206        mRowsFragment.setOnItemClickedListener(listener);
207    }
208
209    /**
210     * Returns the item Clicked listener.
211     */
212    public OnItemClickedListener getOnItemClickedListener() {
213        return mRowsFragment.getOnItemClickedListener();
214    }
215
216    /**
217     * Sets a click listener for the search "affordance".
218     *
219     * The presence of a listener will change the visibility of the search "affordance" in the
220     * title area. When set to non null the title area will contain a call to search action.
221     *
222     * The listener onClick method will be invoked when the user click on the search action.
223     *
224     * @param listener The listener.
225     */
226    public void setOnSearchClickedListener(View.OnClickListener listener) {
227        mSearchOrbView.setOnOrbClickedListener(listener);
228    }
229
230    private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
231            new BrowseFrameLayout.OnFocusSearchListener() {
232        @Override
233        public View onFocusSearch(View focused, int direction) {
234            // If fastlane is disabled, just return null.
235            if (!mCanShowHeaders) return null;
236
237            if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
238            if (!mShowingHeaders && direction == View.FOCUS_LEFT) {
239                mTransitionHelper.runTransition(TransitionHelper.SCENE_WITH_HEADERS);
240                mShowingHeaders = true;
241                return mHeadersFragment.getVerticalGridView();
242
243            } else if (mShowingHeaders && direction == View.FOCUS_RIGHT) {
244                mTransitionHelper.runTransition(TransitionHelper.SCENE_WITHOUT_HEADERS);
245                mShowingHeaders = false;
246                return mRowsFragment.getVerticalGridView();
247            } else if (mSearchOrbView.getVisibility() == View.VISIBLE
248                    && direction == View.FOCUS_DOWN) {
249                return mRowsFragment.getVerticalGridView();
250            } else if (mSearchOrbView.getVisibility() == View.VISIBLE
251                    && direction == View.FOCUS_UP) {
252                return mSearchOrbView;
253            } else {
254                return null;
255            }
256        }
257    };
258
259    @Override
260    public void onCreate(Bundle savedInstanceState) {
261        super.onCreate(savedInstanceState);
262
263        mHeadersFragment.setOnHeaderClickListener(mHeaderClickListener);
264
265        mContainerListMarginLeft = (int) getResources().getDimension(
266                R.dimen.lb_browse_rows_margin_left);
267        mContainerListWidth =  getResources().getDimensionPixelSize(R.dimen.lb_browse_rows_width);
268        mContainerListAlignTop =
269            getResources().getDimensionPixelSize(R.dimen.lb_browse_rows_align_top);
270    }
271
272    @Override
273    public View onCreateView(LayoutInflater inflater, ViewGroup container,
274            Bundle savedInstanceState) {
275        View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
276
277        mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
278        mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
279
280        mBrowseTitle = (ViewGroup) root.findViewById(R.id.browse_title_group);
281        mBadgeView = (ImageView) mBrowseTitle.findViewById(R.id.browse_badge);
282        mTitleView = (TextView) mBrowseTitle.findViewById(R.id.browse_title);
283        mSearchOrbView = (SearchOrbView) mBrowseTitle.findViewById(R.id.browse_orb);
284
285        readArguments(getArguments());
286        if (mParams != null) {
287            setBadgeDrawable(mParams.mBadgeDrawable);
288            setTitle(mParams.mTitle);
289            setHeadersState(mParams.mHeadersState);
290        }
291
292        mTransitionHelper = new TransitionHelper(getActivity());
293        mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITH_TITLE, mBrowseFrame,
294                new Runnable() {
295            @Override
296            public void run() {
297                showTitle(true);
298            }
299        });
300        mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITHOUT_TITLE, mBrowseFrame,
301                new Runnable() {
302            @Override
303            public void run() {
304                showTitle(false);
305            }
306        });
307        mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITH_HEADERS, mBrowseFrame,
308                new Runnable() {
309            @Override
310            public void run() {
311                showHeaders(true);
312            }
313        });
314        mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITHOUT_HEADERS, mBrowseFrame,
315                new Runnable() {
316            @Override
317            public void run() {
318                showHeaders(false);
319            }
320        });
321
322        return root;
323    }
324
325    private void showTitle(boolean show) {
326        mBrowseTitle.setVisibility(show ? View.VISIBLE : View.GONE);
327    }
328
329    private void showHeaders(boolean show) {
330        if (DEBUG) Log.v(TAG, "showHeaders " + show);
331        View headerList = mHeadersFragment.getView();
332        View containerList = mRowsFragment.getView();
333        MarginLayoutParams lp;
334
335        headerList.setVisibility(show ? View.VISIBLE : View.GONE);
336        lp = (MarginLayoutParams) containerList.getLayoutParams();
337        lp.leftMargin = show ? mContainerListMarginLeft : 0;
338        containerList.setLayoutParams(lp);
339
340        mRowsFragment.setExpand(!show);
341    }
342
343    private HeaderPresenter.OnHeaderClickListener mHeaderClickListener =
344        new HeaderPresenter.OnHeaderClickListener() {
345            @Override
346            public void onHeaderClicked() {
347                if (!mCanShowHeaders || !mShowingHeaders) return;
348
349                mTransitionHelper.runTransition(TransitionHelper.SCENE_WITHOUT_HEADERS);
350                mShowingHeaders = false;
351                mRowsFragment.getVerticalGridView().requestFocus();
352            }
353        };
354
355    private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() {
356        @Override
357        public void onItemSelected(Object item, Row row) {
358            int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
359            if (DEBUG) Log.v(TAG, "row selected position " + position);
360            onRowSelected(position);
361            if (mExternalOnItemSelectedListener != null) {
362                mExternalOnItemSelectedListener.onItemSelected(item, row);
363            }
364        }
365    };
366
367    private OnItemSelectedListener mHeaderSelectedListener = new OnItemSelectedListener() {
368        @Override
369        public void onItemSelected(Object item, Row row) {
370            int position = mHeadersFragment.getVerticalGridView().getSelectedPosition();
371            if (DEBUG) Log.v(TAG, "header selected position " + position);
372            onRowSelected(position);
373        }
374    };
375
376    private void onRowSelected(int position) {
377        if (position != mSelectedPosition) {
378            mSetSelectionRunnable.mPosition = position;
379            mBrowseFrame.getHandler().post(mSetSelectionRunnable);
380
381            if (position == 0) {
382                if (!mShowingTitle) {
383                    mTransitionHelper.runTransition(TransitionHelper.SCENE_WITH_TITLE);
384                    mShowingTitle = true;
385                }
386            } else if (mShowingTitle) {
387                mTransitionHelper.runTransition(TransitionHelper.SCENE_WITHOUT_TITLE);
388                mShowingTitle = false;
389            }
390        }
391    }
392
393    private class SetSelectionRunnable implements Runnable {
394        int mPosition;
395        @Override
396        public void run() {
397            setSelection(mPosition);
398        }
399    }
400
401    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
402
403    private void setSelection(int position) {
404        if (position != NO_POSITION) {
405            mRowsFragment.setSelectedPosition(position);
406            mHeadersFragment.setSelectedPosition(position);
407        }
408        mSelectedPosition = position;
409    }
410
411    @Override
412    public void onActivityCreated(Bundle savedInstanceState) {
413        super.onActivityCreated(savedInstanceState);
414
415        if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) {
416            getChildFragmentManager().beginTransaction()
417                    .replace(R.id.browse_headers_dock, mHeadersFragment)
418                    .replace(R.id.browse_container_dock, mRowsFragment).commit();
419            mRowsFragment.setOnItemSelectedListener(mRowSelectedListener);
420            mHeadersFragment.setOnItemSelectedListener(mHeaderSelectedListener);
421        }
422    }
423
424    private void setVerticalVerticalGridViewLayout(VerticalGridView listview) {
425        // align the top edge of item to a fixed position
426        listview.setItemAlignmentOffset(0);
427        listview.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
428        listview.setWindowAlignmentOffset(mContainerListAlignTop);
429        listview.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
430        listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
431    }
432
433    /**
434     * Setup dimensions that are only meaningful when the child Fragments are inside
435     * BrowseFragment.
436     */
437    private void setupChildFragmentsLayout() {
438        VerticalGridView headerList = mHeadersFragment.getVerticalGridView();
439        VerticalGridView containerList = mRowsFragment.getVerticalGridView();
440
441        // Both fragments list view has the same alignment
442        setVerticalVerticalGridViewLayout(headerList);
443        setVerticalVerticalGridViewLayout(containerList);
444
445        mRowsFragment.getVerticalGridView().getLayoutParams().width = mContainerListWidth;
446        mRowsFragment.getVerticalGridView().requestLayout();
447    }
448
449    @Override
450    public void onStart() {
451        super.onStart();
452        setupChildFragmentsLayout();
453        if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) {
454            mHeadersFragment.getView().requestFocus();
455        } else if ((!mCanShowHeaders || !mShowingHeaders)
456                && mRowsFragment.getView() != null) {
457            mRowsFragment.getView().requestFocus();
458        }
459        showHeaders(mCanShowHeaders && mShowingHeaders);
460    }
461
462    private void readArguments(Bundle args) {
463        if (args == null) {
464            return;
465        }
466        if (args.containsKey(ARG_TITLE)) {
467            setTitle(args.getString(ARG_TITLE));
468        }
469
470        if (args.containsKey(ARG_BADGE_URI)) {
471            setBadgeUri(args.getString(ARG_BADGE_URI));
472        }
473
474        if (args.containsKey(ARG_HEADERS_STATE)) {
475            setHeadersState(args.getInt(ARG_HEADERS_STATE));
476        }
477    }
478
479    private void setBadgeUri(String badgeUri) {
480        // TODO - need a drawable downloader
481    }
482
483    private void setBadgeDrawable(Drawable drawable) {
484        if (mBadgeView == null) {
485            return;
486        }
487        mBadgeView.setImageDrawable(drawable);
488        if (drawable != null) {
489            mBadgeView.setVisibility(View.VISIBLE);
490        } else {
491            mBadgeView.setVisibility(View.GONE);
492        }
493    }
494
495    private void setTitle(String title) {
496        if (mTitleView != null) {
497            mTitleView.setText(title);
498        }
499    }
500
501    private void setHeadersState(int headersState) {
502        if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
503        switch (headersState) {
504            case HEADERS_ENABLED:
505                mCanShowHeaders = true;
506                mShowingHeaders = true;
507                break;
508            case HEADERS_HIDDEN:
509                mCanShowHeaders = true;
510                mShowingHeaders = false;
511                break;
512            case HEADERS_DISABLED:
513                mCanShowHeaders = false;
514                mShowingHeaders = false;
515                break;
516            default:
517                Log.w(TAG, "Unknown headers state: " + headersState);
518                break;
519        }
520    }
521}
522