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