1 /*
2  * Copyright (C) 2010 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 com.android.contacts.list;
17 
18 import android.content.Context;
19 import android.view.View;
20 import android.view.ViewGroup;
21 import android.widget.ListView;
22 import android.widget.SectionIndexer;
23 
24 /**
25  * A list adapter that supports section indexer and a pinned header.
26  */
27 public abstract class IndexerListAdapter extends PinnedHeaderListAdapter implements SectionIndexer {
28 
29     protected Context mContext;
30     private SectionIndexer mIndexer;
31     private int mIndexedPartition = 0;
32     private boolean mSectionHeaderDisplayEnabled;
33     private View mHeader;
34 
35     /**
36      * An item view is displayed differently depending on whether it is placed
37      * at the beginning, middle or end of a section. It also needs to know the
38      * section header when it is at the beginning of a section. This object
39      * captures all this configuration.
40      */
41     public static final class Placement {
42         private int position = ListView.INVALID_POSITION;
43         public boolean firstInSection;
44         public boolean lastInSection;
45         public String sectionHeader;
46 
invalidate()47         public void invalidate() {
48             position = ListView.INVALID_POSITION;
49         }
50     }
51 
52     private Placement mPlacementCache = new Placement();
53 
54     /**
55      * Constructor.
56      */
IndexerListAdapter(Context context)57     public IndexerListAdapter(Context context) {
58         super(context);
59         mContext = context;
60     }
61 
62     /**
63      * Creates a section header view that will be pinned at the top of the list
64      * as the user scrolls.
65      */
createPinnedSectionHeaderView(Context context, ViewGroup parent)66     protected abstract View createPinnedSectionHeaderView(Context context, ViewGroup parent);
67 
68     /**
69      * Sets the title in the pinned header as the user scrolls.
70      */
setPinnedSectionTitle(View pinnedHeaderView, String title)71     protected abstract void setPinnedSectionTitle(View pinnedHeaderView, String title);
72 
isSectionHeaderDisplayEnabled()73     public boolean isSectionHeaderDisplayEnabled() {
74         return mSectionHeaderDisplayEnabled;
75     }
76 
setSectionHeaderDisplayEnabled(boolean flag)77     public void setSectionHeaderDisplayEnabled(boolean flag) {
78         this.mSectionHeaderDisplayEnabled = flag;
79     }
80 
getIndexedPartition()81     public int getIndexedPartition() {
82         return mIndexedPartition;
83     }
84 
setIndexedPartition(int partition)85     public void setIndexedPartition(int partition) {
86         this.mIndexedPartition = partition;
87     }
88 
getIndexer()89     public SectionIndexer getIndexer() {
90         return mIndexer;
91     }
92 
setIndexer(SectionIndexer indexer)93     public void setIndexer(SectionIndexer indexer) {
94         mIndexer = indexer;
95         mPlacementCache.invalidate();
96     }
97 
getSections()98     public Object[] getSections() {
99         if (mIndexer == null) {
100             return new String[] { " " };
101         } else {
102             return mIndexer.getSections();
103         }
104     }
105 
106     /**
107      * @return relative position of the section in the indexed partition
108      */
getPositionForSection(int sectionIndex)109     public int getPositionForSection(int sectionIndex) {
110         if (mIndexer == null) {
111             return -1;
112         }
113 
114         return mIndexer.getPositionForSection(sectionIndex);
115     }
116 
117     /**
118      * @param position relative position in the indexed partition
119      */
getSectionForPosition(int position)120     public int getSectionForPosition(int position) {
121         if (mIndexer == null) {
122             return -1;
123         }
124 
125         return mIndexer.getSectionForPosition(position);
126     }
127 
128     @Override
getPinnedHeaderCount()129     public int getPinnedHeaderCount() {
130         if (isSectionHeaderDisplayEnabled()) {
131             return super.getPinnedHeaderCount() + 1;
132         } else {
133             return super.getPinnedHeaderCount();
134         }
135     }
136 
137     @Override
getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent)138     public View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent) {
139         if (isSectionHeaderDisplayEnabled() && viewIndex == getPinnedHeaderCount() - 1) {
140             if (mHeader == null) {
141                 mHeader = createPinnedSectionHeaderView(mContext, parent);
142             }
143             return mHeader;
144         } else {
145             return super.getPinnedHeaderView(viewIndex, convertView, parent);
146         }
147     }
148 
149     @Override
configurePinnedHeaders(PinnedHeaderListView listView)150     public void configurePinnedHeaders(PinnedHeaderListView listView) {
151         super.configurePinnedHeaders(listView);
152 
153         if (!isSectionHeaderDisplayEnabled()) {
154             return;
155         }
156 
157         int index = getPinnedHeaderCount() - 1;
158         if (mIndexer == null || getCount() == 0) {
159             listView.setHeaderInvisible(index, false);
160         } else {
161             int listPosition = listView.getPositionAt(listView.getTotalTopPinnedHeaderHeight());
162             int position = listPosition - listView.getHeaderViewsCount();
163 
164             int section = -1;
165             int partition = getPartitionForPosition(position);
166             if (partition == mIndexedPartition) {
167                 int offset = getOffsetInPartition(position);
168                 if (offset != -1) {
169                     section = getSectionForPosition(offset);
170                 }
171             }
172 
173             if (section == -1) {
174                 listView.setHeaderInvisible(index, false);
175             } else {
176                 View topChild = getViewAtVisiblePosition(listView, listPosition);
177                 if (topChild != null) {
178                     // Match the pinned header's height to the height of the list item.
179                     mHeader.setMinimumHeight(topChild.getMeasuredHeight());
180                 }
181                 setPinnedSectionTitle(mHeader, (String)mIndexer.getSections()[section]);
182 
183                 // Compute the item position where the current partition begins
184                 int partitionStart = getPositionForPartition(mIndexedPartition);
185                 if (hasHeader(mIndexedPartition)) {
186                     partitionStart++;
187                 }
188 
189                 // Compute the item position where the next section begins
190                 int nextSectionPosition = partitionStart + getPositionForSection(section + 1);
191                 boolean isLastInSection = position == nextSectionPosition - 1;
192                 listView.setFadingHeader(index, listPosition, isLastInSection);
193             }
194         }
195     }
196 
197     /**
198      * Returns the view used for the specified list position assuming it is visible or null if
199      * it isn't.
200      *
201      * <p>This makes some assumptions about the implementation of ListView (child views are the
202      * item views and are ordered in the same way as the adapter items they are displaying)
203      * but they are probably safe given that the API is stable.</p>
204      */
getViewAtVisiblePosition(ListView list, int position)205     private View getViewAtVisiblePosition(ListView list, int position) {
206         final int firstVisiblePosition = list.getFirstVisiblePosition();
207         final int childCount = list.getChildCount();
208         final int index = position - firstVisiblePosition;
209         if (index >= 0 && index < childCount) {
210             // Position is on-screen, use existing view.
211             return list.getChildAt(index);
212         } else {
213             return null;
214         }
215     }
216 
217     /**
218      * Computes the item's placement within its section and populates the {@code placement}
219      * object accordingly.  Please note that the returned object is volatile and should be
220      * copied if the result needs to be used later.
221      */
getItemPlacementInSection(int position)222     public Placement getItemPlacementInSection(int position) {
223         if (mPlacementCache.position == position) {
224             return mPlacementCache;
225         }
226 
227         mPlacementCache.position = position;
228         if (isSectionHeaderDisplayEnabled()) {
229             int section = getSectionForPosition(position);
230             if (section != -1 && getPositionForSection(section) == position) {
231                 mPlacementCache.firstInSection = true;
232                 mPlacementCache.sectionHeader = (String)getSections()[section];
233             } else {
234                 mPlacementCache.firstInSection = false;
235                 mPlacementCache.sectionHeader = null;
236             }
237 
238             mPlacementCache.lastInSection = (getPositionForSection(section + 1) - 1 == position);
239         } else {
240             mPlacementCache.firstInSection = false;
241             mPlacementCache.lastInSection = false;
242             mPlacementCache.sectionHeader = null;
243         }
244         return mPlacementCache;
245     }
246 }
247