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