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