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  */
14 package androidx.leanback.widget;
15 
16 import android.content.Context;
17 import android.util.Log;
18 import android.view.LayoutInflater;
19 import android.view.View;
20 import android.view.ViewGroup;
21 
22 import androidx.leanback.R;
23 import androidx.leanback.system.Settings;
24 import androidx.leanback.transition.TransitionHelper;
25 
26 /**
27  * A presenter that renders objects in a {@link VerticalGridView}.
28  */
29 public 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
onCreate(ItemBridgeAdapter.ViewHolder viewHolder)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
onBind(final ItemBridgeAdapter.ViewHolder itemViewHolder)46         public void onBind(final ItemBridgeAdapter.ViewHolder itemViewHolder) {
47             // Only when having an OnItemClickListener, 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
onUnbind(ItemBridgeAdapter.ViewHolder viewHolder)64         public void onUnbind(ItemBridgeAdapter.ViewHolder viewHolder) {
65             if (getOnItemViewClickedListener() != null) {
66                 viewHolder.mHolder.view.setOnClickListener(null);
67             }
68         }
69 
70         @Override
onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder)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 
ViewHolder(VerticalGridView view)84         public ViewHolder(VerticalGridView view) {
85             super(view);
86             mGridView = view;
87         }
88 
getGridView()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     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      */
VerticalGridPresenter()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      */
VerticalGridPresenter(int focusZoomFactor)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      */
VerticalGridPresenter(int focusZoomFactor, boolean useFocusDimmer)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      */
setNumberOfColumns(int numColumns)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      */
getNumberOfColumns()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      */
setShadowEnabled(boolean enabled)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      */
getShadowEnabled()178     public final boolean getShadowEnabled() {
179         return mShadowEnabled;
180     }
181 
182     /**
183      * Default implementation returns true if SDK version >= 21, shadow (either static or z-order
184      * based) will be applied to each individual child of {@link VerticalGridView}.
185      * Subclass may return false to disable default implementation of shadow and provide its own.
186      */
isUsingDefaultShadow()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      */
enableChildRoundedCorners(boolean enable)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      */
areChildRoundedCornersEnabled()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      */
isUsingZOrder(Context context)211     public boolean isUsingZOrder(Context context) {
212         return !Settings.getInstance(context).preferStaticShadows();
213     }
214 
needsDefaultShadow()215     final boolean needsDefaultShadow() {
216         return isUsingDefaultShadow() && getShadowEnabled();
217     }
218 
219     /**
220      * Returns the zoom factor used for focus highlighting.
221      */
getFocusZoomFactor()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      */
isFocusDimmerUsed()229     public final boolean isFocusDimmerUsed() {
230         return mUseFocusDimmer;
231     }
232 
233     @Override
onCreateViewHolder(ViewGroup parent)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      */
createGridViewHolder(ViewGroup parent)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      */
initializeGridViewHolder(ViewHolder vh)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_DYNAMIC);
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      */
setKeepChildForeground(boolean keep)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      */
getKeepChildForeground()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 rounded corner.
328      */
createShadowOverlayOptions()329     protected ShadowOverlayHelper.Options createShadowOverlayOptions() {
330         return ShadowOverlayHelper.Options.DEFAULT;
331     }
332 
333     @Override
onBindViewHolder(Presenter.ViewHolder viewHolder, Object item)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
onUnbindViewHolder(Presenter.ViewHolder viewHolder)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      */
setOnItemViewSelectedListener(OnItemViewSelectedListener listener)353     public final void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
354         mOnItemViewSelectedListener = listener;
355     }
356 
357     /**
358      * Returns the item selected listener.
359      */
getOnItemViewSelectedListener()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      */
setOnItemViewClickedListener(OnItemViewClickedListener listener)370     public final void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
371         mOnItemViewClickedListener = listener;
372     }
373 
374     /**
375      * Returns the item clicked listener.
376      */
getOnItemViewClickedListener()377     public final OnItemViewClickedListener getOnItemViewClickedListener() {
378         return mOnItemViewClickedListener;
379     }
380 
selectChildView(ViewHolder vh, View view)381     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      */
setEntranceTransitionState(VerticalGridPresenter.ViewHolder holder, boolean afterEntrance)402     public void setEntranceTransitionState(VerticalGridPresenter.ViewHolder holder,
403             boolean afterEntrance) {
404         holder.mGridView.setChildrenVisibility(afterEntrance? View.VISIBLE : View.INVISIBLE);
405     }
406 }
407