Java tutorial
/* * Copyright (C) 2006 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 android.widget; import android.annotation.ArrayRes; import android.annotation.IdRes; import android.annotation.LayoutRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.res.Resources; import android.util.Log; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * You can use this adapter to provide views for an {@link AdapterView}, * Returns a view for each object in a collection of data objects you * provide, and can be used with list-based user interface widgets such as * {@link ListView} or {@link Spinner}. * <p> * By default, the array adapter creates a view by calling {@link Object#toString()} on each * data object in the collection you provide, and places the result in a TextView. * You may also customize what type of view is used for the data object in the collection. * To customize what type of view is used for the data object, * override {@link #getView(int, View, ViewGroup)} * and inflate a view resource. * For a code example, see * the <a href="https://developer.android.com/samples/CustomChoiceList/index.html"> * CustomChoiceList</a> sample. * </p> * <p> * For an example of using an array adapter with a ListView, see the * <a href="{@docRoot}guide/topics/ui/declaring-layout.html#AdapterViews"> * Adapter Views</a> guide. * </p> * <p> * For an example of using an array adapter with a Spinner, see the * <a href="{@docRoot}guide/topics/ui/controls/spinner.html">Spinners</a> guide. * </p> * <p class="note"><strong>Note:</strong> * If you are considering using array adapter with a ListView, consider using * {@link android.support.v7.widget.RecyclerView} instead. * RecyclerView offers similar features with better performance and more flexibility than * ListView provides. * See the * <a href="https://developer.android.com/guide/topics/ui/layout/recyclerview.html"> * Recycler View</a> guide.</p> */ public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSpinnerAdapter { /** * Lock used to modify the content of {@link #mObjects}. Any write operation * performed on the array should be synchronized on this lock. This lock is also * used by the filter (see {@link #getFilter()} to make a synchronized copy of * the original array of data. */ @UnsupportedAppUsage private final Object mLock = new Object(); private final LayoutInflater mInflater; private final Context mContext; /** * The resource indicating what views to inflate to display the content of this * array adapter. */ private final int mResource; /** * The resource indicating what views to inflate to display the content of this * array adapter in a drop down widget. */ private int mDropDownResource; /** * Contains the list of objects that represent the data of this ArrayAdapter. * The content of this list is referred to as "the array" in the documentation. */ @UnsupportedAppUsage private List<T> mObjects; /** * Indicates whether the contents of {@link #mObjects} came from static resources. */ private boolean mObjectsFromResources; /** * If the inflated resource is not a TextView, {@code mFieldId} is used to find * a TextView inside the inflated views hierarchy. This field must contain the * identifier that matches the one defined in the resource file. */ private int mFieldId = 0; /** * Indicates whether or not {@link #notifyDataSetChanged()} must be called whenever * {@link #mObjects} is modified. */ private boolean mNotifyOnChange = true; // A copy of the original mObjects array, initialized from and then used instead as soon as // the mFilter ArrayFilter is used. mObjects will then only contain the filtered values. @UnsupportedAppUsage private ArrayList<T> mOriginalValues; private ArrayFilter mFilter; /** Layout inflater used for {@link #getDropDownView(int, View, ViewGroup)}. */ private LayoutInflater mDropDownInflater; /** * Constructor * * @param context The current context. * @param resource The resource ID for a layout file containing a TextView to use when * instantiating views. */ public ArrayAdapter(@NonNull Context context, @LayoutRes int resource) { this(context, resource, 0, new ArrayList<>()); } /** * Constructor * * @param context The current context. * @param resource The resource ID for a layout file containing a layout to use when * instantiating views. * @param textViewResourceId The id of the TextView within the layout resource to be populated */ public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId) { this(context, resource, textViewResourceId, new ArrayList<>()); } /** * Constructor. This constructor will result in the underlying data collection being * immutable, so methods such as {@link #clear()} will throw an exception. * * @param context The current context. * @param resource The resource ID for a layout file containing a TextView to use when * instantiating views. * @param objects The objects to represent in the ListView. */ public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull T[] objects) { this(context, resource, 0, Arrays.asList(objects)); } /** * Constructor. This constructor will result in the underlying data collection being * immutable, so methods such as {@link #clear()} will throw an exception. * * @param context The current context. * @param resource The resource ID for a layout file containing a layout to use when * instantiating views. * @param textViewResourceId The id of the TextView within the layout resource to be populated * @param objects The objects to represent in the ListView. */ public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull T[] objects) { this(context, resource, textViewResourceId, Arrays.asList(objects)); } /** * Constructor * * @param context The current context. * @param resource The resource ID for a layout file containing a TextView to use when * instantiating views. * @param objects The objects to represent in the ListView. */ public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull List<T> objects) { this(context, resource, 0, objects); } /** * Constructor * * @param context The current context. * @param resource The resource ID for a layout file containing a layout to use when * instantiating views. * @param textViewResourceId The id of the TextView within the layout resource to be populated * @param objects The objects to represent in the ListView. */ public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull List<T> objects) { this(context, resource, textViewResourceId, objects, false); } private ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull List<T> objects, boolean objsFromResources) { mContext = context; mInflater = LayoutInflater.from(context); mResource = mDropDownResource = resource; mObjects = objects; mObjectsFromResources = objsFromResources; mFieldId = textViewResourceId; } /** * Adds the specified object at the end of the array. * * @param object The object to add at the end of the array. * @throws UnsupportedOperationException if the underlying data collection is immutable */ public void add(@Nullable T object) { synchronized (mLock) { if (mOriginalValues != null) { mOriginalValues.add(object); } else { mObjects.add(object); } mObjectsFromResources = false; } if (mNotifyOnChange) notifyDataSetChanged(); } /** * Adds the specified Collection at the end of the array. * * @param collection The Collection to add at the end of the array. * @throws UnsupportedOperationException if the <tt>addAll</tt> operation * is not supported by this list * @throws ClassCastException if the class of an element of the specified * collection prevents it from being added to this list * @throws NullPointerException if the specified collection contains one * or more null elements and this list does not permit null * elements, or if the specified collection is null * @throws IllegalArgumentException if some property of an element of the * specified collection prevents it from being added to this list */ public void addAll(@NonNull Collection<? extends T> collection) { synchronized (mLock) { if (mOriginalValues != null) { mOriginalValues.addAll(collection); } else { mObjects.addAll(collection); } mObjectsFromResources = false; } if (mNotifyOnChange) notifyDataSetChanged(); } /** * Adds the specified items at the end of the array. * * @param items The items to add at the end of the array. * @throws UnsupportedOperationException if the underlying data collection is immutable */ public void addAll(T... items) { synchronized (mLock) { if (mOriginalValues != null) { Collections.addAll(mOriginalValues, items); } else { Collections.addAll(mObjects, items); } mObjectsFromResources = false; } if (mNotifyOnChange) notifyDataSetChanged(); } /** * Inserts the specified object at the specified index in the array. * * @param object The object to insert into the array. * @param index The index at which the object must be inserted. * @throws UnsupportedOperationException if the underlying data collection is immutable */ public void insert(@Nullable T object, int index) { synchronized (mLock) { if (mOriginalValues != null) { mOriginalValues.add(index, object); } else { mObjects.add(index, object); } mObjectsFromResources = false; } if (mNotifyOnChange) notifyDataSetChanged(); } /** * Removes the specified object from the array. * * @param object The object to remove. * @throws UnsupportedOperationException if the underlying data collection is immutable */ public void remove(@Nullable T object) { synchronized (mLock) { if (mOriginalValues != null) { mOriginalValues.remove(object); } else { mObjects.remove(object); } mObjectsFromResources = false; } if (mNotifyOnChange) notifyDataSetChanged(); } /** * Remove all elements from the list. * * @throws UnsupportedOperationException if the underlying data collection is immutable */ public void clear() { synchronized (mLock) { if (mOriginalValues != null) { mOriginalValues.clear(); } else { mObjects.clear(); } mObjectsFromResources = false; } if (mNotifyOnChange) notifyDataSetChanged(); } /** * Sorts the content of this adapter using the specified comparator. * * @param comparator The comparator used to sort the objects contained * in this adapter. */ public void sort(@NonNull Comparator<? super T> comparator) { synchronized (mLock) { if (mOriginalValues != null) { Collections.sort(mOriginalValues, comparator); } else { Collections.sort(mObjects, comparator); } } if (mNotifyOnChange) notifyDataSetChanged(); } @Override public void notifyDataSetChanged() { super.notifyDataSetChanged(); mNotifyOnChange = true; } /** * Control whether methods that change the list ({@link #add}, {@link #addAll(Collection)}, * {@link #addAll(Object[])}, {@link #insert}, {@link #remove}, {@link #clear}, * {@link #sort(Comparator)}) automatically call {@link #notifyDataSetChanged}. If set to * false, caller must manually call notifyDataSetChanged() to have the changes * reflected in the attached view. * * The default is true, and calling notifyDataSetChanged() * resets the flag to true. * * @param notifyOnChange if true, modifications to the list will * automatically call {@link * #notifyDataSetChanged} */ public void setNotifyOnChange(boolean notifyOnChange) { mNotifyOnChange = notifyOnChange; } /** * Returns the context associated with this array adapter. The context is used * to create views from the resource passed to the constructor. * * @return The Context associated with this adapter. */ public @NonNull Context getContext() { return mContext; } @Override public int getCount() { return mObjects.size(); } @Override public @Nullable T getItem(int position) { return mObjects.get(position); } /** * Returns the position of the specified item in the array. * * @param item The item to retrieve the position of. * * @return The position of the specified item. */ public int getPosition(@Nullable T item) { return mObjects.indexOf(item); } @Override public long getItemId(int position) { return position; } @Override public @NonNull View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { return createViewFromResource(mInflater, position, convertView, parent, mResource); } private @NonNull View createViewFromResource(@NonNull LayoutInflater inflater, int position, @Nullable View convertView, @NonNull ViewGroup parent, int resource) { final View view; final TextView text; if (convertView == null) { view = inflater.inflate(resource, parent, false); } else { view = convertView; } try { if (mFieldId == 0) { // If no custom field is assigned, assume the whole resource is a TextView text = (TextView) view; } else { // Otherwise, find the TextView field within the layout text = view.findViewById(mFieldId); if (text == null) { throw new RuntimeException("Failed to find view with ID " + mContext.getResources().getResourceName(mFieldId) + " in item layout"); } } } catch (ClassCastException e) { Log.e("ArrayAdapter", "You must supply a resource ID for a TextView"); throw new IllegalStateException("ArrayAdapter requires the resource ID to be a TextView", e); } final T item = getItem(position); if (item instanceof CharSequence) { text.setText((CharSequence) item); } else { text.setText(item.toString()); } return view; } /** * <p>Sets the layout resource to create the drop down views.</p> * * @param resource the layout resource defining the drop down views * @see #getDropDownView(int, android.view.View, android.view.ViewGroup) */ public void setDropDownViewResource(@LayoutRes int resource) { this.mDropDownResource = resource; } /** * Sets the {@link Resources.Theme} against which drop-down views are * inflated. * <p> * By default, drop-down views are inflated against the theme of the * {@link Context} passed to the adapter's constructor. * * @param theme the theme against which to inflate drop-down views or * {@code null} to use the theme from the adapter's context * @see #getDropDownView(int, View, ViewGroup) */ @Override public void setDropDownViewTheme(@Nullable Resources.Theme theme) { if (theme == null) { mDropDownInflater = null; } else if (theme == mInflater.getContext().getTheme()) { mDropDownInflater = mInflater; } else { final Context context = new ContextThemeWrapper(mContext, theme); mDropDownInflater = LayoutInflater.from(context); } } @Override public @Nullable Resources.Theme getDropDownViewTheme() { return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme(); } @Override public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { final LayoutInflater inflater = mDropDownInflater == null ? mInflater : mDropDownInflater; return createViewFromResource(inflater, position, convertView, parent, mDropDownResource); } /** * Creates a new ArrayAdapter from external resources. The content of the array is * obtained through {@link android.content.res.Resources#getTextArray(int)}. * * @param context The application's environment. * @param textArrayResId The identifier of the array to use as the data source. * @param textViewResId The identifier of the layout used to create views. * * @return An ArrayAdapter<CharSequence>. */ public static @NonNull ArrayAdapter<CharSequence> createFromResource(@NonNull Context context, @ArrayRes int textArrayResId, @LayoutRes int textViewResId) { final CharSequence[] strings = context.getResources().getTextArray(textArrayResId); return new ArrayAdapter<>(context, textViewResId, 0, Arrays.asList(strings), true); } @Override public @NonNull Filter getFilter() { if (mFilter == null) { mFilter = new ArrayFilter(); } return mFilter; } /** * {@inheritDoc} * * @return values from the string array used by {@link #createFromResource(Context, int, int)}, * or {@code null} if object was created otherwsie or if contents were dynamically changed after * creation. */ @Override public CharSequence[] getAutofillOptions() { // First check if app developer explicitly set them. final CharSequence[] explicitOptions = super.getAutofillOptions(); if (explicitOptions != null) { return explicitOptions; } // Otherwise, only return options that came from static resources. if (!mObjectsFromResources || mObjects == null || mObjects.isEmpty()) { return null; } final int size = mObjects.size(); final CharSequence[] options = new CharSequence[size]; mObjects.toArray(options); return options; } /** * <p>An array filter constrains the content of the array adapter with * a prefix. Each item that does not start with the supplied prefix * is removed from the list.</p> */ private class ArrayFilter extends Filter { @Override protected FilterResults performFiltering(CharSequence prefix) { final FilterResults results = new FilterResults(); if (mOriginalValues == null) { synchronized (mLock) { mOriginalValues = new ArrayList<>(mObjects); } } if (prefix == null || prefix.length() == 0) { final ArrayList<T> list; synchronized (mLock) { list = new ArrayList<>(mOriginalValues); } results.values = list; results.count = list.size(); } else { final String prefixString = prefix.toString().toLowerCase(); final ArrayList<T> values; synchronized (mLock) { values = new ArrayList<>(mOriginalValues); } final int count = values.size(); final ArrayList<T> newValues = new ArrayList<>(); for (int i = 0; i < count; i++) { final T value = values.get(i); final String valueText = value.toString().toLowerCase(); // First match against the whole, non-splitted value if (valueText.startsWith(prefixString)) { newValues.add(value); } else { final String[] words = valueText.split(" "); for (String word : words) { if (word.startsWith(prefixString)) { newValues.add(value); break; } } } } results.values = newValues; results.count = newValues.size(); } return results; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { //noinspection unchecked mObjects = (List<T>) results.values; if (results.count > 0) { notifyDataSetChanged(); } else { notifyDataSetInvalidated(); } } } }