1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.leanback.widget;
18 
19 import android.app.Activity;
20 import android.content.Intent;
21 import android.graphics.Color;
22 import android.os.Bundle;
23 import android.util.Log;
24 import android.view.View;
25 import android.view.View.OnFocusChangeListener;
26 import android.view.ViewGroup;
27 import android.widget.TextView;
28 
29 import androidx.leanback.test.R;
30 import androidx.recyclerview.widget.RecyclerView;
31 
32 import java.util.ArrayList;
33 
34 public class GridActivity extends Activity {
35 
36     private static final String TAG = "GridActivity";
37 
38     interface ImportantForAccessibilityListener {
onImportantForAccessibilityChanged(View view, int newValue)39         void onImportantForAccessibilityChanged(View view, int newValue);
40     }
41 
42     interface AdapterListener {
onBind(RecyclerView.ViewHolder vh, int position)43         void onBind(RecyclerView.ViewHolder vh, int position);
44     }
45 
46     public static final String EXTRA_LAYOUT_RESOURCE_ID = "layoutResourceId";
47     public static final String EXTRA_NUM_ITEMS = "numItems";
48     public static final String EXTRA_ITEMS = "items";
49     public static final String EXTRA_ITEMS_FOCUSABLE = "itemsFocusable";
50     public static final String EXTRA_STAGGERED = "staggered";
51     public static final String EXTRA_REQUEST_LAYOUT_ONFOCUS = "requestLayoutOnFocus";
52     public static final String EXTRA_REQUEST_FOCUS_ONLAYOUT = "requstFocusOnLayout";
53     public static final String EXTRA_CHILD_LAYOUT_ID = "childLayoutId";
54     public static final String EXTRA_SECONDARY_SIZE_ZERO = "secondarySizeZero";
55     public static final String EXTRA_UPDATE_SIZE = "updateSize";
56     public static final String EXTRA_UPDATE_SIZE_SECONDARY = "updateSizeSecondary";
57     public static final String EXTRA_LAYOUT_MARGINS = "layoutMargins";
58     public static final String EXTRA_NINEPATCH_SHADOW = "NINEPATCH_SHADOW";
59     public static final String EXTRA_HAS_STABLE_IDS = "hasStableIds";
60 
61     /**
62      * Class that implements GridWidgetTest.ViewTypeProvider for creating different
63      * view types for each position.
64      */
65     public static final String EXTRA_VIEWTYPEPROVIDER_CLASS = "viewtype_class";
66     /**
67      * Class that implements GridWidgetTest.ItemAlignmentFacetProvider for creating different
68      * ItemAlignmentFacet for each ViewHolder.
69      */
70     public static final String EXTRA_ITEMALIGNMENTPROVIDER_CLASS = "itemalignment_class";
71     /**
72      * Class that implements GridWidgetTest.ItemAlignmentFacetProvider for creating different
73      * ItemAlignmentFacet for a given viewType.
74      */
75     public static final String EXTRA_ITEMALIGNMENTPROVIDER_VIEWTYPE_CLASS =
76             "itemalignment_viewtype_class";
77     public static final String SELECT_ACTION = "android.test.leanback.widget.SELECT";
78 
79     static final int DEFAULT_NUM_ITEMS = 100;
80     static final boolean DEFAULT_STAGGERED = true;
81     static final boolean DEFAULT_REQUEST_LAYOUT_ONFOCUS = false;
82     static final boolean DEFAULT_REQUEST_FOCUS_ONLAYOUT = false;
83 
84     private static final boolean DEBUG = false;
85 
86     int mLayoutId;
87     int mOrientation;
88     int mNumItems;
89     int mChildLayout;
90     boolean mStaggered;
91     boolean mRequestLayoutOnFocus;
92     boolean mRequestFocusOnLayout;
93     boolean mSecondarySizeZero;
94     GridWidgetTest.ViewTypeProvider mViewTypeProvider;
95     GridWidgetTest.ItemAlignmentFacetProvider mAlignmentProvider;
96     GridWidgetTest.ItemAlignmentFacetProvider mAlignmentViewTypeProvider;
97     AdapterListener mAdapterListener;
98     boolean mUpdateSize = true;
99     boolean mUpdateSizeSecondary = false;
100     boolean mHasStableIds;
101 
102     int[] mGridViewLayoutSize;
103     BaseGridView mGridView;
104     int[] mItemLengths;
105     boolean[] mItemFocusables;
106     int[] mLayoutMargins;
107     int mNinePatchShadow;
108 
109     private int mBoundCount;
110     ImportantForAccessibilityListener mImportantForAccessibilityListener;
111 
createView()112     private View createView() {
113 
114         View view = getLayoutInflater().inflate(mLayoutId, null, false);
115         mGridView = (BaseGridView) view.findViewById(R.id.gridview);
116         mOrientation = mGridView instanceof HorizontalGridView ? BaseGridView.HORIZONTAL :
117                 BaseGridView.VERTICAL;
118         mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_BOTH_EDGE);
119         mGridView.setWindowAlignmentOffsetPercent(35);
120         mGridView.setOnChildSelectedListener(new OnChildSelectedListener() {
121             @Override
122             public void onChildSelected(ViewGroup parent, View view, int position, long id) {
123                 if (DEBUG) Log.d(TAG, "onChildSelected position=" + position +  " id="+id);
124             }
125         });
126         if (mNinePatchShadow != 0) {
127             mGridView.setLayoutMode(ViewGroup.LAYOUT_MODE_OPTICAL_BOUNDS);
128         }
129         return view;
130     }
131 
132     @Override
onCreate(Bundle savedInstanceState)133     protected void onCreate(Bundle savedInstanceState) {
134         Intent intent = getIntent();
135 
136         mLayoutId = intent.getIntExtra(EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_grid);
137         mChildLayout = intent.getIntExtra(EXTRA_CHILD_LAYOUT_ID, -1);
138         mStaggered = intent.getBooleanExtra(EXTRA_STAGGERED, DEFAULT_STAGGERED);
139         mRequestLayoutOnFocus = intent.getBooleanExtra(EXTRA_REQUEST_LAYOUT_ONFOCUS,
140                 DEFAULT_REQUEST_LAYOUT_ONFOCUS);
141         mRequestFocusOnLayout = intent.getBooleanExtra(EXTRA_REQUEST_FOCUS_ONLAYOUT,
142                 DEFAULT_REQUEST_FOCUS_ONLAYOUT);
143         mUpdateSize = intent.getBooleanExtra(EXTRA_UPDATE_SIZE, true);
144         mUpdateSizeSecondary = intent.getBooleanExtra(EXTRA_UPDATE_SIZE_SECONDARY, false);
145         mSecondarySizeZero = intent.getBooleanExtra(EXTRA_SECONDARY_SIZE_ZERO, false);
146         mItemLengths = intent.getIntArrayExtra(EXTRA_ITEMS);
147         mHasStableIds = intent.getBooleanExtra(EXTRA_HAS_STABLE_IDS, false);
148         mItemFocusables = intent.getBooleanArrayExtra(EXTRA_ITEMS_FOCUSABLE);
149         mLayoutMargins = intent.getIntArrayExtra(EXTRA_LAYOUT_MARGINS);
150         String alignmentClass = intent.getStringExtra(EXTRA_ITEMALIGNMENTPROVIDER_CLASS);
151         String alignmentViewTypeClass =
152                 intent.getStringExtra(EXTRA_ITEMALIGNMENTPROVIDER_VIEWTYPE_CLASS);
153         String viewTypeClass = intent.getStringExtra(EXTRA_VIEWTYPEPROVIDER_CLASS);
154         mNinePatchShadow = intent.getIntExtra(EXTRA_NINEPATCH_SHADOW, 0);
155         try {
156             if (alignmentClass != null) {
157                 mAlignmentProvider = (GridWidgetTest.ItemAlignmentFacetProvider)
158                         Class.forName(alignmentClass).newInstance();
159             }
160             if (alignmentViewTypeClass != null) {
161                 mAlignmentViewTypeProvider = (GridWidgetTest.ItemAlignmentFacetProvider)
162                         Class.forName(alignmentViewTypeClass).newInstance();
163             }
164             if (viewTypeClass != null) {
165                 mViewTypeProvider = (GridWidgetTest.ViewTypeProvider)
166                         Class.forName(viewTypeClass).newInstance();
167             }
168         } catch (ClassNotFoundException ex) {
169             throw new RuntimeException(ex);
170         } catch (InstantiationException ex) {
171             throw new RuntimeException(ex);
172         } catch (IllegalAccessException ex) {
173             throw new RuntimeException(ex);
174         }
175 
176         super.onCreate(savedInstanceState);
177 
178         if (DEBUG) Log.v(TAG, "onCreate " + this);
179 
180         RecyclerView.Adapter adapter = new MyAdapter();
181         adapter.setHasStableIds(mHasStableIds);
182 
183         View view = createView();
184         if (mItemLengths == null) {
185             mNumItems = intent.getIntExtra(EXTRA_NUM_ITEMS, DEFAULT_NUM_ITEMS);
186             mItemLengths = new int[mNumItems];
187             for (int i = 0; i < mItemLengths.length; i++) {
188                 if (mOrientation == BaseGridView.HORIZONTAL) {
189                     mItemLengths[i] = mStaggered ? (int)(Math.random() * 180) + 180 : 240;
190                 } else {
191                     mItemLengths[i] = mStaggered ? (int)(Math.random() * 120) + 120 : 160;
192                 }
193             }
194         } else {
195             mNumItems = mItemLengths.length;
196         }
197 
198         mGridView.setAdapter(adapter);
199         setContentView(view);
200     }
201 
rebindToNewAdapter()202     void rebindToNewAdapter() {
203         mGridView.setAdapter(new MyAdapter());
204     }
205 
206     @Override
onNewIntent(Intent intent)207     protected void onNewIntent(Intent intent) {
208         if (DEBUG) Log.v(TAG, "onNewIntent " + intent+ " "+this);
209         if (intent.getAction().equals(SELECT_ACTION)) {
210             int position = intent.getIntExtra("SELECT_POSITION", -1);
211             if (position >= 0) {
212                 mGridView.setSelectedPosition(position);
213             }
214         }
215         super.onNewIntent(intent);
216     }
217 
218     private OnFocusChangeListener mItemFocusChangeListener = new OnFocusChangeListener() {
219 
220         @Override
221         public void onFocusChange(View v, boolean hasFocus) {
222             if (hasFocus) {
223                 v.setBackgroundColor(Color.YELLOW);
224             } else {
225                 v.setBackgroundColor(Color.LTGRAY);
226             }
227             if (mRequestLayoutOnFocus) {
228                 RecyclerView.ViewHolder vh = mGridView.getChildViewHolder(v);
229                 int position = vh.getAdapterPosition();
230                 updateSize(v, position);
231             }
232         }
233     };
234 
235     private OnFocusChangeListener mSubItemFocusChangeListener = new OnFocusChangeListener() {
236 
237         @Override
238         public void onFocusChange(View v, boolean hasFocus) {
239             if (hasFocus) {
240                 v.setBackgroundColor(Color.YELLOW);
241             } else {
242                 v.setBackgroundColor(Color.LTGRAY);
243             }
244         }
245     };
246 
resetBoundCount()247     void resetBoundCount() {
248         mBoundCount = 0;
249     }
250 
getBoundCount()251     int getBoundCount() {
252        return mBoundCount;
253     }
254 
swap(int index1, int index2)255     void swap(int index1, int index2) {
256         if (index1 == index2) {
257             return;
258         } else if (index1 > index2) {
259             int index = index1;
260             index1 = index2;
261             index2 = index;
262         }
263         int value = mItemLengths[index1];
264         mItemLengths[index1] = mItemLengths[index2];
265         mItemLengths[index2] = value;
266         mGridView.getAdapter().notifyItemMoved(index1, index2);
267         mGridView.getAdapter().notifyItemMoved(index2 - 1, index1);
268     }
269 
moveItem(int index1, int index2, boolean notify)270     void moveItem(int index1, int index2, boolean notify) {
271         if (index1 == index2) {
272             return;
273         }
274         int[] items = removeItems(index1, 1, false);
275         addItems(index2, items, false);
276         if (notify) {
277             mGridView.getAdapter().notifyItemMoved(index1, index2);
278         }
279     }
280 
changeArraySize(int length)281     void changeArraySize(int length) {
282         mNumItems = length;
283         mGridView.getAdapter().notifyDataSetChanged();
284     }
285 
removeItems(int index, int length)286     int[] removeItems(int index, int length) {
287         return removeItems(index, length, true);
288     }
289 
removeItems(int index, int length, boolean notify)290     int[] removeItems(int index, int length, boolean notify) {
291         int[] removed = new int[length];
292         System.arraycopy(mItemLengths, index, removed, 0, length);
293         System.arraycopy(mItemLengths, index + length, mItemLengths, index,
294                 mNumItems - index - length);
295         mNumItems -= length;
296         if (mGridView.getAdapter() != null && notify) {
297             mGridView.getAdapter().notifyItemRangeRemoved(index, length);
298         }
299         return removed;
300     }
301 
attachToNewAdapter(int[] items)302     void attachToNewAdapter(int[] items) {
303         mItemLengths = items;
304         mNumItems = items.length;
305         mGridView.setAdapter(new MyAdapter());
306     }
307 
308 
changeItem(int position, int itemValue)309     void changeItem(int position, int itemValue) {
310         mItemLengths[position] = itemValue;
311         if (mGridView.getAdapter() != null) {
312             mGridView.getAdapter().notifyItemChanged(position);
313         }
314     }
315 
addItems(int index, int[] items)316     void addItems(int index, int[] items) {
317         addItems(index, items, true);
318     }
319 
addItems(int index, int[] items, boolean notify)320     void addItems(int index, int[] items, boolean notify) {
321         int length = items.length;
322         if (mItemLengths.length < mNumItems + length) {
323             int[] array = new int[mNumItems + length];
324             System.arraycopy(mItemLengths, 0, array, 0, mNumItems);
325             mItemLengths = array;
326         }
327         System.arraycopy(mItemLengths, index, mItemLengths, index + length, mNumItems - index);
328         System.arraycopy(items, 0, mItemLengths, index, length);
329         mNumItems += length;
330         if (notify && mGridView.getAdapter() != null) {
331             mGridView.getAdapter().notifyItemRangeInserted(index, length);
332         }
333     }
334 
335     class MyAdapter extends RecyclerView.Adapter implements FacetProviderAdapter {
336 
337         @Override
getItemViewType(int position)338         public int getItemViewType(int position) {
339             if (mViewTypeProvider != null) {
340                 return mViewTypeProvider.getViewType(position);
341             }
342             return 0;
343         }
344 
345         @Override
getFacetProvider(int viewType)346         public FacetProvider getFacetProvider(int viewType) {
347             final Object alignmentFacet = mAlignmentViewTypeProvider != null
348                     ? mAlignmentViewTypeProvider.getItemAlignmentFacet(viewType) : null;
349             if (alignmentFacet != null) {
350                 return new FacetProvider() {
351                     @Override
352                     public Object getFacet(Class facetClass) {
353                         if (facetClass.equals(ItemAlignmentFacet.class)) {
354                             return alignmentFacet;
355                         }
356                         return null;
357                     }
358                 };
359             }
360             return null;
361         }
362 
363         @Override
364         public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
365             if (DEBUG) Log.v(TAG, "createViewHolder " + viewType);
366             View itemView;
367             if (mChildLayout != -1) {
368                 final View view = getLayoutInflater().inflate(mChildLayout, parent, false);
369                 ArrayList<View> focusables = new ArrayList<View>();
370                 view.addFocusables(focusables, View.FOCUS_UP);
371                 for (int i = 0; i < focusables.size(); i++) {
372                     View f = focusables.get(i);
373                     f.setBackgroundColor(Color.LTGRAY);
374                     f.setOnFocusChangeListener(new OnFocusChangeListener() {
375                         @Override
376                         public void onFocusChange(View v, boolean hasFocus) {
377                             if (hasFocus) {
378                                 v.setBackgroundColor(Color.YELLOW);
379                             } else {
380                                 v.setBackgroundColor(Color.LTGRAY);
381                             }
382                             if (mRequestLayoutOnFocus) {
383                                 if (v == view) {
384                                     RecyclerView.ViewHolder vh = mGridView.getChildViewHolder(v);
385                                     int position = vh.getAdapterPosition();
386                                     updateSize(v, position);
387                                 }
388                                 view.requestLayout();
389                             }
390                         }
391                     });
392                 }
393                 itemView = view;
394             } else {
395                 TextView textView = new TextView(parent.getContext()) {
396                     @Override
397                     protected void onLayout(boolean change, int left, int top, int right,
398                             int bottom) {
399                         super.onLayout(change, left, top, right, bottom);
400                         if (mRequestFocusOnLayout) {
401                             if (hasFocus()) {
402                                 clearFocus();
403                                 requestFocus();
404                             }
405                         }
406                     }
407 
408                     @Override
409                     public void setImportantForAccessibility(int mode) {
410                         super.setImportantForAccessibility(mode);
411                         if (mImportantForAccessibilityListener != null) {
412                             mImportantForAccessibilityListener.onImportantForAccessibilityChanged(
413                                     this, mode);
414                         }
415                     }
416                 };
417                 textView.setTextColor(Color.BLACK);
418                 textView.setOnFocusChangeListener(mItemFocusChangeListener);
419                 itemView = textView;
420             }
421             if (mLayoutMargins != null) {
422                 ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)
423                         itemView.getLayoutParams();
424                 if (lp == null) {
425                     lp = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
426                             ViewGroup.LayoutParams.WRAP_CONTENT);
427                 }
428                 lp.leftMargin = mLayoutMargins[0];
429                 lp.topMargin = mLayoutMargins[1];
430                 lp.rightMargin = mLayoutMargins[2];
431                 lp.bottomMargin = mLayoutMargins[3];
432                 itemView.setLayoutParams(lp);
433             }
434             if (mNinePatchShadow != 0) {
435                 ViewGroup viewGroup = (ViewGroup) itemView;
436                 View shadow = new View(viewGroup.getContext());
437                 shadow.setBackgroundResource(mNinePatchShadow);
438                 viewGroup.addView(shadow);
439                 viewGroup.setLayoutMode(ViewGroup.LAYOUT_MODE_OPTICAL_BOUNDS);
440             }
441             return new ViewHolder(itemView);
442         }
443 
444         @Override
445         public void onBindViewHolder(RecyclerView.ViewHolder baseHolder, int position) {
446             if (DEBUG) Log.v(TAG, "bindViewHolder " + position + " " + baseHolder);
447             mBoundCount++;
448             ViewHolder holder = (ViewHolder) baseHolder;
449             if (mAlignmentProvider != null) {
450                 holder.mItemAlignment = mAlignmentProvider.getItemAlignmentFacet(position);
451             } else {
452                 holder.mItemAlignment = null;
453             }
454             if (mChildLayout == -1) {
455                 ((TextView) holder.itemView).setText("Item "+mItemLengths[position]
456                         + " type=" + getItemViewType(position));
457                 boolean focusable = true;
458                 if (mItemFocusables != null) {
459                     focusable = mItemFocusables[position];
460                 }
461                 ((TextView) holder.itemView).setFocusable(focusable);
462                 ((TextView) holder.itemView).setFocusableInTouchMode(focusable);
463                 holder.itemView.setBackgroundColor(Color.LTGRAY);
464             } else {
465                 if (holder.itemView instanceof TextView) {
466                     ((TextView) holder.itemView).setText("Item "+mItemLengths[position]
467                             + " type=" + getItemViewType(position));
468                 }
469             }
470             updateSize(holder.itemView, position);
471             if (mAdapterListener != null) {
472                 mAdapterListener.onBind(baseHolder, position);
473             }
474         }
475 
476         @Override
477         public int getItemCount() {
478             return mNumItems;
479         }
480 
481         @Override
482         public long getItemId(int position) {
483             if (!mHasStableIds) return -1;
484             return position;
485         }
486     }
487 
488     void updateSize(View view, int position) {
489         if (!mUpdateSize && !mUpdateSizeSecondary) {
490             return;
491         }
492         ViewGroup.LayoutParams p = view.getLayoutParams();
493         if (p == null) {
494             p = new ViewGroup.LayoutParams(0, 0);
495         }
496         if (mOrientation == BaseGridView.HORIZONTAL) {
497             p.width = mItemLengths[position]
498                     + (mUpdateSize && mRequestLayoutOnFocus && view.hasFocus() ? 1 : 0);
499             p.height = mSecondarySizeZero ? 0
500                     : (mUpdateSizeSecondary && mRequestLayoutOnFocus && view.hasFocus() ? 96 : 80);
501         } else {
502             p.width = mSecondarySizeZero ? 0
503                     : (mUpdateSizeSecondary && mRequestLayoutOnFocus && view.hasFocus()
504                             ? 260 : 240);
505             p.height = mItemLengths[position] + (mRequestLayoutOnFocus && view.hasFocus() ? 1 : 0);
506         }
507         view.setLayoutParams(p);
508     }
509 
510     static class ViewHolder extends RecyclerView.ViewHolder implements FacetProvider {
511 
512         ItemAlignmentFacet mItemAlignment;
513         public ViewHolder(View v) {
514             super(v);
515         }
516 
517         @Override
518         public Object getFacet(Class facetClass) {
519             if (facetClass.equals(ItemAlignmentFacet.class)) {
520                 return mItemAlignment;
521             }
522             return null;
523         }
524     }
525 }
526