Home | History | Annotate | Download | only in deskclock
      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 com.android.deskclock;
     18 
     19 import android.os.Bundle;
     20 import android.support.annotation.NonNull;
     21 import android.support.v7.widget.RecyclerView;
     22 import android.util.SparseArray;
     23 import android.view.View;
     24 import android.view.ViewGroup;
     25 
     26 import java.util.ArrayList;
     27 import java.util.List;
     28 
     29 import static android.support.v7.widget.RecyclerView.NO_ID;
     30 
     31 /**
     32  * Base adapter class for displaying a collection of items. Provides functionality for handling
     33  * changing items, persistent item state, item click events, and re-usable item views.
     34  */
     35 public class ItemAdapter<T extends ItemAdapter.ItemHolder>
     36         extends RecyclerView.Adapter<ItemAdapter.ItemViewHolder> {
     37 
     38     /**
     39      * Finds the position of the changed item holder and invokes {@link #notifyItemChanged(int)} or
     40      * {@link #notifyItemChanged(int, Object)} if payloads are present (in order to do in-place
     41      * change animations).
     42      */
     43     private final OnItemChangedListener mItemChangedNotifier = new OnItemChangedListener() {
     44         @Override
     45         public void onItemChanged(ItemHolder<?> itemHolder) {
     46             if (mOnItemChangedListener != null) {
     47                 mOnItemChangedListener.onItemChanged(itemHolder);
     48             }
     49             final int position = mItemHolders.indexOf(itemHolder);
     50             if (position != RecyclerView.NO_POSITION) {
     51                 notifyItemChanged(position);
     52             }
     53         }
     54 
     55         @Override
     56         public void onItemChanged(ItemHolder<?> itemHolder, Object payload) {
     57             if (mOnItemChangedListener != null) {
     58                 mOnItemChangedListener.onItemChanged(itemHolder, payload);
     59             }
     60             final int position = mItemHolders.indexOf(itemHolder);
     61             if (position != RecyclerView.NO_POSITION) {
     62                 notifyItemChanged(position, payload);
     63             }
     64         }
     65     };
     66 
     67     /**
     68      * Invokes the {@link OnItemClickedListener} in {@link #mListenersByViewType} corresponding
     69      * to {@link ItemViewHolder#getItemViewType()}
     70      */
     71     private final OnItemClickedListener mOnItemClickedListener = new OnItemClickedListener() {
     72         @Override
     73         public void onItemClicked(ItemViewHolder<?> viewHolder, int id) {
     74             final OnItemClickedListener listener =
     75                     mListenersByViewType.get(viewHolder.getItemViewType());
     76             if (listener != null) {
     77                 listener.onItemClicked(viewHolder, id);
     78             }
     79         }
     80     };
     81 
     82     /**
     83      * Invoked when any item changes.
     84      */
     85     private OnItemChangedListener mOnItemChangedListener;
     86 
     87     /**
     88      * Factories for creating new {@link ItemViewHolder} entities.
     89      */
     90     private final SparseArray<ItemViewHolder.Factory> mFactoriesByViewType = new SparseArray<>();
     91 
     92     /**
     93      * Listeners to invoke in {@link #mOnItemClickedListener}.
     94      */
     95     private final SparseArray<OnItemClickedListener> mListenersByViewType = new SparseArray<>();
     96 
     97     /**
     98      * List of current item holders represented by this adapter.
     99      */
    100     private List<T> mItemHolders;
    101 
    102     /**
    103      * Convenience for calling {@link #setHasStableIds(boolean)} with {@code true}.
    104      *
    105      * @return this object, allowing calls to methods in this class to be chained
    106      */
    107     public ItemAdapter setHasStableIds() {
    108         setHasStableIds(true);
    109         return this;
    110     }
    111 
    112     /**
    113      * Sets the {@link ItemViewHolder.Factory} and {@link OnItemClickedListener} used to create
    114      * new item view holders in {@link #onCreateViewHolder(ViewGroup, int)}.
    115      *
    116      * @param factory   the {@link ItemViewHolder.Factory} used to create new item view holders
    117      * @param listener  the {@link OnItemClickedListener} to be invoked by
    118      *                  {@link #mItemChangedNotifier}
    119      * @param viewTypes the unique identifier for the view types to be created
    120      * @return this object, allowing calls to methods in this class to be chained
    121      */
    122     public ItemAdapter withViewTypes(ItemViewHolder.Factory factory,
    123             OnItemClickedListener listener, int... viewTypes) {
    124         for (int viewType : viewTypes) {
    125             mFactoriesByViewType.put(viewType, factory);
    126             mListenersByViewType.put(viewType, listener);
    127         }
    128         return this;
    129     }
    130 
    131     /**
    132      * @return the current list of item holders represented by this adapter
    133      */
    134     public final List<T> getItems() {
    135         return mItemHolders;
    136     }
    137 
    138     /**
    139      * Sets the list of item holders to serve as the dataset for this adapter and invokes
    140      * {@link #notifyDataSetChanged()} to update the UI.
    141      * <p/>
    142      * If {@link #hasStableIds()} returns {@code true}, then the instance state will preserved
    143      * between new and old holders that have matching {@link ItemHolder#itemId} values.
    144      *
    145      * @param itemHolders the new list of item holders
    146      * @return this object, allowing calls to methods in this class to be chained
    147      */
    148     public ItemAdapter setItems(List<T> itemHolders) {
    149         final List<T> oldItemHolders = mItemHolders;
    150         if (oldItemHolders != itemHolders) {
    151             if (oldItemHolders != null) {
    152                 // remove the item change listener from the old item holders
    153                 for (T oldItemHolder : oldItemHolders) {
    154                     oldItemHolder.removeOnItemChangedListener(mItemChangedNotifier);
    155                 }
    156             }
    157 
    158             if (oldItemHolders != null && itemHolders != null && hasStableIds()) {
    159                 // transfer instance state from old to new item holders based on item id,
    160                 // we use a simple O(N^2) implementation since we assume the number of items is
    161                 // relatively small and generating a temporary map would be more expensive
    162                 final Bundle bundle = new Bundle();
    163                 for (ItemHolder newItemHolder : itemHolders) {
    164                     for (ItemHolder oldItemHolder : oldItemHolders) {
    165                         if (newItemHolder.itemId == oldItemHolder.itemId
    166                                 && newItemHolder != oldItemHolder) {
    167                             // clear any existing state from the bundle
    168                             bundle.clear();
    169 
    170                             // transfer instance state from old to new item holder
    171                             oldItemHolder.onSaveInstanceState(bundle);
    172                             newItemHolder.onRestoreInstanceState(bundle);
    173 
    174                             break;
    175                         }
    176                     }
    177                 }
    178             }
    179 
    180             if (itemHolders != null) {
    181                 // add the item change listener to the new item holders
    182                 for (ItemHolder newItemHolder : itemHolders) {
    183                     newItemHolder.addOnItemChangedListener(mItemChangedNotifier);
    184                 }
    185             }
    186 
    187             // finally update the current list of item holders and inform the RV to update the UI
    188             mItemHolders = itemHolders;
    189             notifyDataSetChanged();
    190         }
    191 
    192         return this;
    193     }
    194 
    195     /**
    196      * Inserts the specified item holder at the specified position. Invokes
    197      * {@link #notifyItemInserted} to update the UI.
    198      *
    199      * @param position   the index to which to add the item holder
    200      * @param itemHolder the item holder to add
    201      * @return this object, allowing calls to methods in this class to be chained
    202      */
    203     public ItemAdapter addItem(int position, @NonNull T itemHolder) {
    204         itemHolder.addOnItemChangedListener(mItemChangedNotifier);
    205         position = Math.min(position, mItemHolders.size());
    206         mItemHolders.add(position, itemHolder);
    207         notifyItemInserted(position);
    208         return this;
    209     }
    210 
    211     /**
    212      * Removes the first occurrence of the specified element from this list, if it is present
    213      * (optional operation). If this list does not contain the element, it is unchanged. Invokes
    214      * {@link #notifyItemRemoved} to update the UI.
    215      *
    216      * @param itemHolder the item holder to remove
    217      * @return this object, allowing calls to methods in this class to be chained
    218      */
    219     public ItemAdapter removeItem(@NonNull T itemHolder) {
    220         final int index = mItemHolders.indexOf(itemHolder);
    221         if (index >= 0) {
    222             itemHolder = mItemHolders.remove(index);
    223             itemHolder.removeOnItemChangedListener(mItemChangedNotifier);
    224             notifyItemRemoved(index);
    225         }
    226         return this;
    227     }
    228 
    229     /**
    230      * Sets the listener to be invoked whenever any item changes.
    231      */
    232     public void setOnItemChangedListener(OnItemChangedListener listener) {
    233         mOnItemChangedListener = listener;
    234     }
    235 
    236     @Override
    237     public int getItemCount() {
    238         return mItemHolders == null ? 0 : mItemHolders.size();
    239     }
    240 
    241     @Override
    242     public long getItemId(int position) {
    243         return hasStableIds() ? mItemHolders.get(position).itemId : NO_ID;
    244     }
    245 
    246     public T findItemById(long id) {
    247         for (T holder : mItemHolders) {
    248             if (holder.itemId == id) {
    249                 return holder;
    250             }
    251         }
    252         return null;
    253     }
    254 
    255     @Override
    256     public int getItemViewType(int position) {
    257         return mItemHolders.get(position).getItemViewType();
    258     }
    259 
    260     @Override
    261     public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    262         final ItemViewHolder.Factory factory = mFactoriesByViewType.get(viewType);
    263         if (factory != null) {
    264             return factory.createViewHolder(parent, viewType);
    265         }
    266         throw new IllegalArgumentException("Unsupported view type: " + viewType);
    267     }
    268 
    269     @Override
    270     @SuppressWarnings("unchecked")
    271     public void onBindViewHolder(ItemViewHolder viewHolder, int position) {
    272         // suppress any unchecked warnings since it is up to the subclass to guarantee
    273         // compatibility of their view holders with the item holder at the corresponding position
    274         viewHolder.bindItemView(mItemHolders.get(position));
    275         viewHolder.setOnItemClickedListener(mOnItemClickedListener);
    276     }
    277 
    278     @Override
    279     public void onViewRecycled(ItemViewHolder viewHolder) {
    280         viewHolder.setOnItemClickedListener(null);
    281         viewHolder.recycleItemView();
    282     }
    283 
    284     /**
    285      * Base class for wrapping an item for compatibility with an {@link ItemHolder}.
    286      * <p/>
    287      * An {@link ItemHolder} serves as bridge between the model and view layer; subclassers should
    288      * implement properties that fall beyond the scope of their model layer but are necessary for
    289      * the view layer. Properties that should be persisted across dataset changes can be
    290      * preserved via the {@link #onSaveInstanceState(Bundle)} and
    291      * {@link #onRestoreInstanceState(Bundle)} methods.
    292      * <p/>
    293      * Note: An {@link ItemHolder} can be used by multiple {@link ItemHolder} and any state changes
    294      * should simultaneously be reflected in both UIs.  It is not thread-safe however and should
    295      * only be used on a single thread at a given time.
    296      *
    297      * @param <T> the item type wrapped by the holder
    298      */
    299     public static abstract class ItemHolder<T> {
    300 
    301         /**
    302          * The item held by this holder.
    303          */
    304         public final T item;
    305 
    306         /**
    307          * Globally unique id corresponding to the item.
    308          */
    309         public final long itemId;
    310 
    311         /**
    312          * Listeners to be invoked by {@link #notifyItemChanged()}.
    313          */
    314         private final List<OnItemChangedListener> mOnItemChangedListeners = new ArrayList<>();
    315 
    316         /**
    317          * Designated constructor.
    318          *
    319          * @param item   the {@link T} item to be held by this holder
    320          * @param itemId the globally unique id corresponding to the item
    321          */
    322         public ItemHolder(T item, long itemId) {
    323             this.item = item;
    324             this.itemId = itemId;
    325         }
    326 
    327         /**
    328          * @return the unique identifier for the view that should be used to represent the item,
    329          * e.g. the layout resource id.
    330          */
    331         public abstract int getItemViewType();
    332 
    333         /**
    334          * Adds the listener to the current list of registered listeners if it is not already
    335          * registered.
    336          *
    337          * @param listener the listener to add
    338          */
    339         public final void addOnItemChangedListener(OnItemChangedListener listener) {
    340             if (!mOnItemChangedListeners.contains(listener)) {
    341                 mOnItemChangedListeners.add(listener);
    342             }
    343         }
    344 
    345         /**
    346          * Removes the listener from the current list of registered listeners.
    347          *
    348          * @param listener the listener to remove
    349          */
    350         public final void removeOnItemChangedListener(OnItemChangedListener listener) {
    351             mOnItemChangedListeners.remove(listener);
    352         }
    353 
    354         /**
    355          * Invokes {@link OnItemChangedListener#onItemChanged(ItemHolder)} for all listeners added
    356          * via {@link #addOnItemChangedListener(OnItemChangedListener)}.
    357          */
    358         public final void notifyItemChanged() {
    359             for (OnItemChangedListener listener : mOnItemChangedListeners) {
    360                 listener.onItemChanged(this);
    361             }
    362         }
    363 
    364         /**
    365          * Invokes {@link OnItemChangedListener#onItemChanged(ItemHolder, Object)} for all
    366          * listeners added via {@link #addOnItemChangedListener(OnItemChangedListener)}.
    367          */
    368         public final void notifyItemChanged(Object payload) {
    369             for (OnItemChangedListener listener : mOnItemChangedListeners) {
    370                 listener.onItemChanged(this, payload);
    371             }
    372         }
    373 
    374         /**
    375          * Called to retrieve per-instance state when the item may disappear or change so that
    376          * state can be restored in {@link #onRestoreInstanceState(Bundle)}.
    377          * <p/>
    378          * Note: Subclasses must not maintain a reference to the {@link Bundle} as it may be
    379          * reused for other items in the {@link ItemHolder}.
    380          *
    381          * @param bundle the {@link Bundle} in which to place saved state
    382          */
    383         public void onSaveInstanceState(Bundle bundle) {
    384             // for subclassers
    385         }
    386 
    387         /**
    388          * Called to restore any per-instance state which was previously saved in
    389          * {@link #onSaveInstanceState(Bundle)} for an item with a matching {@link #itemId}.
    390          * <p/>
    391          * Note: Subclasses must not maintain a reference to the {@link Bundle} as it may be
    392          * reused for other items in the {@link ItemHolder}.
    393          *
    394          * @param bundle the {@link Bundle} in which to retrieve saved state
    395          */
    396         public void onRestoreInstanceState(Bundle bundle) {
    397             // for subclassers
    398         }
    399     }
    400 
    401     /**
    402      * Base class for a reusable {@link RecyclerView.ViewHolder} compatible with an
    403      * {@link ItemViewHolder}. Provides an interface for binding to an {@link ItemHolder} and later
    404      * being recycled.
    405      */
    406     public static class ItemViewHolder<T extends ItemHolder> extends RecyclerView.ViewHolder {
    407 
    408         /**
    409          * The current {@link ItemHolder} bound to this holder.
    410          */
    411         private T mItemHolder;
    412 
    413         /**
    414          * The current {@link OnItemClickedListener} associated with this holder.
    415          */
    416         private OnItemClickedListener mOnItemClickedListener;
    417 
    418         /**
    419          * Designated constructor.
    420          *
    421          * @param itemView the item {@link View} to associate with this holder
    422          */
    423         public ItemViewHolder(View itemView) {
    424             super(itemView);
    425         }
    426 
    427         /**
    428          * @return the current {@link ItemHolder} bound to this holder, or {@code null} if unbound
    429          */
    430         public final T getItemHolder() {
    431             return mItemHolder;
    432         }
    433 
    434         /**
    435          * Binds the holder's {@link #itemView} to a particular item.
    436          *
    437          * @param itemHolder the {@link ItemHolder} to bind
    438          */
    439         public final void bindItemView(T itemHolder) {
    440             mItemHolder = itemHolder;
    441             onBindItemView(itemHolder);
    442         }
    443 
    444         /**
    445          * Called when a new item is bound to the holder. Subclassers should override to bind any
    446          * relevant data to their {@link #itemView} in this method.
    447          *
    448          * @param itemHolder the {@link ItemHolder} to bind
    449          */
    450         protected void onBindItemView(T itemHolder) {
    451             // for subclassers
    452         }
    453 
    454         /**
    455          * Recycles the current item view, unbinding the current item holder and state.
    456          */
    457         public final void recycleItemView() {
    458             mItemHolder = null;
    459             mOnItemClickedListener = null;
    460 
    461             onRecycleItemView();
    462         }
    463 
    464         /**
    465          * Called when the current item view is recycled. Subclassers should override to release
    466          * any bound item state and prepare their {@link #itemView} for reuse.
    467          */
    468         protected void onRecycleItemView() {
    469             // for subclassers
    470         }
    471 
    472         /**
    473          * Sets the current {@link OnItemClickedListener} to be invoked via
    474          * {@link #notifyItemClicked}.
    475          *
    476          * @param listener the new {@link OnItemClickedListener}, or {@code null} to clear
    477          */
    478         public final void setOnItemClickedListener(OnItemClickedListener listener) {
    479             mOnItemClickedListener = listener;
    480         }
    481 
    482         /**
    483          * Called by subclasses to invoke the current {@link OnItemClickedListener} for a
    484          * particular click event so it can be handled at a higher level.
    485          *
    486          * @param id the unique identifier for the click action that has occurred
    487          */
    488         public final void notifyItemClicked(int id) {
    489             if (mOnItemClickedListener != null) {
    490                 mOnItemClickedListener.onItemClicked(this, id);
    491             }
    492         }
    493 
    494         /**
    495          * Factory interface used by {@link ItemAdapter} for creating new {@link ItemViewHolder}.
    496          */
    497         public interface Factory {
    498             /**
    499              * Used by {@link ItemAdapter#createViewHolder(ViewGroup, int)} to make new
    500              * {@link ItemViewHolder} for a given view type.
    501              *
    502              * @param parent   the {@code ViewGroup} that the {@link ItemViewHolder#itemView} will
    503              *                 be attached
    504              * @param viewType the unique id of the item view to create
    505              * @return a new initialized {@link ItemViewHolder}
    506              */
    507             public ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType);
    508         }
    509     }
    510 
    511     /**
    512      * Callback interface for when an item changes and should be re-bound.
    513      */
    514     public interface OnItemChangedListener {
    515         /**
    516          * Invoked by {@link ItemHolder#notifyItemChanged()}.
    517          *
    518          * @param itemHolder the item holder that has changed
    519          */
    520         void onItemChanged(ItemHolder<?> itemHolder);
    521 
    522 
    523         /**
    524          * Invoked by {@link ItemHolder#notifyItemChanged(Object payload)}.
    525          *
    526          * @param itemHolder the item holder that has changed
    527          * @param payload the payload object
    528          */
    529         void onItemChanged(ItemAdapter.ItemHolder<?> itemHolder, Object payload);
    530     }
    531 
    532     /**
    533      * Callback interface for handling when an item is clicked.
    534      */
    535     public interface OnItemClickedListener {
    536         /**
    537          * Invoked by {@link ItemViewHolder#notifyItemClicked(int)}
    538          *
    539          * @param viewHolder the {@link ItemViewHolder} containing the view that was clicked
    540          * @param id         the unique identifier for the click action that has occurred
    541          */
    542         void onItemClicked(ItemViewHolder<?> viewHolder, int id);
    543     }
    544 }