Java tutorial
/* * Copyright 2014 Google Inc. All rights reserved. * * 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.google.samples.apps.iosched.ui.widget; import android.content.Context; import android.content.res.TypedArray; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.BaseAdapter; import android.widget.LinearLayout; import android.widget.ListView; import com.google.samples.apps.iosched.R; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import static com.google.samples.apps.iosched.util.LogUtils.LOGD; import static com.google.samples.apps.iosched.util.LogUtils.LOGE; import static com.google.samples.apps.iosched.util.LogUtils.LOGW; import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag; /** * This Widget can be used to display a list of items separated with headers. The list of items of * each group can be displayed in several columns below each headers. * * A {@link CollectionViewCallbacks} must be defined using * {@link #setCollectionAdapter(CollectionViewCallbacks)} to create the layout of each elements: * headers and items. Alternatively a {@link CollectionViewCallbacks.GroupCollectionViewCallbacks} * can be defined to also specify a custom GroupView where each groups will be contained. * * Then {@link #updateInventory(CollectionView.Inventory)} has to be called to specify the items and * groups of the Collection. The number of columns can be defined for each groups using * {@link CollectionView.InventoryGroup#setDisplayCols(int)}. */ public class CollectionView extends ListView { private static final String TAG = makeLogTag(CollectionView.class); private static final int BUILTIN_VIEWTYPE_HEADER = 0; private static final int BUILTIN_VIEWTYPE_COUNT = 1; private static final int BUILTIN_VIEWTYPE_GROUP = 0; private Inventory mInventory = new Inventory(); private CollectionViewCallbacks mCallbacks = null; private boolean mCustomGroupViewDisabled = false; private int mContentTopClearance = 0; private int mInternalPadding; private MultiScrollListener mMultiScrollListener; public CollectionView(Context context) { this(context, null); } public CollectionView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CollectionView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setAdapter(new MyListAdapter()); setDivider(null); setDividerHeight(0); setItemsCanFocus(false); setChoiceMode(ListView.CHOICE_MODE_NONE); setSelector(android.R.color.transparent); if (attrs != null) { final TypedArray xmlArgs = context.obtainStyledAttributes(attrs, R.styleable.CollectionView, defStyle, 0); mInternalPadding = xmlArgs.getDimensionPixelSize(R.styleable.CollectionView_internalPadding, 0); mContentTopClearance = xmlArgs.getDimensionPixelSize(R.styleable.CollectionView_contentTopClearance, 0); xmlArgs.recycle(); } } /** * Returns true if a custom container View should be used for each groups. */ private boolean hasCustomGroupView() { return !mCustomGroupViewDisabled && mCallbacks instanceof CollectionViewCallbacks.GroupCollectionViewCallbacks; } /** * Disables using a custom GroupView container for Groups even if {@code mCallbacks} implements * {@link CollectionViewCallbacks.GroupCollectionViewCallbacks}. */ private void disableCustomGroupView() { mCustomGroupViewDisabled = true; } /** * Enables using a custom GroupView container for Groups if {@code mCallbacks} implements * {@link CollectionViewCallbacks.GroupCollectionViewCallbacks}. */ private void enableCustomGroupView() { mCustomGroupViewDisabled = false; } /** * Updates the elements displayed on this View with the given {@link CollectionView.Inventory}. */ public void updateInventory(final Inventory inv) { updateInventory(inv, true); } /** * Updates the elements displayed on this View with the given {@link CollectionView.Inventory} * with the ability to enable/disable the animation on appearing elements. */ public void updateInventory(final Inventory inv, boolean animate) { if (animate) { LOGD(TAG, "CollectionView updating inventory with animation."); setAlpha(0); updateInventoryImmediate(inv, true); doFadeInAnimation(); } else { LOGD(TAG, "CollectionView updating inventory without animation."); updateInventoryImmediate(inv, false); } } private void updateInventoryImmediate(Inventory inv, boolean animate) { mInventory = new Inventory(inv); notifyAdapterDataSetChanged(); if (animate) { startLayoutAnimation(); } } /** * Starts a fade-in animation. */ private void doFadeInAnimation() { setAlpha(0); ViewCompat.animate(this).setDuration(250).alpha(1).withLayer(); } /** * Registers the given {@link CollectionViewCallbacks} that will be used to create Views for * each elements of the collection. */ public void setCollectionAdapter(CollectionViewCallbacks adapter) { mCallbacks = adapter; } private void notifyAdapterDataSetChanged() { // We have to set up a new adapter (as opposed to just calling notifyDataSetChanged() // because we might need MORE view types than before, and ListView isn't prepared to // handle the case where its existing adapter suddenly needs to increase the number of // view types it needs. setAdapter(new MyListAdapter()); } /** * Programmatically sets a clearance space above the element. * * @param clearance Space to clear above the element in pixels. */ public void setContentTopClearance(int clearance) { if (mContentTopClearance != clearance) { mContentTopClearance = clearance; setPadding(getPaddingLeft(), mContentTopClearance, getPaddingRight(), getPaddingBottom()); notifyAdapterDataSetChanged(); } } private class RowComputeResult { int row; boolean isHeader; int groupId; InventoryGroup group; int groupOffset; } private boolean computeRowContent(int row, RowComputeResult result) { int curRow = 0; int posInGroup; if (hasCustomGroupView()) { if (row >= mInventory.mGroups.size()) { return false; } else { InventoryGroup group = mInventory.mGroups.get(row); // row is a group container! result.row = row; result.isHeader = false; result.groupId = group.mGroupId; result.group = group; result.groupOffset = 0; for (int i = 0; i < row; i++) { InventoryGroup previousGroup = mInventory.mGroups.get(i); result.groupOffset += previousGroup.getRowCount(); } return true; } } for (InventoryGroup group : mInventory.mGroups) { if (group.mShowHeader) { if (curRow == row) { // row is a group header! result.row = row; result.isHeader = true; result.groupId = group.mGroupId; result.group = group; result.groupOffset = -1; return true; } curRow++; } posInGroup = 0; while (posInGroup < group.mItemCount) { if (curRow == row) { // this is the row we are looking for result.row = row; result.isHeader = false; result.groupId = group.mGroupId; result.group = group; result.groupOffset = posInGroup; return true; } // advance to next row posInGroup += group.mDisplayCols; curRow++; } } return false; } /** * A {@link BaseAdapter} for the underlying ListView. */ protected class MyListAdapter extends BaseAdapter { @Override public int getCount() { // If we have defined a custom group view we can't display an item on each row but // instead we'll display a group on each row. if (hasCustomGroupView()) { return mInventory.mGroups.size(); } int rowCount = 0; for (InventoryGroup group : mInventory.mGroups) { int thisGroupRowCount = group.getRowCount(); rowCount += thisGroupRowCount; } return rowCount; } @Override public Object getItem(int position) { return position; } @Override public long getItemId(int position) { return position; } @Override public View getView(int row, View convertView, ViewGroup parent) { return getRowView(row, convertView, parent); } @Override public int getItemViewType(int row) { return getRowViewType(row); } @Override public int getViewTypeCount() { if (hasCustomGroupView()) { return 1; } else { return BUILTIN_VIEWTYPE_COUNT + mInventory.mGroups.size(); } } } private View getRowView(int row, View convertView, ViewGroup parent) { RowComputeResult mRowComputeResult = new RowComputeResult(); if (computeRowContent(row, mRowComputeResult)) { return makeRow(convertView, mRowComputeResult, parent); } else { Log.e(TAG, "Invalid row passed to getView: " + row); return convertView != null ? convertView : new View(getContext()); } } private int getRowViewType(int row) { RowComputeResult mRowComputeResult = new RowComputeResult(); if (computeRowContent(row, mRowComputeResult)) { if (hasCustomGroupView()) { return BUILTIN_VIEWTYPE_GROUP; } else if (mRowComputeResult.isHeader) { return BUILTIN_VIEWTYPE_HEADER; } else { return BUILTIN_VIEWTYPE_COUNT + mInventory.getGroupIndex(mRowComputeResult.groupId); } } else { Log.e(TAG, "Invalid row passed to getItemViewType: " + row); return 0; } } @Override public void setOnScrollListener(OnScrollListener listener) { if (mMultiScrollListener == null) { mMultiScrollListener = new MultiScrollListener(); super.setOnScrollListener(mMultiScrollListener); } mMultiScrollListener.addOnScrollListener(listener); } private View makeRow(View view, RowComputeResult rowInfo, ViewGroup parent) { if (mCallbacks == null) { Log.e(TAG, "Call to makeRow without an adapter installed"); return view != null ? view : new View(getContext()); } // Notice that view types are tied to a specific instance of mInventory by hashcode, // so when mInventory is updated, we don't attempt to reuse views that were used for // a previous incarnation of mInventory (the views may be incompatible). String desiredViewType = mInventory.hashCode() + "." + getRowViewType(rowInfo.row); String actualViewType = (view != null && view.getTag() != null) ? view.getTag().toString() : ""; if (!desiredViewType.equals(actualViewType)) { // We can't recycle this view. We have to make a new one. view = null; } if (rowInfo.isHeader) { if (view == null) { view = mCallbacks.newCollectionHeaderView(getContext(), rowInfo.groupId, parent); } mCallbacks.bindCollectionHeaderView(getContext(), view, rowInfo.groupId, rowInfo.group.mHeaderLabel, rowInfo.group.mHeaderTag); } else { view = makeItemRow(view, rowInfo); } view.setTag(desiredViewType); return view; } private View makeItemRow(View convertView, RowComputeResult rowInfo) { if (convertView == null) { return makeNewItemRow(rowInfo); } else { return recycleItemRow(convertView, rowInfo); } } private static class EmptyView extends View { private EmptyView(Context ctx) { super(ctx); } } private View createGroupView(RowComputeResult rowInfo, View view, ViewGroup parent) { ViewGroup groupView; if (view != null && view instanceof ViewGroup) { groupView = (ViewGroup) view; // If there are more children in the recycled view we remove the extra ones. if (groupView.getChildAt(0) instanceof LinearLayout) { LinearLayout groupViewContent = (LinearLayout) groupView.getChildAt(0); if (groupViewContent.getChildCount() > rowInfo.group.getRowCount()) { groupViewContent.removeViews(rowInfo.group.getRowCount(), groupViewContent.getChildCount() - rowInfo.group.getRowCount()); } } // Use the defined callbacks if the user has chosen to by implementing a // CardsCollectionViewCallbacks. } else if (mCallbacks instanceof CollectionViewCallbacks.GroupCollectionViewCallbacks) { groupView = ((CollectionViewCallbacks.GroupCollectionViewCallbacks) mCallbacks) .newCollectionGroupView(getContext(), rowInfo.groupId, rowInfo.group, parent); // This should never happened but if it does we'll display an EmptyView. } else { LOGE(TAG, "Tried to create a group view but the callback is not an instance of " + "GroupCollectionViewCallbacks"); return new EmptyView(getContext()); } LinearLayout groupViewContent; if (groupView.getChildAt(0) instanceof LinearLayout) { groupViewContent = (LinearLayout) groupView.getChildAt(0); } else { groupViewContent = new LinearLayout(getContext()); groupViewContent.setOrientation(LinearLayout.VERTICAL); LayoutParams LLParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); groupViewContent.setLayoutParams(LLParams); groupView.addView(groupViewContent); } disableCustomGroupView(); for (int i = 0; i < rowInfo.group.getRowCount(); i++) { View itemView; try { itemView = getRowView(rowInfo.groupOffset + i, groupViewContent.getChildAt(i), groupViewContent); } catch (Exception e) { // Recycling failed (maybe the items were not compatible) so we start again without // recycling. itemView = getRowView(rowInfo.groupOffset + i, null, groupViewContent); } if (itemView != groupViewContent.getChildAt(i)) { if (groupViewContent.getChildCount() > i) { groupViewContent.removeViewAt(i); } groupViewContent.addView(itemView, i); } } enableCustomGroupView(); return groupView; } private View getItemView(RowComputeResult rowInfo, int column, View view, ViewGroup parent) { // If there is a custom group container view defined. if (hasCustomGroupView()) { return createGroupView(rowInfo, view, parent); } // In the case of regular list view without group containers. int indexInGroup = rowInfo.groupOffset + column; if (indexInGroup >= rowInfo.group.mItemCount) { // out of bounds, so use an empty view if (view != null && view instanceof EmptyView) { return view; } view = new EmptyView(getContext()); view.setLayoutParams( new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); return view; } if (view == null || view instanceof EmptyView) { view = mCallbacks.newCollectionItemView(getContext(), rowInfo.groupId, parent); } mCallbacks.bindCollectionItemView(getContext(), view, rowInfo.groupId, indexInGroup, rowInfo.group.getDataIndex(indexInGroup), rowInfo.group.getItemTag(rowInfo.groupOffset + column)); return view; } private void setupLayoutParams(View view) { // In case a custom layout for the groups are defined we don't set internal padding on the // group container. if (hasCustomGroupView()) { return; } LinearLayout.LayoutParams viewLayoutParams; if (view.getLayoutParams() instanceof LinearLayout.LayoutParams) { viewLayoutParams = (LinearLayout.LayoutParams) view.getLayoutParams(); } else { // This shouldn't happen... but if it does, let's work around it as well as we can. LOGW(TAG, "Unexpected class for collection view item's layout params: " + view.getLayoutParams().getClass().getName()); viewLayoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); } viewLayoutParams.leftMargin = mInternalPadding / 2; viewLayoutParams.rightMargin = mInternalPadding / 2; viewLayoutParams.bottomMargin = mInternalPadding; viewLayoutParams.width = LayoutParams.MATCH_PARENT; viewLayoutParams.weight = 1.0f; view.setLayoutParams(viewLayoutParams); } private View makeNewItemRow(RowComputeResult rowInfo) { LinearLayout ll = new LinearLayout(getContext()); LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); ll.setOrientation(LinearLayout.HORIZONTAL); ll.setLayoutParams(params); int nbColumns = rowInfo.group.mDisplayCols; if (hasCustomGroupView()) { nbColumns = 1; } for (int i = 0; i < nbColumns; i++) { View view = getItemView(rowInfo, i, null, ll); setupLayoutParams(view); ll.addView(view); } return ll; } private View recycleItemRow(View convertView, RowComputeResult rowInfo) { LinearLayout ll = (LinearLayout) convertView; int nbColumns = rowInfo.group.mDisplayCols; if (hasCustomGroupView()) { nbColumns = 1; } for (int i = 0; i < nbColumns; i++) { View view = ll.getChildAt(i); View newView = getItemView(rowInfo, i, view, ll); if (view != newView) { setupLayoutParams(newView); ll.removeViewAt(i); ll.addView(newView, i); } } return ll; } /** * Represents the data of the items to display in the {@link CollectionView}. * This is defined as a list of {@link InventoryGroup} which represents a group of items with a * header. */ public static class Inventory { private ArrayList<InventoryGroup> mGroups = new ArrayList<>(); public Inventory() { } public Inventory(Inventory copyFrom) { for (InventoryGroup group : copyFrom.mGroups) { mGroups.add(group); } } public void addGroup(InventoryGroup group) { if (group.mItemCount > 0) { mGroups.add(new InventoryGroup(group)); } } public int getTotalItemCount() { int total = 0; for (InventoryGroup group : mGroups) { total += group.mItemCount; } return total; } public int getGroupCount() { return mGroups.size(); } public int getGroupIndex(int groupId) { for (int i = 0; i < mGroups.size(); i++) { if (mGroups.get(i).mGroupId == groupId) { return i; } } return -1; } } private static class MultiScrollListener implements OnScrollListener { private final Set<OnScrollListener> children = new HashSet<>(); public void addOnScrollListener(OnScrollListener listener) { children.add(listener); } @Override public void onScrollStateChanged(AbsListView absListView, int i) { for (OnScrollListener listener : children) { listener.onScrollStateChanged(absListView, i); } } @Override public void onScroll(AbsListView absListView, int i, int i2, int i3) { for (OnScrollListener listener : children) { listener.onScroll(absListView, i, i2, i3); } } } /** * Represents a group of items with a header to be displayed in the {@link CollectionView}. */ public static class InventoryGroup implements Cloneable { private int mGroupId = 0; private boolean mShowHeader = false; private String mHeaderLabel = ""; private Object mHeaderTag; private int mDataIndexStart = 0; private int mDisplayCols = 1; private int mItemCount = 0; private SparseArray<Object> mItemTag = new SparseArray<>(); private SparseArray<Integer> mItemCustomDataIndices = new SparseArray<>(); public InventoryGroup(int groupId) { mGroupId = groupId; } public InventoryGroup(InventoryGroup copyFrom) { mGroupId = copyFrom.mGroupId; mShowHeader = copyFrom.mShowHeader; mDataIndexStart = copyFrom.mDataIndexStart; mDisplayCols = copyFrom.mDisplayCols; mItemCount = copyFrom.mItemCount; mHeaderLabel = copyFrom.mHeaderLabel; mHeaderTag = copyFrom.mHeaderTag; mItemTag = cloneSparseArray(copyFrom.mItemTag); mItemCustomDataIndices = cloneSparseArray(copyFrom.mItemCustomDataIndices); } public InventoryGroup setShowHeader(boolean showHeader) { mShowHeader = showHeader; return this; } public InventoryGroup setHeaderLabel(String label) { mHeaderLabel = label; return this; } public String getHeaderLabel() { return mHeaderLabel; } public InventoryGroup setHeaderTag(Object headerTag) { mHeaderTag = headerTag; return this; } public Object getHeaderTag() { return mHeaderTag; } public InventoryGroup setDataIndexStart(int dataIndexStart) { mDataIndexStart = dataIndexStart; return this; } public InventoryGroup setCustomDataIndex(int groupIndex, int customDataIndex) { mItemCustomDataIndices.put(groupIndex, customDataIndex); return this; } public int getDataIndex(int indexInGroup) { return mItemCustomDataIndices.get(indexInGroup, mDataIndexStart + indexInGroup); } public InventoryGroup setDisplayCols(int cols) { mDisplayCols = cols > 1 ? cols : 1; return this; } public InventoryGroup setItemCount(int count) { mItemCount = count; return this; } public InventoryGroup incrementItemCount() { mItemCount++; return this; } public InventoryGroup setItemTag(int index, Object tag) { mItemTag.put(index, tag); return this; } public InventoryGroup addItemWithTag(Object tag) { mItemCount++; setItemTag(mItemCount - 1, tag); return this; } public InventoryGroup addItemWithCustomDataIndex(int customDataIndex) { mItemCount++; setCustomDataIndex(mItemCount - 1, customDataIndex); return this; } public int getRowCount() { return (mShowHeader ? 1 : 0) + (mItemCount / mDisplayCols) + ((mItemCount % mDisplayCols > 0) ? 1 : 0); } public Object getItemTag(int i) { return mItemTag.get(i, null); } private static <E> SparseArray<E> cloneSparseArray(SparseArray<E> orig) { SparseArray<E> result = new SparseArray<E>(); for (int i = 0; i < orig.size(); i++) { result.put(orig.keyAt(i), orig.valueAt(i)); } return result; } } }