1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 package android.support.v17.leanback.widget;
15 
16 import android.os.Bundle;
17 import android.os.Parcelable;
18 import android.support.v4.util.LruCache;
19 import android.util.SparseArray;
20 import android.view.View;
21 
22 import java.util.Iterator;
23 import java.util.Map;
24 import java.util.Map.Entry;
25 
26 import static android.support.v17.leanback.widget.BaseGridView.SAVE_NO_CHILD;
27 import static android.support.v17.leanback.widget.BaseGridView.SAVE_ON_SCREEN_CHILD;
28 import static android.support.v17.leanback.widget.BaseGridView.SAVE_LIMITED_CHILD;
29 import static android.support.v17.leanback.widget.BaseGridView.SAVE_ALL_CHILD;
30 
31 /**
32  * Maintains a bundle of states for a group of views. Each view must have a unique id to identify
33  * it. There are four different strategies {@link #SAVE_NO_CHILD} {@link #SAVE_ON_SCREEN_CHILD}
34  * {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}.
35  * <p>
36  * This class serves purpose of nested "listview" e.g.  a vertical list of horizontal list.
37  * Vertical list maintains id->bundle mapping of all it's children (even the children is offscreen
38  * and being pruned).
39  * <p>
40  * The class is currently used within {@link GridLayoutManager}, but it might be used by other
41  * ViewGroup.
42  */
43 class ViewsStateBundle {
44 
45     public static final int LIMIT_DEFAULT = 100;
46     public static final int UNLIMITED = Integer.MAX_VALUE;
47 
48     private int mSavePolicy;
49     private int mLimitNumber;
50 
51     private LruCache<String, SparseArray<Parcelable>> mChildStates;
52 
ViewsStateBundle()53     public ViewsStateBundle() {
54         mSavePolicy = SAVE_NO_CHILD;
55         mLimitNumber = LIMIT_DEFAULT;
56     }
57 
clear()58     public void clear() {
59         if (mChildStates != null) {
60             mChildStates.evictAll();
61         }
62     }
63 
remove(int id)64     public void remove(int id) {
65         if (mChildStates != null && mChildStates.size() != 0) {
66             mChildStates.remove(getSaveStatesKey(id));
67         }
68     }
69 
70     /**
71      * @return the saved views states
72      */
saveAsBundle()73     public final Bundle saveAsBundle() {
74         if (mChildStates == null || mChildStates.size() == 0) {
75             return null;
76         }
77         Map<String, SparseArray<Parcelable>> snapshot = mChildStates.snapshot();
78         Bundle bundle = new Bundle();
79         for (Iterator<Entry<String, SparseArray<Parcelable>>> i =
80                 snapshot.entrySet().iterator(); i.hasNext(); ) {
81             Entry<String, SparseArray<Parcelable>> e = i.next();
82             bundle.putSparseParcelableArray(e.getKey(), e.getValue());
83         }
84         return bundle;
85     }
86 
loadFromBundle(Bundle savedBundle)87     public final void loadFromBundle(Bundle savedBundle) {
88         if (mChildStates != null && savedBundle != null) {
89             mChildStates.evictAll();
90             for (Iterator<String> i = savedBundle.keySet().iterator(); i.hasNext(); ) {
91                 String key = i.next();
92                 mChildStates.put(key, savedBundle.getSparseParcelableArray(key));
93             }
94         }
95     }
96 
97     /**
98      * @return the savePolicy, see {@link #SAVE_NO_CHILD} {@link #SAVE_ON_SCREEN_CHILD}
99      *         {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}
100      */
getSavePolicy()101     public final int getSavePolicy() {
102         return mSavePolicy;
103     }
104 
105     /**
106      * @return the limitNumber, only works when {@link #getSavePolicy()} is
107      *         {@link #SAVE_LIMITED_CHILD}
108      */
getLimitNumber()109     public final int getLimitNumber() {
110         return mLimitNumber;
111     }
112 
113     /**
114      * @see ViewsStateBundle#getSavePolicy()
115      */
setSavePolicy(int savePolicy)116     public final void setSavePolicy(int savePolicy) {
117         this.mSavePolicy = savePolicy;
118         applyPolicyChanges();
119     }
120 
121     /**
122      * @see ViewsStateBundle#getLimitNumber()
123      */
setLimitNumber(int limitNumber)124     public final void setLimitNumber(int limitNumber) {
125         this.mLimitNumber = limitNumber;
126         applyPolicyChanges();
127     }
128 
applyPolicyChanges()129     protected void applyPolicyChanges() {
130         if (mSavePolicy == SAVE_LIMITED_CHILD) {
131             if (mLimitNumber <= 0) {
132                 throw new IllegalArgumentException();
133             }
134             if (mChildStates == null || mChildStates.maxSize() != mLimitNumber) {
135                 mChildStates = new LruCache<String, SparseArray<Parcelable>>(mLimitNumber);
136             }
137         } else if (mSavePolicy == SAVE_ALL_CHILD || mSavePolicy == SAVE_ON_SCREEN_CHILD) {
138             if (mChildStates == null || mChildStates.maxSize() != UNLIMITED) {
139                 mChildStates = new LruCache<String, SparseArray<Parcelable>>(UNLIMITED);
140             }
141         } else {
142             mChildStates = null;
143         }
144     }
145 
146     /**
147      * Load view from states, it's none operation if the there is no state associated with the id.
148      *
149      * @param view view where loads into
150      * @param id unique id for the view within this ViewsStateBundle
151      */
loadView(View view, int id)152     public final void loadView(View view, int id) {
153         if (mChildStates != null) {
154             String key = getSaveStatesKey(id);
155             // Once loaded the state, do not keep the state of child. The child state will
156             // be saved again either when child is offscreen or when the parent is saved.
157             SparseArray<Parcelable> container = mChildStates.remove(key);
158             if (container != null) {
159                 view.restoreHierarchyState(container);
160             }
161         }
162     }
163 
164     /**
165      * Save views regardless what's the current policy is.
166      *
167      * @param view view to save
168      * @param id unique id for the view within this ViewsStateBundle
169      */
saveViewUnchecked(View view, int id)170     protected final void saveViewUnchecked(View view, int id) {
171         if (mChildStates != null) {
172             String key = getSaveStatesKey(id);
173             SparseArray<Parcelable> container = new SparseArray<Parcelable>();
174             view.saveHierarchyState(container);
175             mChildStates.put(key, container);
176         }
177     }
178 
179     /**
180      * The on screen view is saved when policy is not {@link #SAVE_NO_CHILD}.
181      *
182      * @param bundle   Bundle where we save the on screen view state.  If null,
183      *                 a new Bundle is created and returned.
184      * @param view     The view to save.
185      * @param id       Id of the view.
186      */
saveOnScreenView(Bundle bundle, View view, int id)187     public final Bundle saveOnScreenView(Bundle bundle, View view, int id) {
188         if (mSavePolicy != SAVE_NO_CHILD) {
189             String key = getSaveStatesKey(id);
190             SparseArray<Parcelable> container = new SparseArray<Parcelable>();
191             view.saveHierarchyState(container);
192             if (bundle == null) {
193                 bundle = new Bundle();
194             }
195             bundle.putSparseParcelableArray(key, container);
196         }
197         return bundle;
198     }
199 
200     /**
201      * Save off screen views according to policy.
202      *
203      * @param view view to save
204      * @param id unique id for the view within this ViewsStateBundle
205      */
saveOffscreenView(View view, int id)206     public final void saveOffscreenView(View view, int id) {
207         switch (mSavePolicy) {
208             case SAVE_LIMITED_CHILD:
209             case SAVE_ALL_CHILD:
210                 saveViewUnchecked(view, id);
211                 break;
212             case SAVE_ON_SCREEN_CHILD:
213                 remove(id);
214                 break;
215             default:
216                 break;
217         }
218     }
219 
getSaveStatesKey(int id)220     static String getSaveStatesKey(int id) {
221         return Integer.toString(id);
222     }
223 }
224