RowsFragment.java revision 42752c860a26deacca04ea9ebeb00ddb4d8ce2fc
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 java.util.ArrayList;
17
18import android.animation.TimeAnimator;
19import android.animation.TimeAnimator.TimeListener;
20import android.os.Bundle;
21import android.support.v17.leanback.R;
22import android.support.v17.leanback.widget.ItemBridgeAdapter;
23import android.support.v17.leanback.widget.OnItemViewClickedListener;
24import android.support.v17.leanback.widget.OnItemViewSelectedListener;
25import android.support.v17.leanback.widget.RowPresenter.ViewHolder;
26import android.support.v17.leanback.widget.VerticalGridView;
27import android.support.v17.leanback.widget.HorizontalGridView;
28import android.support.v17.leanback.widget.OnItemSelectedListener;
29import android.support.v17.leanback.widget.OnItemClickedListener;
30import android.support.v17.leanback.widget.RowPresenter;
31import android.support.v17.leanback.widget.ListRowPresenter;
32import android.support.v17.leanback.widget.Presenter;
33import android.support.v7.widget.RecyclerView;
34import android.util.Log;
35import android.view.View;
36import android.view.ViewGroup;
37import android.view.ViewTreeObserver;
38import android.view.animation.DecelerateInterpolator;
39import android.view.animation.Interpolator;
40
41/**
42 * An ordered set of rows of leanback widgets.
43 */
44public class RowsFragment extends BaseRowFragment {
45
46    /**
47     * Internal helper class that manages row select animation and apply a default
48     * dim to each row.
49     */
50    final class RowViewHolderExtra implements TimeListener {
51        final RowPresenter mRowPresenter;
52        final Presenter.ViewHolder mRowViewHolder;
53
54        final TimeAnimator mSelectAnimator = new TimeAnimator();
55
56        int mSelectAnimatorDurationInUse;
57        Interpolator mSelectAnimatorInterpolatorInUse;
58        float mSelectLevelAnimStart;
59        float mSelectLevelAnimDelta;
60
61        RowViewHolderExtra(ItemBridgeAdapter.ViewHolder ibvh) {
62            mRowPresenter = (RowPresenter) ibvh.getPresenter();
63            mRowViewHolder = ibvh.getViewHolder();
64            mSelectAnimator.setTimeListener(this);
65        }
66
67        @Override
68        public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
69            if (mSelectAnimator.isRunning()) {
70                updateSelect(totalTime, deltaTime);
71            }
72        }
73
74        void updateSelect(long totalTime, long deltaTime) {
75            float fraction;
76            if (totalTime >= mSelectAnimatorDurationInUse) {
77                fraction = 1;
78                mSelectAnimator.end();
79            } else {
80                fraction = (float) (totalTime / (double) mSelectAnimatorDurationInUse);
81            }
82            if (mSelectAnimatorInterpolatorInUse != null) {
83                fraction = mSelectAnimatorInterpolatorInUse.getInterpolation(fraction);
84            }
85            float level =  mSelectLevelAnimStart + fraction * mSelectLevelAnimDelta;
86            mRowPresenter.setSelectLevel(mRowViewHolder, level);
87        }
88
89        void animateSelect(boolean select, boolean immediate) {
90            endSelectAnimation();
91            final float end = select ? 1 : 0;
92            if (immediate) {
93                mRowPresenter.setSelectLevel(mRowViewHolder, end);
94            } else if (mRowPresenter.getSelectLevel(mRowViewHolder) != end) {
95                mSelectAnimatorDurationInUse = mSelectAnimatorDuration;
96                mSelectAnimatorInterpolatorInUse = mSelectAnimatorInterpolator;
97                mSelectLevelAnimStart = mRowPresenter.getSelectLevel(mRowViewHolder);
98                mSelectLevelAnimDelta = end - mSelectLevelAnimStart;
99                mSelectAnimator.start();
100            }
101        }
102
103        void endAnimations() {
104            endSelectAnimation();
105        }
106
107        void endSelectAnimation() {
108            mSelectAnimator.end();
109        }
110
111    }
112
113    private static final String TAG = "RowsFragment";
114    private static final boolean DEBUG = false;
115
116    private ItemBridgeAdapter.ViewHolder mSelectedViewHolder;
117    private boolean mExpand = true;
118    private boolean mViewsCreated;
119    private float mRowScaleFactor;
120    private boolean mRowScaleEnabled;
121
122    private OnItemSelectedListener mOnItemSelectedListener;
123    private OnItemViewSelectedListener mOnItemViewSelectedListener;
124    private OnItemClickedListener mOnItemClickedListener;
125    private OnItemViewClickedListener mOnItemViewClickedListener;
126
127    // Select animation and interpolator are not intended to be
128    // exposed at this moment. They might be synced with vertical scroll
129    // animation later.
130    int mSelectAnimatorDuration;
131    Interpolator mSelectAnimatorInterpolator = new DecelerateInterpolator(2);
132
133    private RecyclerView.RecycledViewPool mRecycledViewPool;
134    private ArrayList<Presenter> mPresenterMapper;
135
136    private ItemBridgeAdapter.AdapterListener mExternalAdapterListener;
137
138    /**
139     * Sets an item clicked listener on the fragment.
140     * OnItemClickedListener will override {@link View.OnClickListener} that
141     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
142     * So in general,  developer should choose one of the listeners but not both.
143     * @deprecated Use {@link #setOnItemViewClickedListener(OnItemViewClickedListener)}
144     */
145    public void setOnItemClickedListener(OnItemClickedListener listener) {
146        mOnItemClickedListener = listener;
147        if (mViewsCreated) {
148            throw new IllegalStateException(
149                    "Item clicked listener must be set before views are created");
150        }
151    }
152
153    /**
154     * Returns the item clicked listener.
155     * @deprecated Use {@link #getOnItemClickedListener()}
156     */
157    public OnItemClickedListener getOnItemClickedListener() {
158        return mOnItemClickedListener;
159    }
160
161    /**
162     * Sets an item clicked listener on the fragment.
163     * OnItemViewClickedListener will override {@link View.OnClickListener} that
164     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
165     * So in general,  developer should choose one of the listeners but not both.
166     */
167    public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
168        mOnItemViewClickedListener = listener;
169        if (mViewsCreated) {
170            throw new IllegalStateException(
171                    "Item clicked listener must be set before views are created");
172        }
173    }
174
175    /**
176     * Returns the item clicked listener.
177     */
178    public OnItemViewClickedListener getOnItemViewClickedListener() {
179        return mOnItemViewClickedListener;
180    }
181
182    /**
183     * Set the visibility of titles/hovercard of browse rows.
184     */
185    public void setExpand(boolean expand) {
186        mExpand = expand;
187        VerticalGridView listView = getVerticalGridView();
188        if (listView != null) {
189            updateRowScaling(!expand);
190            final int count = listView.getChildCount();
191            if (DEBUG) Log.v(TAG, "setExpand " + expand + " count " + count);
192            for (int i = 0; i < count; i++) {
193                View view = listView.getChildAt(i);
194                ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) listView.getChildViewHolder(view);
195                setRowViewExpanded(vh, mExpand);
196            }
197        }
198    }
199
200    /**
201     * Sets an item selection listener.
202     * @deprecated Use {@link #setOnItemViewSelectedListener(OnItemViewSelectedListener)}
203     */
204    public void setOnItemSelectedListener(OnItemSelectedListener listener) {
205        mOnItemSelectedListener = listener;
206        VerticalGridView listView = getVerticalGridView();
207        if (listView != null) {
208            final int count = listView.getChildCount();
209            for (int i = 0; i < count; i++) {
210                View view = listView.getChildAt(i);
211                ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
212                        listView.getChildViewHolder(view);
213                setOnItemSelectedListener(vh, mOnItemSelectedListener);
214            }
215        }
216    }
217
218    /**
219     * Sets an item selection listener.
220     */
221    public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
222        mOnItemViewSelectedListener = listener;
223        VerticalGridView listView = getVerticalGridView();
224        if (listView != null) {
225            final int count = listView.getChildCount();
226            for (int i = 0; i < count; i++) {
227                View view = listView.getChildAt(i);
228                ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
229                        listView.getChildViewHolder(view);
230                setOnItemViewSelectedListener(vh, mOnItemViewSelectedListener);
231            }
232        }
233    }
234
235    /**
236     * Returns an item selection listener.
237     */
238    public OnItemViewSelectedListener getOnItemViewSelectedListener() {
239        return mOnItemViewSelectedListener;
240    }
241
242    /**
243     * Enables scaling of rows.
244     *
245     * @param enable true to enable row scaling
246     */
247    public void enableRowScaling(boolean enable) {
248        mRowScaleEnabled = enable;
249    }
250
251    @Override
252    void onRowSelected(ViewGroup parent, View view, int position, long id) {
253        VerticalGridView listView = getVerticalGridView();
254        if (listView == null) {
255            return;
256        }
257        ItemBridgeAdapter.ViewHolder vh = (view == null) ? null :
258            (ItemBridgeAdapter.ViewHolder) listView.getChildViewHolder(view);
259
260        if (mSelectedViewHolder != vh) {
261            if (DEBUG) Log.v(TAG, "new row selected position " + position + " view " + view);
262
263            if (mSelectedViewHolder != null) {
264                setRowViewSelected(mSelectedViewHolder, false, false);
265            }
266            mSelectedViewHolder = vh;
267            if (mSelectedViewHolder != null) {
268                setRowViewSelected(mSelectedViewHolder, true, false);
269            }
270        }
271    }
272
273    @Override
274    int getLayoutResourceId() {
275        return R.layout.lb_rows_fragment;
276    }
277
278    @Override
279    public void onCreate(Bundle savedInstanceState) {
280        super.onCreate(savedInstanceState);
281        mSelectAnimatorDuration = getResources().getInteger(
282                R.integer.lb_browse_rows_anim_duration);
283        mRowScaleFactor = getResources().getFraction(
284                R.fraction.lb_browse_rows_scale, 1, 1);
285    }
286
287    @Override
288    public void onViewCreated(View view, Bundle savedInstanceState) {
289        if (DEBUG) Log.v(TAG, "onViewCreated");
290        super.onViewCreated(view, savedInstanceState);
291        // Align the top edge of child with id row_content.
292        // Need set this for directly using RowsFragment.
293        getVerticalGridView().setItemAlignmentViewId(R.id.row_content);
294        getVerticalGridView().setSaveChildrenPolicy(VerticalGridView.SAVE_LIMITED_CHILD);
295
296        mRecycledViewPool = null;
297        mPresenterMapper = null;
298    }
299
300    @Override
301    void setItemAlignment() {
302        super.setItemAlignment();
303        if (getVerticalGridView() != null) {
304            getVerticalGridView().setItemAlignmentOffsetWithPadding(true);
305        }
306    }
307
308    void setExternalAdapterListener(ItemBridgeAdapter.AdapterListener listener) {
309        mExternalAdapterListener = listener;
310    }
311
312    private static void setRowViewExpanded(ItemBridgeAdapter.ViewHolder vh, boolean expanded) {
313        ((RowPresenter) vh.getPresenter()).setRowViewExpanded(vh.getViewHolder(), expanded);
314    }
315
316    private static void setRowViewSelected(ItemBridgeAdapter.ViewHolder vh, boolean selected,
317            boolean immediate) {
318        RowViewHolderExtra extra = (RowViewHolderExtra) vh.getExtraObject();
319        extra.animateSelect(selected, immediate);
320        ((RowPresenter) vh.getPresenter()).setRowViewSelected(vh.getViewHolder(), selected);
321    }
322
323    private static void setOnItemSelectedListener(ItemBridgeAdapter.ViewHolder vh,
324            OnItemSelectedListener listener) {
325        ((RowPresenter) vh.getPresenter()).setOnItemSelectedListener(listener);
326    }
327
328    private static void setOnItemViewSelectedListener(ItemBridgeAdapter.ViewHolder vh,
329            OnItemViewSelectedListener listener) {
330        ((RowPresenter) vh.getPresenter()).setOnItemViewSelectedListener(listener);
331    }
332
333    private final ItemBridgeAdapter.AdapterListener mBridgeAdapterListener =
334            new ItemBridgeAdapter.AdapterListener() {
335        @Override
336        public void onAddPresenter(Presenter presenter, int type) {
337            ((RowPresenter) presenter).setOnItemClickedListener(mOnItemClickedListener);
338            ((RowPresenter) presenter).setOnItemViewClickedListener(mOnItemViewClickedListener);
339            if (mExternalAdapterListener != null) {
340                mExternalAdapterListener.onAddPresenter(presenter, type);
341            }
342        }
343        @Override
344        public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
345            VerticalGridView listView = getVerticalGridView();
346            if (listView != null && ((RowPresenter) vh.getPresenter()).canDrawOutOfBounds()) {
347                listView.setClipChildren(false);
348            }
349            setupSharedViewPool(vh);
350            mViewsCreated = true;
351            vh.setExtraObject(new RowViewHolderExtra(vh));
352            // selected state is initialized to false, then driven by grid view onChildSelected
353            // events.  When there is rebind, grid view fires onChildSelected event properly.
354            // So we don't need do anything special later in onBind or onAttachedToWindow.
355            setRowViewSelected(vh, false, true);
356            if (mExternalAdapterListener != null) {
357                mExternalAdapterListener.onCreate(vh);
358            }
359        }
360        @Override
361        public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) {
362            if (DEBUG) Log.v(TAG, "onAttachToWindow");
363            // All views share the same mExpand value.  When we attach a view to grid view,
364            // we should make sure it pick up the latest mExpand value we set early on other
365            // attached views.  For no-structure-change update,  the view is rebound to new data,
366            // but again it should use the unchanged mExpand value,  so we don't need do any
367            // thing in onBind.
368            setRowViewExpanded(vh, mExpand);
369            setOnItemSelectedListener(vh, mOnItemSelectedListener);
370            setOnItemViewSelectedListener(vh, mOnItemViewSelectedListener);
371            if (mExternalAdapterListener != null) {
372                mExternalAdapterListener.onAttachedToWindow(vh);
373            }
374        }
375        @Override
376        public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) {
377            if (mSelectedViewHolder == vh) {
378                setRowViewSelected(mSelectedViewHolder, false, true);
379                mSelectedViewHolder = null;
380            }
381            if (mExternalAdapterListener != null) {
382                mExternalAdapterListener.onDetachedFromWindow(vh);
383            }
384        }
385        @Override
386        public void onBind(ItemBridgeAdapter.ViewHolder vh) {
387            if (mExternalAdapterListener != null) {
388                mExternalAdapterListener.onBind(vh);
389            }
390        }
391        @Override
392        public void onUnbind(ItemBridgeAdapter.ViewHolder vh) {
393            RowViewHolderExtra extra = (RowViewHolderExtra) vh.getExtraObject();
394            extra.endAnimations();
395            if (mExternalAdapterListener != null) {
396                mExternalAdapterListener.onUnbind(vh);
397            }
398        }
399    };
400
401    private void setupSharedViewPool(ItemBridgeAdapter.ViewHolder bridgeVh) {
402        RowPresenter rowPresenter = (RowPresenter) bridgeVh.getPresenter();
403        RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(bridgeVh.getViewHolder());
404
405        if (rowVh instanceof ListRowPresenter.ViewHolder) {
406            HorizontalGridView view = ((ListRowPresenter.ViewHolder) rowVh).getGridView();
407            // Recycled view pool is shared between all list rows
408            if (mRecycledViewPool == null) {
409                mRecycledViewPool = view.getRecycledViewPool();
410            } else {
411                view.setRecycledViewPool(mRecycledViewPool);
412            }
413
414            ItemBridgeAdapter bridgeAdapter =
415                    ((ListRowPresenter.ViewHolder) rowVh).getBridgeAdapter();
416            if (mPresenterMapper == null) {
417                mPresenterMapper = bridgeAdapter.getPresenterMapper();
418            } else {
419                bridgeAdapter.setPresenterMapper(mPresenterMapper);
420            }
421        }
422    }
423
424    @Override
425    void updateAdapter() {
426        super.updateAdapter();
427        mSelectedViewHolder = null;
428        mViewsCreated = false;
429
430        ItemBridgeAdapter adapter = getBridgeAdapter();
431        if (adapter != null) {
432            adapter.setAdapterListener(mBridgeAdapterListener);
433        }
434    }
435
436    @Override
437    void onTransitionStart() {
438        super.onTransitionStart();
439        freezeRows(true);
440    }
441
442    class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener {
443
444        final View mVerticalView;
445        final Runnable mCallback;
446        int mState;
447
448        final static int STATE_INIT = 0;
449        final static int STATE_FIRST_DRAW = 1;
450        final static int STATE_SECOND_DRAW = 2;
451
452        ExpandPreLayout(Runnable callback) {
453            mVerticalView = getVerticalGridView();
454            mCallback = callback;
455        }
456
457        void execute() {
458            mVerticalView.getViewTreeObserver().addOnPreDrawListener(this);
459            setExpand(false);
460            mState = STATE_INIT;
461        }
462
463        @Override
464        public boolean onPreDraw() {
465            if (mState == STATE_INIT) {
466                setExpand(true);
467                mState = STATE_FIRST_DRAW;
468            } else if (mState == STATE_FIRST_DRAW) {
469                mCallback.run();
470                mVerticalView.getViewTreeObserver().removeOnPreDrawListener(this);
471                mState = STATE_SECOND_DRAW;
472            }
473            return false;
474        }
475    }
476
477    void onExpandTransitionStart(boolean expand, final Runnable callback) {
478        onTransitionStart();
479        if (expand) {
480            callback.run();
481            return;
482        }
483        // Run a "pre" layout when we go non-expand, in order to get the initial
484        // positions of added rows.
485        new ExpandPreLayout(callback).execute();
486    }
487
488    private void updateRowScaling(boolean scale) {
489        VerticalGridView view = getVerticalGridView();
490        ((ViewGroup) view.getParent()).setClipChildren(!mRowScaleEnabled && scale);
491        view.setPrimaryOverReach((mRowScaleEnabled && scale) ? 1f / mRowScaleFactor : 1f);
492
493        final float scaleFactor = (mRowScaleEnabled && scale) ? mRowScaleFactor : 1f;
494        view.setScaleX(scaleFactor);
495        view.setScaleY(scaleFactor);
496    }
497
498    @Override
499    void onTransitionEnd() {
500        super.onTransitionEnd();
501        freezeRows(false);
502    }
503
504    private void freezeRows(boolean freeze) {
505        VerticalGridView verticalView = getVerticalGridView();
506        if (verticalView != null) {
507            final int count = verticalView.getChildCount();
508            for (int i = 0; i < count; i++) {
509                ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder)
510                    verticalView.getChildViewHolder(verticalView.getChildAt(i));
511                RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter();
512                RowPresenter.ViewHolder vh = rowPresenter.getRowViewHolder(ibvh.getViewHolder());
513                rowPresenter.freeze(vh, freeze);
514            }
515        }
516    }
517}
518