Home | History | Annotate | Download | only in widget
      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 {
     39         void onImportantForAccessibilityChanged(View view, int newValue);
     40     }
     41 
     42     interface AdapterListener {
     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 
    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
    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 
    202     void rebindToNewAdapter() {
    203         mGridView.setAdapter(new MyAdapter());
    204     }
    205 
    206     @Override
    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 
    247     void resetBoundCount() {
    248         mBoundCount = 0;
    249     }
    250 
    251     int getBoundCount() {
    252        return mBoundCount;
    253     }
    254 
    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 
    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 
    281     void changeArraySize(int length) {
    282         mNumItems = length;
    283         mGridView.getAdapter().notifyDataSetChanged();
    284     }
    285 
    286     int[] removeItems(int index, int length) {
    287         return removeItems(index, length, true);
    288     }
    289 
    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 
    302     void attachToNewAdapter(int[] items) {
    303         mItemLengths = items;
    304         mNumItems = items.length;
    305         mGridView.setAdapter(new MyAdapter());
    306     }
    307 
    308 
    309     void changeItem(int position, int itemValue) {
    310         mItemLengths[position] = itemValue;
    311         if (mGridView.getAdapter() != null) {
    312             mGridView.getAdapter().notifyItemChanged(position);
    313         }
    314     }
    315 
    316     void addItems(int index, int[] items) {
    317         addItems(index, items, true);
    318     }
    319 
    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
    338         public int getItemViewType(int position) {
    339             if (mViewTypeProvider != null) {
    340                 return mViewTypeProvider.getViewType(position);
    341             }
    342             return 0;
    343         }
    344 
    345         @Override
    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