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