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.widget;
15
16import android.content.Context;
17import android.support.v17.leanback.R;
18import android.support.v17.leanback.system.Settings;
19import android.support.v17.leanback.transition.TransitionHelper;
20import android.util.Log;
21import android.view.LayoutInflater;
22import android.view.View;
23import android.view.ViewGroup;
24
25/**
26 * A presenter that renders objects in a {@link VerticalGridView}.
27 */
28public class VerticalGridPresenter extends Presenter {
29    private static final String TAG = "GridPresenter";
30    private static final boolean DEBUG = false;
31
32    class VerticalGridItemBridgeAdapter extends ItemBridgeAdapter {
33        @Override
34        protected void onCreate(ItemBridgeAdapter.ViewHolder viewHolder) {
35            if (viewHolder.itemView instanceof ViewGroup) {
36                TransitionHelper.setTransitionGroup((ViewGroup) viewHolder.itemView,
37                        true);
38            }
39            if (mShadowOverlayHelper != null) {
40                mShadowOverlayHelper.onViewCreated(viewHolder.itemView);
41            }
42        }
43
44        @Override
45        public void onBind(final ItemBridgeAdapter.ViewHolder itemViewHolder) {
46            // Only when having an OnItemClickListener, we attach the OnClickListener.
47            if (getOnItemViewClickedListener() != null) {
48                final View itemView = itemViewHolder.mHolder.view;
49                itemView.setOnClickListener(new View.OnClickListener() {
50                    @Override
51                    public void onClick(View view) {
52                        if (getOnItemViewClickedListener() != null) {
53                            // Row is always null
54                            getOnItemViewClickedListener().onItemClicked(
55                                    itemViewHolder.mHolder, itemViewHolder.mItem, null, null);
56                        }
57                    }
58                });
59            }
60        }
61
62        @Override
63        public void onUnbind(ItemBridgeAdapter.ViewHolder viewHolder) {
64            if (getOnItemViewClickedListener() != null) {
65                viewHolder.mHolder.view.setOnClickListener(null);
66            }
67        }
68
69        @Override
70        public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
71            viewHolder.itemView.setActivated(true);
72        }
73    }
74
75    /**
76     * ViewHolder for the VerticalGridPresenter.
77     */
78    public static class ViewHolder extends Presenter.ViewHolder {
79        ItemBridgeAdapter mItemBridgeAdapter;
80        final VerticalGridView mGridView;
81        boolean mInitialized;
82
83        public ViewHolder(VerticalGridView view) {
84            super(view);
85            mGridView = view;
86        }
87
88        public VerticalGridView getGridView() {
89            return mGridView;
90        }
91    }
92
93    private int mNumColumns = -1;
94    private int mFocusZoomFactor;
95    private boolean mUseFocusDimmer;
96    private boolean mShadowEnabled = true;
97    private boolean mKeepChildForeground = true;
98    private OnItemViewSelectedListener mOnItemViewSelectedListener;
99    private OnItemViewClickedListener mOnItemViewClickedListener;
100    private boolean mRoundedCornersEnabled = true;
101    ShadowOverlayHelper mShadowOverlayHelper;
102    private ItemBridgeAdapter.Wrapper mShadowOverlayWrapper;
103
104    /**
105     * Constructs a VerticalGridPresenter with defaults.
106     * Uses {@link FocusHighlight#ZOOM_FACTOR_MEDIUM} for focus zooming and
107     * enabled dimming on focus.
108     */
109    public VerticalGridPresenter() {
110        this(FocusHighlight.ZOOM_FACTOR_LARGE);
111    }
112
113    /**
114     * Constructs a VerticalGridPresenter with the given parameters.
115     *
116     * @param focusZoomFactor Controls the zoom factor used when an item view is focused. One of
117     *         {@link FocusHighlight#ZOOM_FACTOR_NONE},
118     *         {@link FocusHighlight#ZOOM_FACTOR_SMALL},
119     *         {@link FocusHighlight#ZOOM_FACTOR_XSMALL},
120     *         {@link FocusHighlight#ZOOM_FACTOR_MEDIUM},
121     *         {@link FocusHighlight#ZOOM_FACTOR_LARGE}
122     * enabled dimming on focus.
123     */
124    public VerticalGridPresenter(int focusZoomFactor) {
125        this(focusZoomFactor, true);
126    }
127
128    /**
129     * Constructs a VerticalGridPresenter with the given parameters.
130     *
131     * @param focusZoomFactor Controls the zoom factor used when an item view is focused. One of
132     *         {@link FocusHighlight#ZOOM_FACTOR_NONE},
133     *         {@link FocusHighlight#ZOOM_FACTOR_SMALL},
134     *         {@link FocusHighlight#ZOOM_FACTOR_XSMALL},
135     *         {@link FocusHighlight#ZOOM_FACTOR_MEDIUM},
136     *         {@link FocusHighlight#ZOOM_FACTOR_LARGE}
137     * @param useFocusDimmer determines if the FocusHighlighter will use the dimmer
138     */
139    public VerticalGridPresenter(int focusZoomFactor, boolean useFocusDimmer) {
140        mFocusZoomFactor = focusZoomFactor;
141        mUseFocusDimmer = useFocusDimmer;
142    }
143
144    /**
145     * Sets the number of columns in the vertical grid.
146     */
147    public void setNumberOfColumns(int numColumns) {
148        if (numColumns < 0) {
149            throw new IllegalArgumentException("Invalid number of columns");
150        }
151        if (mNumColumns != numColumns) {
152            mNumColumns = numColumns;
153        }
154    }
155
156    /**
157     * Returns the number of columns in the vertical grid.
158     */
159    public int getNumberOfColumns() {
160        return mNumColumns;
161    }
162
163    /**
164     * Enable or disable child shadow.
165     * This is not only for enable/disable default shadow implementation but also subclass must
166     * respect this flag.
167     */
168    public final void setShadowEnabled(boolean enabled) {
169        mShadowEnabled = enabled;
170    }
171
172    /**
173     * Returns true if child shadow is enabled.
174     * This is not only for enable/disable default shadow implementation but also subclass must
175     * respect this flag.
176     */
177    public final boolean getShadowEnabled() {
178        return mShadowEnabled;
179    }
180
181    /**
182     * Default implementation returns true if SDK version >= 21, shadow (either static or z-order
183     * based) will be applied to each individual child of {@link VerticalGridView}.
184     * Subclass may return false to disable default implementation of shadow and provide its own.
185     */
186    public boolean isUsingDefaultShadow() {
187        return ShadowOverlayHelper.supportsShadow();
188    }
189
190    /**
191     * Enables or disabled rounded corners on children of this row.
192     * Supported on Android SDK >= L.
193     */
194    public final void enableChildRoundedCorners(boolean enable) {
195        mRoundedCornersEnabled = enable;
196    }
197
198    /**
199     * Returns true if rounded corners are enabled for children of this row.
200     */
201    public final boolean areChildRoundedCornersEnabled() {
202        return mRoundedCornersEnabled;
203    }
204
205    /**
206     * Returns true if SDK >= L, where Z shadow is enabled so that Z order is enabled
207     * on each child of vertical grid.   If subclass returns false in isUsingDefaultShadow()
208     * and does not use Z-shadow on SDK >= L, it should override isUsingZOrder() return false.
209     */
210    public boolean isUsingZOrder(Context context) {
211        return !Settings.getInstance(context).preferStaticShadows();
212    }
213
214    final boolean needsDefaultShadow() {
215        return isUsingDefaultShadow() && getShadowEnabled();
216    }
217
218    /**
219     * Returns the zoom factor used for focus highlighting.
220     */
221    public final int getFocusZoomFactor() {
222        return mFocusZoomFactor;
223    }
224
225    /**
226     * Returns true if the focus dimmer is used for focus highlighting; false otherwise.
227     */
228    public final boolean isFocusDimmerUsed() {
229        return mUseFocusDimmer;
230    }
231
232    @Override
233    public final ViewHolder onCreateViewHolder(ViewGroup parent) {
234        ViewHolder vh = createGridViewHolder(parent);
235        vh.mInitialized = false;
236        vh.mItemBridgeAdapter = new VerticalGridItemBridgeAdapter();
237        initializeGridViewHolder(vh);
238        if (!vh.mInitialized) {
239            throw new RuntimeException("super.initializeGridViewHolder() must be called");
240        }
241        return vh;
242    }
243
244    /**
245     * Subclass may override this to inflate a different layout.
246     */
247    protected ViewHolder createGridViewHolder(ViewGroup parent) {
248        View root = LayoutInflater.from(parent.getContext()).inflate(
249                R.layout.lb_vertical_grid, parent, false);
250        return new ViewHolder((VerticalGridView) root.findViewById(R.id.browse_grid));
251    }
252
253    /**
254     * Called after a {@link VerticalGridPresenter.ViewHolder} is created.
255     * Subclasses may override this method and start by calling
256     * super.initializeGridViewHolder(ViewHolder).
257     *
258     * @param vh The ViewHolder to initialize for the vertical grid.
259     */
260    protected void initializeGridViewHolder(ViewHolder vh) {
261        if (mNumColumns == -1) {
262            throw new IllegalStateException("Number of columns must be set");
263        }
264        if (DEBUG) Log.v(TAG, "mNumColumns " + mNumColumns);
265        vh.getGridView().setNumColumns(mNumColumns);
266        vh.mInitialized = true;
267
268        Context context = vh.mGridView.getContext();
269        if (mShadowOverlayHelper == null) {
270            mShadowOverlayHelper = new ShadowOverlayHelper.Builder()
271                    .needsOverlay(mUseFocusDimmer)
272                    .needsShadow(needsDefaultShadow())
273                    .needsRoundedCorner(areChildRoundedCornersEnabled())
274                    .preferZOrder(isUsingZOrder(context))
275                    .keepForegroundDrawable(mKeepChildForeground)
276                    .options(createShadowOverlayOptions())
277                    .build(context);
278            if (mShadowOverlayHelper.needsWrapper()) {
279                mShadowOverlayWrapper = new ItemBridgeAdapterShadowOverlayWrapper(
280                        mShadowOverlayHelper);
281            }
282        }
283        vh.mItemBridgeAdapter.setWrapper(mShadowOverlayWrapper);
284        mShadowOverlayHelper.prepareParentForShadow(vh.mGridView);
285        vh.getGridView().setFocusDrawingOrderEnabled(mShadowOverlayHelper.getShadowType()
286                != ShadowOverlayHelper.SHADOW_DYNAMIC);
287        FocusHighlightHelper.setupBrowseItemFocusHighlight(vh.mItemBridgeAdapter,
288                mFocusZoomFactor, mUseFocusDimmer);
289
290        final ViewHolder gridViewHolder = vh;
291        vh.getGridView().setOnChildSelectedListener(new OnChildSelectedListener() {
292            @Override
293            public void onChildSelected(ViewGroup parent, View view, int position, long id) {
294                selectChildView(gridViewHolder, view);
295            }
296        });
297    }
298
299    /**
300     * Set if keeps foreground of child of this grid, the foreground will not
301     * be used for overlay color.  Default value is true.
302     *
303     * @param keep   True if keep foreground of child of this grid.
304     */
305    public final void setKeepChildForeground(boolean keep) {
306        mKeepChildForeground = keep;
307    }
308
309    /**
310     * Returns true if keeps foreground of child of this grid, the foreground will not
311     * be used for overlay color.  Default value is true.
312     *
313     * @return   True if keeps foreground of child of this grid.
314     */
315    public final boolean getKeepChildForeground() {
316        return mKeepChildForeground;
317    }
318
319    /**
320     * Create ShadowOverlayHelper Options.  Subclass may override.
321     * e.g.
322     * <code>
323     * return new ShadowOverlayHelper.Options().roundedCornerRadius(10);
324     * </code>
325     *
326     * @return   The options to be used for shadow, overlay and rounded corner.
327     */
328    protected ShadowOverlayHelper.Options createShadowOverlayOptions() {
329        return ShadowOverlayHelper.Options.DEFAULT;
330    }
331
332    @Override
333    public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
334        if (DEBUG) Log.v(TAG, "onBindViewHolder " + item);
335        ViewHolder vh = (ViewHolder) viewHolder;
336        vh.mItemBridgeAdapter.setAdapter((ObjectAdapter) item);
337        vh.getGridView().setAdapter(vh.mItemBridgeAdapter);
338    }
339
340    @Override
341    public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
342        if (DEBUG) Log.v(TAG, "onUnbindViewHolder");
343        ViewHolder vh = (ViewHolder) viewHolder;
344        vh.mItemBridgeAdapter.setAdapter(null);
345        vh.getGridView().setAdapter(null);
346    }
347
348    /**
349     * Sets the item selected listener.
350     * Since this is a grid the row parameter is always null.
351     */
352    public final void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
353        mOnItemViewSelectedListener = listener;
354    }
355
356    /**
357     * Returns the item selected listener.
358     */
359    public final OnItemViewSelectedListener getOnItemViewSelectedListener() {
360        return mOnItemViewSelectedListener;
361    }
362
363    /**
364     * Sets the item clicked listener.
365     * OnItemViewClickedListener will override {@link View.OnClickListener} that
366     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
367     * So in general, developer should choose one of the listeners but not both.
368     */
369    public final void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
370        mOnItemViewClickedListener = listener;
371    }
372
373    /**
374     * Returns the item clicked listener.
375     */
376    public final OnItemViewClickedListener getOnItemViewClickedListener() {
377        return mOnItemViewClickedListener;
378    }
379
380    void selectChildView(ViewHolder vh, View view) {
381        if (getOnItemViewSelectedListener() != null) {
382            ItemBridgeAdapter.ViewHolder ibh = (view == null) ? null :
383                    (ItemBridgeAdapter.ViewHolder) vh.getGridView().getChildViewHolder(view);
384            if (ibh == null) {
385                getOnItemViewSelectedListener().onItemSelected(null, null, null, null);
386            } else {
387                getOnItemViewSelectedListener().onItemSelected(ibh.mHolder, ibh.mItem, null, null);
388            }
389        }
390    }
391
392    /**
393     * Changes the visibility of views.  The entrance transition will be run against the views that
394     * change visibilities.  This method is called by the fragment, it should not be called
395     * directly by the application.
396     *
397     * @param holder         The ViewHolder for the vertical grid.
398     * @param afterEntrance  true if children of vertical grid participating in entrance transition
399     *                       should be set to visible, false otherwise.
400     */
401    public void setEntranceTransitionState(VerticalGridPresenter.ViewHolder holder,
402            boolean afterEntrance) {
403        holder.mGridView.setChildrenVisibility(afterEntrance? View.VISIBLE : View.INVISIBLE);
404    }
405}
406