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