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.transition.TransitionHelper;
18import android.support.v17.leanback.widget.BrowseFrameLayout;
19import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter;
20import android.support.v17.leanback.widget.ItemAlignmentFacet;
21import android.support.v17.leanback.widget.ItemBridgeAdapter;
22import android.support.v17.leanback.widget.ObjectAdapter;
23import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
24import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
25import android.support.v17.leanback.widget.Presenter;
26import android.support.v17.leanback.widget.PresenterSelector;
27import android.support.v17.leanback.widget.RowPresenter;
28import android.support.v17.leanback.widget.TitleHelper;
29import android.support.v17.leanback.widget.VerticalGridView;
30import android.os.Bundle;
31import android.util.Log;
32import android.util.TypedValue;
33import android.view.LayoutInflater;
34import android.view.View;
35import android.view.ViewGroup;
36
37/**
38 * A fragment for creating Leanback details screens.
39 *
40 * <p>
41 * A DetailsFragment renders the elements of its {@link ObjectAdapter} as a set
42 * of rows in a vertical list.The Adapter's {@link PresenterSelector} must maintain subclasses
43 * of {@link RowPresenter}.
44 * </p>
45 *
46 * When {@link FullWidthDetailsOverviewRowPresenter} is found in adapter,  DetailsFragment will
47 * setup default behavior of the DetailsOverviewRow:
48 * <li>
49 * The alignment of FullWidthDetailsOverviewRowPresenter is setup in
50 * {@link #setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter)}.
51 * </li>
52 * <li>
53 * The view status switching of FullWidthDetailsOverviewRowPresenter is done in
54 * {@link #onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter,
55 * FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int)}.
56 * </li>
57 *
58 * <p>
59 * The recommended activity themes to use with a DetailsFragment are
60 * <li>
61 * {@link android.support.v17.leanback.R.style#Theme_Leanback_Details} with activity
62 * shared element transition for {@link FullWidthDetailsOverviewRowPresenter}.
63 * </li>
64 * <li>
65 * {@link android.support.v17.leanback.R.style#Theme_Leanback_Details_NoSharedElementTransition}
66 * if shared element transition is not needed, for example if first row is not rendered by
67 * {@link FullWidthDetailsOverviewRowPresenter}.
68 * </li>
69 * </p>
70 */
71public class DetailsFragment extends BaseFragment {
72    static final String TAG = "DetailsFragment";
73    static boolean DEBUG = false;
74
75    private class SetSelectionRunnable implements Runnable {
76        int mPosition;
77        boolean mSmooth = true;
78
79        SetSelectionRunnable() {
80        }
81
82        @Override
83        public void run() {
84            if (mRowsFragment == null) {
85                return;
86            }
87            mRowsFragment.setSelectedPosition(mPosition, mSmooth);
88        }
89    }
90
91    RowsFragment mRowsFragment;
92
93    private ObjectAdapter mAdapter;
94    private int mContainerListAlignTop;
95    BaseOnItemViewSelectedListener mExternalOnItemViewSelectedListener;
96    private BaseOnItemViewClickedListener mOnItemViewClickedListener;
97
98    private Object mSceneAfterEntranceTransition;
99
100    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
101
102    private final BaseOnItemViewSelectedListener<Object> mOnItemViewSelectedListener =
103            new BaseOnItemViewSelectedListener<Object>() {
104        @Override
105        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
106                                   RowPresenter.ViewHolder rowViewHolder, Object row) {
107            int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
108            int subposition = mRowsFragment.getVerticalGridView().getSelectedSubPosition();
109            if (DEBUG) Log.v(TAG, "row selected position " + position
110                    + " subposition " + subposition);
111            onRowSelected(position, subposition);
112            if (mExternalOnItemViewSelectedListener != null) {
113                mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
114                        rowViewHolder, row);
115            }
116        }
117    };
118
119    /**
120     * Sets the list of rows for the fragment.
121     */
122    public void setAdapter(ObjectAdapter adapter) {
123        mAdapter = adapter;
124        Presenter[] presenters = adapter.getPresenterSelector().getPresenters();
125        if (presenters != null) {
126            for (int i = 0; i < presenters.length; i++) {
127                setupPresenter(presenters[i]);
128            }
129        } else {
130            Log.e(TAG, "PresenterSelector.getPresenters() not implemented");
131        }
132        if (mRowsFragment != null) {
133            mRowsFragment.setAdapter(adapter);
134        }
135    }
136
137    /**
138     * Returns the list of rows.
139     */
140    public ObjectAdapter getAdapter() {
141        return mAdapter;
142    }
143
144    /**
145     * Sets an item selection listener.
146     */
147    public void setOnItemViewSelectedListener(BaseOnItemViewSelectedListener listener) {
148        mExternalOnItemViewSelectedListener = listener;
149    }
150
151    /**
152     * Sets an item clicked listener.
153     */
154    public void setOnItemViewClickedListener(BaseOnItemViewClickedListener listener) {
155        if (mOnItemViewClickedListener != listener) {
156            mOnItemViewClickedListener = listener;
157            if (mRowsFragment != null) {
158                mRowsFragment.setOnItemViewClickedListener(listener);
159            }
160        }
161    }
162
163    /**
164     * Returns the item clicked listener.
165     */
166    public BaseOnItemViewClickedListener getOnItemViewClickedListener() {
167        return mOnItemViewClickedListener;
168    }
169
170    @Override
171    public void onCreate(Bundle savedInstanceState) {
172        super.onCreate(savedInstanceState);
173
174        mContainerListAlignTop =
175            getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top);
176    }
177
178    @Override
179    public View onCreateView(LayoutInflater inflater, ViewGroup container,
180            Bundle savedInstanceState) {
181        View view = inflater.inflate(R.layout.lb_details_fragment, container, false);
182        ViewGroup fragment_root = (ViewGroup) view.findViewById(R.id.details_fragment_root);
183        installTitleView(inflater, fragment_root, savedInstanceState);
184        mRowsFragment = (RowsFragment) getChildFragmentManager().findFragmentById(
185                R.id.details_rows_dock);
186        if (mRowsFragment == null) {
187            mRowsFragment = new RowsFragment();
188            getChildFragmentManager().beginTransaction()
189                    .replace(R.id.details_rows_dock, mRowsFragment).commit();
190        }
191        mRowsFragment.setAdapter(mAdapter);
192        mRowsFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
193        mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
194
195        mSceneAfterEntranceTransition = TransitionHelper.createScene(
196                (ViewGroup) view, new Runnable() {
197            @Override
198            public void run() {
199                mRowsFragment.setEntranceTransitionState(true);
200            }
201        });
202        return view;
203    }
204
205    /**
206     * @deprecated override {@link #onInflateTitleView(LayoutInflater,ViewGroup,Bundle)} instead.
207     */
208    @Deprecated
209    protected View inflateTitle(LayoutInflater inflater, ViewGroup parent,
210            Bundle savedInstanceState) {
211        return super.onInflateTitleView(inflater, parent, savedInstanceState);
212    }
213
214    @Override
215    public View onInflateTitleView(LayoutInflater inflater, ViewGroup parent,
216                                   Bundle savedInstanceState) {
217        return inflateTitle(inflater, parent, savedInstanceState);
218    }
219
220    void setVerticalGridViewLayout(VerticalGridView listview) {
221        // align the top edge of item to a fixed position
222        listview.setItemAlignmentOffset(-mContainerListAlignTop);
223        listview.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
224        listview.setWindowAlignmentOffset(0);
225        listview.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
226        listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
227    }
228
229    /**
230     * Called to setup each Presenter of Adapter passed in {@link #setAdapter(ObjectAdapter)}.  Note
231     * that setup should only change the Presenter behavior that is meaningful in DetailsFragment.  For
232     * example how a row is aligned in details Fragment.   The default implementation invokes
233     * {@link #setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter)}
234     *
235     */
236    protected void setupPresenter(Presenter rowPresenter) {
237        if (rowPresenter instanceof FullWidthDetailsOverviewRowPresenter) {
238            setupDetailsOverviewRowPresenter((FullWidthDetailsOverviewRowPresenter) rowPresenter);
239        }
240    }
241
242    /**
243     * Called to setup {@link FullWidthDetailsOverviewRowPresenter}.  The default implementation
244     * adds two alignment positions({@link ItemAlignmentFacet}) for ViewHolder of
245     * FullWidthDetailsOverviewRowPresenter to align in fragment.
246     */
247    protected void setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter presenter) {
248        ItemAlignmentFacet facet = new ItemAlignmentFacet();
249        // by default align details_frame to half window height
250        ItemAlignmentFacet.ItemAlignmentDef alignDef1 = new ItemAlignmentFacet.ItemAlignmentDef();
251        alignDef1.setItemAlignmentViewId(R.id.details_frame);
252        alignDef1.setItemAlignmentOffset(- getResources()
253                .getDimensionPixelSize(R.dimen.lb_details_v2_align_pos_for_actions));
254        alignDef1.setItemAlignmentOffsetPercent(0);
255        // when description is selected, align details_frame to top edge
256        ItemAlignmentFacet.ItemAlignmentDef alignDef2 = new ItemAlignmentFacet.ItemAlignmentDef();
257        alignDef2.setItemAlignmentViewId(R.id.details_frame);
258        alignDef2.setItemAlignmentFocusViewId(R.id.details_overview_description);
259        alignDef2.setItemAlignmentOffset(- getResources()
260                .getDimensionPixelSize(R.dimen.lb_details_v2_align_pos_for_description));
261        alignDef2.setItemAlignmentOffsetPercent(0);
262        ItemAlignmentFacet.ItemAlignmentDef[] defs =
263                new ItemAlignmentFacet.ItemAlignmentDef[] {alignDef1, alignDef2};
264        facet.setAlignmentDefs(defs);
265        presenter.setFacet(ItemAlignmentFacet.class, facet);
266    }
267
268    VerticalGridView getVerticalGridView() {
269        return mRowsFragment == null ? null : mRowsFragment.getVerticalGridView();
270    }
271
272    /**
273     * Gets embedded RowsFragment showing multiple rows for DetailsFragment.  If view of
274     * DetailsFragment is not created, the method returns null.
275     * @return Embedded RowsFragment showing multiple rows for DetailsFragment.
276     */
277    public RowsFragment getRowsFragment() {
278        return mRowsFragment;
279    }
280
281    /**
282     * Setup dimensions that are only meaningful when the child Fragments are inside
283     * DetailsFragment.
284     */
285    private void setupChildFragmentLayout() {
286        setVerticalGridViewLayout(mRowsFragment.getVerticalGridView());
287    }
288
289    private void setupFocusSearchListener() {
290        TitleHelper titleHelper = getTitleHelper();
291        if (titleHelper != null) {
292            BrowseFrameLayout browseFrameLayout = (BrowseFrameLayout) getView().findViewById(
293                    R.id.details_fragment_root);
294            browseFrameLayout.setOnFocusSearchListener(titleHelper.getOnFocusSearchListener());
295        }
296    }
297
298    /**
299     * Sets the selected row position with smooth animation.
300     */
301    public void setSelectedPosition(int position) {
302        setSelectedPosition(position, true);
303    }
304
305    /**
306     * Sets the selected row position.
307     */
308    public void setSelectedPosition(int position, boolean smooth) {
309        mSetSelectionRunnable.mPosition = position;
310        mSetSelectionRunnable.mSmooth = smooth;
311        if (getView() != null && getView().getHandler() != null) {
312            getView().getHandler().post(mSetSelectionRunnable);
313        }
314    }
315
316    void onRowSelected(int selectedPosition, int selectedSubPosition) {
317        ObjectAdapter adapter = getAdapter();
318        if (adapter == null || adapter.size() == 0 ||
319                (selectedPosition == 0 && selectedSubPosition == 0)) {
320            showTitle(true);
321        } else {
322            showTitle(false);
323        }
324        if (adapter != null && adapter.size() > selectedPosition) {
325            final VerticalGridView gridView = getVerticalGridView();
326            final int count = gridView.getChildCount();
327            for (int i = 0; i < count; i++) {
328                ItemBridgeAdapter.ViewHolder bridgeViewHolder = (ItemBridgeAdapter.ViewHolder)
329                        gridView.getChildViewHolder(gridView.getChildAt(i));
330                RowPresenter rowPresenter = (RowPresenter) bridgeViewHolder.getPresenter();
331                onSetRowStatus(rowPresenter,
332                        rowPresenter.getRowViewHolder(bridgeViewHolder.getViewHolder()),
333                        bridgeViewHolder.getAdapterPosition(),
334                        selectedPosition, selectedSubPosition);
335            }
336        }
337    }
338
339    /**
340     * Called on every visible row to change view status when current selected row position
341     * or selected sub position changed.  Subclass may override.   The default
342     * implementation calls {@link #onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter,
343     * FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int)} if presenter is
344     * instance of {@link FullWidthDetailsOverviewRowPresenter}.
345     *
346     * @param presenter   The presenter used to create row ViewHolder.
347     * @param viewHolder  The visible (attached) row ViewHolder, note that it may or may not
348     *                    be selected.
349     * @param adapterPosition  The adapter position of viewHolder inside adapter.
350     * @param selectedPosition The adapter position of currently selected row.
351     * @param selectedSubPosition The sub position within currently selected row.  This is used
352     *                            When a row has multiple alignment positions.
353     */
354    protected void onSetRowStatus(RowPresenter presenter, RowPresenter.ViewHolder viewHolder, int
355            adapterPosition, int selectedPosition, int selectedSubPosition) {
356        if (presenter instanceof FullWidthDetailsOverviewRowPresenter) {
357            onSetDetailsOverviewRowStatus((FullWidthDetailsOverviewRowPresenter) presenter,
358                    (FullWidthDetailsOverviewRowPresenter.ViewHolder) viewHolder,
359                    adapterPosition, selectedPosition, selectedSubPosition);
360        }
361    }
362
363    /**
364     * Called to change DetailsOverviewRow view status when current selected row position
365     * or selected sub position changed.  Subclass may override.   The default
366     * implementation switches between three states based on the positions:
367     * {@link FullWidthDetailsOverviewRowPresenter#STATE_HALF},
368     * {@link FullWidthDetailsOverviewRowPresenter#STATE_FULL} and
369     * {@link FullWidthDetailsOverviewRowPresenter#STATE_SMALL}.
370     *
371     * @param presenter   The presenter used to create row ViewHolder.
372     * @param viewHolder  The visible (attached) row ViewHolder, note that it may or may not
373     *                    be selected.
374     * @param adapterPosition  The adapter position of viewHolder inside adapter.
375     * @param selectedPosition The adapter position of currently selected row.
376     * @param selectedSubPosition The sub position within currently selected row.  This is used
377     *                            When a row has multiple alignment positions.
378     */
379    protected void onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter presenter,
380            FullWidthDetailsOverviewRowPresenter.ViewHolder viewHolder, int adapterPosition,
381            int selectedPosition, int selectedSubPosition) {
382        if (selectedPosition > adapterPosition) {
383            presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_HALF);
384        } else if (selectedPosition == adapterPosition && selectedSubPosition == 1) {
385            presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_HALF);
386        } else if (selectedPosition == adapterPosition && selectedSubPosition == 0){
387            presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_FULL);
388        } else {
389            presenter.setState(viewHolder,
390                    FullWidthDetailsOverviewRowPresenter.STATE_SMALL);
391        }
392    }
393
394    @Override
395    public void onStart() {
396        super.onStart();
397        setupChildFragmentLayout();
398        setupFocusSearchListener();
399        if (isEntranceTransitionEnabled()) {
400            mRowsFragment.setEntranceTransitionState(false);
401        }
402    }
403
404    @Override
405    protected Object createEntranceTransition() {
406        return TransitionHelper.loadTransition(getActivity(),
407                R.transition.lb_details_enter_transition);
408    }
409
410    @Override
411    protected void runEntranceTransition(Object entranceTransition) {
412        TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
413    }
414
415    @Override
416    protected void onEntranceTransitionEnd() {
417        mRowsFragment.onTransitionEnd();
418    }
419
420    @Override
421    protected void onEntranceTransitionPrepare() {
422        mRowsFragment.onTransitionPrepare();
423    }
424
425    @Override
426    protected void onEntranceTransitionStart() {
427        mRowsFragment.onTransitionStart();
428    }
429}
430