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