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