308 lines
9.3 KiB
Java
308 lines
9.3 KiB
Java
/*
|
|
* Copyright (C) 2015 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.setupwizardlib.items;
|
|
|
|
import android.content.Context;
|
|
import android.util.AttributeSet;
|
|
import android.util.Log;
|
|
import android.util.SparseIntArray;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
public class ItemGroup extends AbstractItemHierarchy
|
|
implements ItemInflater.ItemParent, ItemHierarchy.Observer {
|
|
|
|
/* static section */
|
|
|
|
private static final String TAG = "ItemGroup";
|
|
|
|
/**
|
|
* Binary search for the closest value that's smaller than or equal to {@code value}, and return
|
|
* the corresponding key.
|
|
*/
|
|
private static int binarySearch(SparseIntArray array, int value) {
|
|
final int size = array.size();
|
|
int lo = 0;
|
|
int hi = size - 1;
|
|
|
|
while (lo <= hi) {
|
|
final int mid = (lo + hi) >>> 1;
|
|
final int midVal = array.valueAt(mid);
|
|
|
|
if (midVal < value) {
|
|
lo = mid + 1;
|
|
} else if (midVal > value) {
|
|
hi = mid - 1;
|
|
} else {
|
|
return array.keyAt(mid); // value found
|
|
}
|
|
}
|
|
// Value not found. Return the last item before our search range, which is the closest
|
|
// value smaller than the value we are looking for.
|
|
return array.keyAt(lo - 1);
|
|
}
|
|
|
|
/**
|
|
* Same as {@link List#indexOf(Object)}, but using identity comparison rather than {@link
|
|
* Object#equals(Object)}.
|
|
*/
|
|
private static <T> int identityIndexOf(List<T> list, T object) {
|
|
final int count = list.size();
|
|
for (int i = 0; i < count; i++) {
|
|
if (list.get(i) == object) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* non-static section */
|
|
|
|
private final List<ItemHierarchy> children = new ArrayList<>();
|
|
|
|
/**
|
|
* A mapping from the index of an item hierarchy in children, to the first position in which the
|
|
* corresponding child hierarchy represents. For example:
|
|
*
|
|
* <p>ItemHierarchy Item Item Position Index
|
|
*
|
|
* <p>0 [ Wi-Fi AP 1 ] 0 | Wi-Fi AP 2 | 1 | Wi-Fi AP 3 | 2 | Wi-Fi AP 4 | 3 [ Wi-Fi AP 5 ] 4
|
|
*
|
|
* <p>1 [ <Empty Item Hierarchy> ]
|
|
*
|
|
* <p>2 [ Use cellular data ] 5
|
|
*
|
|
* <p>3 [ Don't connect ] 6
|
|
*
|
|
* <p>For this example of Wi-Fi screen, the following mapping will be produced: [ 0 -> 0 | 2 -> 5
|
|
* | 3 -> 6 ]
|
|
*
|
|
* <p>Also note how ItemHierarchy index 1 is not present in the map, because it is empty.
|
|
*
|
|
* <p>ItemGroup uses this map to look for which ItemHierarchy an item at a given position belongs
|
|
* to.
|
|
*/
|
|
private final SparseIntArray hierarchyStart = new SparseIntArray();
|
|
|
|
private int count = 0;
|
|
private boolean dirty = false;
|
|
|
|
public ItemGroup() {
|
|
super();
|
|
}
|
|
|
|
public ItemGroup(Context context, AttributeSet attrs) {
|
|
// Constructor for XML inflation
|
|
super(context, attrs);
|
|
}
|
|
|
|
/** Add a child hierarchy to this item group. */
|
|
@Override
|
|
public void addChild(ItemHierarchy child) {
|
|
dirty = true;
|
|
children.add(child);
|
|
child.registerObserver(this);
|
|
|
|
final int count = child.getCount();
|
|
if (count > 0) {
|
|
notifyItemRangeInserted(getChildPosition(child), count);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove a previously added child from this item group.
|
|
*
|
|
* @return True if there is a match for the child and it is removed. False if the child could not
|
|
* be found in our list of child hierarchies.
|
|
*/
|
|
public boolean removeChild(ItemHierarchy child) {
|
|
final int childIndex = identityIndexOf(children, child);
|
|
final int childPosition = getChildPosition(childIndex);
|
|
dirty = true;
|
|
if (childIndex != -1) {
|
|
final int childCount = child.getCount();
|
|
children.remove(childIndex);
|
|
child.unregisterObserver(this);
|
|
if (childCount > 0) {
|
|
notifyItemRangeRemoved(childPosition, childCount);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Remove all children from this hierarchy. */
|
|
public void clear() {
|
|
if (children.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
final int numRemoved = getCount();
|
|
|
|
for (ItemHierarchy item : children) {
|
|
item.unregisterObserver(this);
|
|
}
|
|
dirty = true;
|
|
children.clear();
|
|
notifyItemRangeRemoved(0, numRemoved);
|
|
}
|
|
|
|
@Override
|
|
public int getCount() {
|
|
updateDataIfNeeded();
|
|
return count;
|
|
}
|
|
|
|
@Override
|
|
public IItem getItemAt(int position) {
|
|
int itemIndex = getItemIndex(position);
|
|
ItemHierarchy item = children.get(itemIndex);
|
|
int subpos = position - hierarchyStart.get(itemIndex);
|
|
return item.getItemAt(subpos);
|
|
}
|
|
|
|
@Override
|
|
public void onChanged(ItemHierarchy hierarchy) {
|
|
// Need to set dirty, because our children may have gotten more items.
|
|
dirty = true;
|
|
notifyChanged();
|
|
}
|
|
|
|
/**
|
|
* @return The "Item Position" of the given child, or -1 if the child is not found. If the given
|
|
* child is empty, position of the next visible item is returned.
|
|
*/
|
|
private int getChildPosition(ItemHierarchy child) {
|
|
// Check the identity of the child rather than using .equals(), because here we want
|
|
// to find the index of the instance itself rather than something that equals to it.
|
|
return getChildPosition(identityIndexOf(children, child));
|
|
}
|
|
|
|
private int getChildPosition(int childIndex) {
|
|
updateDataIfNeeded();
|
|
if (childIndex != -1) {
|
|
int childPos = -1;
|
|
int childCount = children.size();
|
|
for (int i = childIndex; childPos < 0 && i < childCount; i++) {
|
|
// Find the position of the first visible child after childIndex. This is required
|
|
// when removing the last item from a nested ItemGroup.
|
|
childPos = hierarchyStart.get(i, -1);
|
|
}
|
|
if (childPos < 0) {
|
|
// If the last item in a group is being removed, there will be no visible item.
|
|
// In that case return the count instead, since that is where the item would have
|
|
// been if the child is not empty.
|
|
childPos = getCount();
|
|
}
|
|
return childPos;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
@Override
|
|
public void onItemRangeChanged(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
|
|
// No need to set dirty because onItemRangeChanged does not include any structural changes.
|
|
final int childPosition = getChildPosition(itemHierarchy);
|
|
if (childPosition >= 0) {
|
|
notifyItemRangeChanged(childPosition + positionStart, itemCount);
|
|
} else {
|
|
Log.e(TAG, "Unexpected child change " + itemHierarchy);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onItemRangeInserted(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
|
|
dirty = true;
|
|
final int childPosition = getChildPosition(itemHierarchy);
|
|
if (childPosition >= 0) {
|
|
notifyItemRangeInserted(childPosition + positionStart, itemCount);
|
|
} else {
|
|
Log.e(TAG, "Unexpected child insert " + itemHierarchy);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onItemRangeMoved(
|
|
ItemHierarchy itemHierarchy, int fromPosition, int toPosition, int itemCount) {
|
|
dirty = true;
|
|
final int childPosition = getChildPosition(itemHierarchy);
|
|
if (childPosition >= 0) {
|
|
notifyItemRangeMoved(childPosition + fromPosition, childPosition + toPosition, itemCount);
|
|
} else {
|
|
Log.e(TAG, "Unexpected child move " + itemHierarchy);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onItemRangeRemoved(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
|
|
dirty = true;
|
|
final int childPosition = getChildPosition(itemHierarchy);
|
|
if (childPosition >= 0) {
|
|
notifyItemRangeRemoved(childPosition + positionStart, itemCount);
|
|
} else {
|
|
Log.e(TAG, "Unexpected child remove " + itemHierarchy);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public ItemHierarchy findItemById(int id) {
|
|
if (id == getId()) {
|
|
return this;
|
|
}
|
|
for (ItemHierarchy child : children) {
|
|
ItemHierarchy childFindItem = child.findItemById(id);
|
|
if (childFindItem != null) {
|
|
return childFindItem;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** If dirty, this method will recalculate the number of items and hierarchyStart. */
|
|
private void updateDataIfNeeded() {
|
|
if (dirty) {
|
|
count = 0;
|
|
hierarchyStart.clear();
|
|
for (int itemIndex = 0; itemIndex < children.size(); itemIndex++) {
|
|
ItemHierarchy item = children.get(itemIndex);
|
|
if (item.getCount() > 0) {
|
|
hierarchyStart.put(itemIndex, count);
|
|
}
|
|
count += item.getCount();
|
|
}
|
|
dirty = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Use binary search to locate the item hierarchy a position is contained in.
|
|
*
|
|
* @return Index of the item hierarchy which is responsible for the item at {@code position}.
|
|
*/
|
|
private int getItemIndex(int position) {
|
|
updateDataIfNeeded();
|
|
if (position < 0 || position >= count) {
|
|
throw new IndexOutOfBoundsException("size=" + count + "; index=" + position);
|
|
}
|
|
int result = binarySearch(hierarchyStart, position);
|
|
if (result < 0) {
|
|
throw new IllegalStateException("Cannot have item start index < 0");
|
|
}
|
|
return result;
|
|
}
|
|
}
|