├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── ListViewVariants.iml ├── README.md ├── app ├── .gitignore ├── Sample.iml ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── lb │ │ └── listviewvariants │ │ ├── MainActivity.java │ │ └── utils │ │ ├── CircularContactView.java │ │ ├── ContactImageUtil.java │ │ ├── ContactsQuery.java │ │ ├── ImageCache.java │ │ └── async_task_thread_pool │ │ ├── AsyncTaskEx.java │ │ ├── AsyncTaskThreadPool.java │ │ ├── ForceQueuePolicy.java │ │ ├── ScalingQueue.java │ │ └── ScalingThreadPoolExecutor.java │ └── res │ ├── drawable-hdpi │ ├── ic_action_action_info_outline.png │ └── ic_launcher.png │ ├── drawable-mdpi │ ├── ic_action_action_info_outline.png │ └── ic_launcher.png │ ├── drawable-v21 │ ├── listview_ripple_background_selector.xml │ └── listview_selector.xml │ ├── drawable-xhdpi │ ├── ic_action_action_info_outline.png │ ├── ic_launcher.png │ └── ic_person_white_120dp.png │ ├── drawable-xxhdpi │ ├── ic_action_action_info_outline.png │ └── ic_launcher.png │ ├── drawable-xxxhdpi │ └── ic_action_action_info_outline.png │ ├── drawable │ └── listview_selector.xml │ ├── layout │ ├── activity_main.xml │ ├── listview_item.xml │ └── pinned_header_listview_side_header.xml │ ├── menu │ └── activity_main.xml │ └── values │ ├── color.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── demo.gif ├── device-2014-12-28-230610.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle ├── library.iml ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── lb │ └── library │ ├── BasePinnedHeaderListViewAdapter.java │ ├── BaseSectionedAdapter.java │ ├── IndexedPinnedHeaderListViewAdapter.java │ ├── PinnedHeaderListView.java │ ├── SearchablePinnedHeaderListViewAdapter.java │ ├── SectionedSectionIndexer.java │ └── StringArrayAlphabetIndexer.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | ListViewVariants -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 26 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 53 | 54 | 55 | 56 | 57 | 1.8 58 | 59 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /ListViewVariants.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ListViewVariants 2 | ================ 3 | 4 | Provides special ways to handle ListViews, including PinnedHeaderListView in Lollipop's Contacts-app style 5 | 6 | Screenshot 7 | ---------- 8 | Here's a screenshot of how it shows the contacts of the device, very similar to how Lollipop's Contacts-app shows it , except for the blur, which I've added myself ... :) 9 | 10 | ![animated demo](https://raw.githubusercontent.com/AndroidDeveloperLB/ListViewVariants/master/demo.gif) 11 | 12 | ![enter image description here](https://raw.githubusercontent.com/AndroidDeveloperLB/ListViewVariants/master/device-2014-12-28-230610.png) 13 | 14 | Requirements 15 | ------------ 16 | The min API level is 8. 17 | 18 | Also, in case you wish to use any class that causes the headers to be ordered, you must sort the items accordingly. 19 | 20 | In case the headers should all be in uppercase, the sorting should make sure that items that start with each letter (uppercase or not) will be together (for example "Dan" and "duke" should be together on the same chunk that belongs to "D"). 21 | 22 | Known Issues 23 | ------------ 24 | 25 | 1. Missing some documentations and also some samples. Hope to work on this when I get the time. :) 26 | 2. RTL alignment on the headers (of the sample) isn't supported yet. Maybe it's a simple matter to fix. 27 | However, this doesn't mean that it won't work on RTL locale of the device. Just not that the UI components will get mirrored (meaning it will look exactly like for English locale, for example) ... 28 | 29 | Note 30 | ---- 31 | There is another nice alternative library for RecyclerView, here: 32 | https://github.com/TonicArtos/SuperSLiM 33 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/Sample.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "21.1.2" 6 | 7 | defaultConfig { 8 | applicationId "lb.listviewvariants" 9 | minSdkVersion 8 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | compile 'com.android.support:appcompat-v7:23.2.1' 25 | compile project(':library') 26 | } 27 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\android/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/lb/listviewvariants/MainActivity.java: -------------------------------------------------------------------------------- 1 | package lb.listviewvariants; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.content.pm.PackageManager; 6 | import android.database.Cursor; 7 | import android.graphics.Bitmap; 8 | import android.media.ThumbnailUtils; 9 | import android.net.Uri; 10 | import android.os.Bundle; 11 | import android.provider.ContactsContract; 12 | import android.support.v4.view.MenuItemCompat; 13 | import android.support.v7.app.ActionBarActivity; 14 | import android.support.v7.widget.SearchView; 15 | import android.support.v7.widget.SearchView.OnQueryTextListener; 16 | import android.text.TextUtils; 17 | import android.util.TypedValue; 18 | import android.view.LayoutInflater; 19 | import android.view.Menu; 20 | import android.view.MenuItem; 21 | import android.view.View; 22 | import android.view.ViewGroup; 23 | import android.widget.TextView; 24 | 25 | import java.util.ArrayList; 26 | import java.util.Collections; 27 | import java.util.Comparator; 28 | import java.util.List; 29 | import java.util.Locale; 30 | import java.util.Random; 31 | 32 | import lb.library.PinnedHeaderListView; 33 | import lb.library.SearchablePinnedHeaderListViewAdapter; 34 | import lb.library.StringArrayAlphabetIndexer; 35 | import lb.listviewvariants.utils.CircularContactView; 36 | import lb.listviewvariants.utils.ContactImageUtil; 37 | import lb.listviewvariants.utils.ContactsQuery; 38 | import lb.listviewvariants.utils.ImageCache; 39 | import lb.listviewvariants.utils.async_task_thread_pool.AsyncTaskEx; 40 | import lb.listviewvariants.utils.async_task_thread_pool.AsyncTaskThreadPool; 41 | 42 | 43 | public class MainActivity extends ActionBarActivity { 44 | private LayoutInflater mInflater; 45 | private PinnedHeaderListView mListView; 46 | private ContactsAdapter mAdapter; 47 | 48 | @Override 49 | protected void onCreate(Bundle savedInstanceState) { 50 | super.onCreate(savedInstanceState); 51 | mInflater = LayoutInflater.from(MainActivity.this); 52 | setContentView(R.layout.activity_main); 53 | final ArrayList contacts = getContacts(); 54 | Collections.sort(contacts, new Comparator() { 55 | @Override 56 | public int compare(Contact lhs, Contact rhs) { 57 | char lhsFirstLetter = TextUtils.isEmpty(lhs.displayName) ? ' ' : lhs.displayName.charAt(0); 58 | char rhsFirstLetter = TextUtils.isEmpty(rhs.displayName) ? ' ' : rhs.displayName.charAt(0); 59 | int firstLetterComparison = Character.toUpperCase(lhsFirstLetter) - Character.toUpperCase(rhsFirstLetter); 60 | if (firstLetterComparison == 0) 61 | return lhs.displayName.compareTo(rhs.displayName); 62 | return firstLetterComparison; 63 | } 64 | }); 65 | mListView = (PinnedHeaderListView) findViewById(android.R.id.list); 66 | mAdapter = new ContactsAdapter(contacts); 67 | 68 | int pinnedHeaderBackgroundColor = getResources().getColor(getResIdFromAttribute(this, android.R.attr.colorBackground)); 69 | mAdapter.setPinnedHeaderBackgroundColor(pinnedHeaderBackgroundColor); 70 | mAdapter.setPinnedHeaderTextColor(getResources().getColor(R.color.pinned_header_text)); 71 | mListView.setPinnedHeaderView(mInflater.inflate(R.layout.pinned_header_listview_side_header, mListView, false)); 72 | mListView.setAdapter(mAdapter); 73 | mListView.setOnScrollListener(mAdapter); 74 | mListView.setEnableHeaderTransparencyChanges(false); 75 | // mAdapter.getFilter().filter(mQueryText,new FilterListener() ... 76 | //You can also perform operations on selected item by using : 77 | // mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() ... 78 | } 79 | 80 | 81 | public static int getResIdFromAttribute(final Activity activity, final int attr) { 82 | if (attr == 0) 83 | return 0; 84 | final TypedValue typedValue = new TypedValue(); 85 | activity.getTheme().resolveAttribute(attr, typedValue, true); 86 | return typedValue.resourceId; 87 | } 88 | 89 | private ArrayList getContacts() { 90 | if (checkContactsReadPermission()) { 91 | Uri uri = ContactsQuery.CONTENT_URI; 92 | final Cursor cursor = managedQuery(uri, ContactsQuery.PROJECTION, ContactsQuery.SELECTION, null, ContactsQuery.SORT_ORDER); 93 | if (cursor == null) 94 | return null; 95 | ArrayList result = new ArrayList<>(); 96 | while (cursor.moveToNext()) { 97 | Contact contact = new Contact(); 98 | contact.contactUri = ContactsContract.Contacts.getLookupUri( 99 | cursor.getLong(ContactsQuery.ID), 100 | cursor.getString(ContactsQuery.LOOKUP_KEY)); 101 | contact.displayName = cursor.getString(ContactsQuery.DISPLAY_NAME); 102 | contact.photoId = cursor.getString(ContactsQuery.PHOTO_THUMBNAIL_DATA); 103 | result.add(contact); 104 | } 105 | 106 | return result; 107 | } 108 | ArrayList result = new ArrayList<>(); 109 | Random r = new Random(); 110 | StringBuilder sb = new StringBuilder(); 111 | for (int i = 0; i < 1000; ++i) { 112 | Contact contact = new Contact(); 113 | sb.delete(0, sb.length()); 114 | int strLength = r.nextInt(10) + 1; 115 | for (int j = 0; j < strLength; ++j) 116 | switch (r.nextInt(3)) { 117 | case 0: 118 | sb.append((char) ('a' + r.nextInt('z' - 'a'))); 119 | break; 120 | case 1: 121 | sb.append((char) ('A' + r.nextInt('Z' - 'A'))); 122 | break; 123 | case 2: 124 | sb.append((char) ('0' + r.nextInt('9' - '0'))); 125 | break; 126 | } 127 | 128 | contact.displayName = sb.toString(); 129 | result.add(contact); 130 | } 131 | return result; 132 | } 133 | 134 | private boolean checkContactsReadPermission() { 135 | String permission = "android.permission.READ_CONTACTS"; 136 | int res = checkCallingOrSelfPermission(permission); 137 | return (res == PackageManager.PERMISSION_GRANTED); 138 | } 139 | 140 | @Override 141 | protected void onDestroy() { 142 | super.onDestroy(); 143 | mAdapter.mAsyncTaskThreadPool.cancelAllTasks(true); 144 | } 145 | 146 | private static class Contact { 147 | long contactId; 148 | Uri contactUri; 149 | String displayName; 150 | String photoId; 151 | } 152 | 153 | @Override 154 | public boolean onCreateOptionsMenu(final Menu menu) { 155 | getMenuInflater().inflate(R.menu.activity_main, menu); 156 | final SearchView searchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.menuItem_search)); 157 | searchView.setOnQueryTextListener(new OnQueryTextListener() { 158 | @Override 159 | public boolean onQueryTextSubmit(final String query) { 160 | return false; 161 | } 162 | 163 | @Override 164 | public boolean onQueryTextChange(final String newText) { 165 | performSearch(newText); 166 | return true; 167 | } 168 | }); 169 | 170 | return super.onCreateOptionsMenu(menu); 171 | } 172 | 173 | public void performSearch(final String queryText) { 174 | mAdapter.getFilter().filter(queryText); 175 | mAdapter.setHeaderViewVisible(TextUtils.isEmpty(queryText)); 176 | } 177 | 178 | @SuppressWarnings("deprecation") 179 | @Override 180 | public boolean onOptionsItemSelected(final MenuItem item) { 181 | String url = null; 182 | switch (item.getItemId()) { 183 | case R.id.menuItem_all_my_apps: 184 | url = "https://play.google.com/store/apps/developer?id=AndroidDeveloperLB"; 185 | break; 186 | case R.id.menuItem_all_my_repositories: 187 | url = "https://github.com/AndroidDeveloperLB"; 188 | break; 189 | case R.id.menuItem_current_repository_website: 190 | url = "https://github.com/AndroidDeveloperLB/ListViewVariants"; 191 | break; 192 | } 193 | if (url == null) 194 | return true; 195 | final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); 196 | intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 197 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 198 | startActivity(intent); 199 | return true; 200 | } 201 | 202 | // //////////////////////////////////////////////////////////// 203 | // ContactsAdapter // 204 | // ////////////////// 205 | private class ContactsAdapter extends SearchablePinnedHeaderListViewAdapter { 206 | private ArrayList mContacts; 207 | private final int CONTACT_PHOTO_IMAGE_SIZE; 208 | private final int[] PHOTO_TEXT_BACKGROUND_COLORS; 209 | private final AsyncTaskThreadPool mAsyncTaskThreadPool = new AsyncTaskThreadPool(1, 2, 10); 210 | 211 | @Override 212 | public CharSequence getSectionTitle(int sectionIndex) { 213 | return ((StringArrayAlphabetIndexer.AlphaBetSection) getSections()[sectionIndex]).getName(); 214 | } 215 | 216 | public ContactsAdapter(final ArrayList contacts) { 217 | setData(contacts); 218 | PHOTO_TEXT_BACKGROUND_COLORS = getResources().getIntArray(R.array.contacts_text_background_colors); 219 | CONTACT_PHOTO_IMAGE_SIZE = getResources().getDimensionPixelSize( 220 | R.dimen.list_item__contact_imageview_size); 221 | } 222 | 223 | public void setData(final ArrayList contacts) { 224 | this.mContacts = contacts; 225 | final String[] generatedContactNames = generateContactNames(contacts); 226 | setSectionIndexer(new StringArrayAlphabetIndexer(generatedContactNames, true)); 227 | } 228 | 229 | private String[] generateContactNames(final List contacts) { 230 | final ArrayList contactNames = new ArrayList(); 231 | if (contacts != null) 232 | for (final Contact contactEntity : contacts) 233 | contactNames.add(contactEntity.displayName); 234 | return contactNames.toArray(new String[contactNames.size()]); 235 | } 236 | 237 | @Override 238 | public View getView(final int position, final View convertView, final ViewGroup parent) { 239 | final ViewHolder holder; 240 | final View rootView; 241 | if (convertView == null) { 242 | holder = new ViewHolder(); 243 | rootView = mInflater.inflate(R.layout.listview_item, parent, false); 244 | holder.friendProfileCircularContactView = (CircularContactView) rootView 245 | .findViewById(R.id.listview_item__friendPhotoImageView); 246 | holder.friendProfileCircularContactView.getTextView().setTextColor(0xFFffffff); 247 | holder.friendName = (TextView) rootView 248 | .findViewById(R.id.listview_item__friendNameTextView); 249 | holder.headerView = (TextView) rootView.findViewById(R.id.header_text); 250 | rootView.setTag(holder); 251 | } else { 252 | rootView = convertView; 253 | holder = (ViewHolder) rootView.getTag(); 254 | } 255 | final Contact contact = getItem(position); 256 | final String displayName = contact.displayName; 257 | holder.friendName.setText(displayName); 258 | boolean hasPhoto = !TextUtils.isEmpty(contact.photoId); 259 | if (holder.updateTask != null && !holder.updateTask.isCancelled()) 260 | holder.updateTask.cancel(true); 261 | final Bitmap cachedBitmap = hasPhoto ? ImageCache.INSTANCE.getBitmapFromMemCache(contact.photoId) : null; 262 | if (cachedBitmap != null) 263 | holder.friendProfileCircularContactView.setImageBitmap(cachedBitmap); 264 | else { 265 | final int backgroundColorToUse = PHOTO_TEXT_BACKGROUND_COLORS[position 266 | % PHOTO_TEXT_BACKGROUND_COLORS.length]; 267 | if (TextUtils.isEmpty(displayName)) 268 | holder.friendProfileCircularContactView.setImageResource(R.drawable.ic_person_white_120dp, 269 | backgroundColorToUse); 270 | else { 271 | final String characterToShow = TextUtils.isEmpty(displayName) ? "" : displayName.substring(0, 1).toUpperCase(Locale.getDefault()); 272 | holder.friendProfileCircularContactView.setTextAndBackgroundColor(characterToShow, backgroundColorToUse); 273 | } 274 | if (hasPhoto) { 275 | holder.updateTask = new AsyncTaskEx() { 276 | 277 | @Override 278 | public Bitmap doInBackground(final Void... params) { 279 | if (isCancelled()) 280 | return null; 281 | final Bitmap b = ContactImageUtil.loadContactPhotoThumbnail(MainActivity.this, contact.photoId, CONTACT_PHOTO_IMAGE_SIZE); 282 | if (b != null) 283 | return ThumbnailUtils.extractThumbnail(b, CONTACT_PHOTO_IMAGE_SIZE, 284 | CONTACT_PHOTO_IMAGE_SIZE); 285 | return null; 286 | } 287 | 288 | @Override 289 | public void onPostExecute(final Bitmap result) { 290 | super.onPostExecute(result); 291 | if (result == null) 292 | return; 293 | ImageCache.INSTANCE.addBitmapToCache(contact.photoId, result); 294 | holder.friendProfileCircularContactView.setImageBitmap(result); 295 | } 296 | }; 297 | mAsyncTaskThreadPool.executeAsyncTask(holder.updateTask); 298 | } 299 | } 300 | bindSectionHeader(holder.headerView, null, position); 301 | return rootView; 302 | } 303 | 304 | @Override 305 | public boolean doFilter(final Contact item, final CharSequence constraint) { 306 | if (TextUtils.isEmpty(constraint)) 307 | return true; 308 | final String displayName = item.displayName; 309 | return !TextUtils.isEmpty(displayName) && displayName.toLowerCase(Locale.getDefault()) 310 | .contains(constraint.toString().toLowerCase(Locale.getDefault())); 311 | } 312 | 313 | @Override 314 | public ArrayList getOriginalList() { 315 | return mContacts; 316 | } 317 | 318 | 319 | } 320 | 321 | // ///////////////////////////////////////////////////////////////////////////////////// 322 | // ViewHolder // 323 | // ///////////// 324 | private static class ViewHolder { 325 | public CircularContactView friendProfileCircularContactView; 326 | TextView friendName, headerView; 327 | public AsyncTaskEx updateTask; 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /app/src/main/java/lb/listviewvariants/utils/CircularContactView.java: -------------------------------------------------------------------------------- 1 | package lb.listviewvariants.utils; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.graphics.Rect; 7 | import android.graphics.drawable.ShapeDrawable; 8 | import android.graphics.drawable.shapes.OvalShape; 9 | import android.media.ThumbnailUtils; 10 | import android.os.Build; 11 | import android.support.v4.graphics.drawable.RoundedBitmapDrawable; 12 | import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; 13 | import android.util.AttributeSet; 14 | import android.util.TypedValue; 15 | import android.view.Gravity; 16 | import android.widget.FrameLayout; 17 | import android.widget.ImageView; 18 | import android.widget.ImageView.ScaleType; 19 | import android.widget.TextView; 20 | import android.widget.ViewSwitcher; 21 | 22 | import lb.listviewvariants.R; 23 | 24 | public class CircularContactView extends ViewSwitcher 25 | { 26 | // private static final int DEFAULT_CONTENT_SIZE_IN_DP=20; 27 | private ImageView mImageView; 28 | private TextView mTextView; 29 | private Bitmap mBitmap; 30 | private CharSequence mText; 31 | private int mBackgroundColor=0, mImageResId=0; 32 | private int mContentSize; 33 | 34 | public CircularContactView(final Context context) 35 | { 36 | this(context,null); 37 | } 38 | 39 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 40 | public CircularContactView(final Context context,final AttributeSet attrs) 41 | { 42 | super(context,attrs); 43 | addView(mImageView=new ImageView(context),new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, 44 | LayoutParams.MATCH_PARENT,Gravity.CENTER)); 45 | addView(mTextView=new TextView(context),new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, 46 | LayoutParams.MATCH_PARENT,Gravity.CENTER)); 47 | mTextView.setGravity(Gravity.CENTER); 48 | if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.ICE_CREAM_SANDWICH) 49 | mTextView.setAllCaps(true); 50 | mContentSize=getResources().getDimensionPixelSize(R.dimen.list_item__contact_imageview_size); 51 | if(isInEditMode()) 52 | setTextAndBackgroundColor("",0xFFff0000); 53 | } 54 | 55 | public void setContentSize(final int contentSize) 56 | { 57 | this.mContentSize=contentSize; 58 | } 59 | 60 | @SuppressWarnings("deprecation") 61 | private void drawContent(final int viewWidth,final int viewHeight) 62 | { 63 | ShapeDrawable roundedBackgroundDrawable=null; 64 | if(mBackgroundColor!=0) 65 | { 66 | roundedBackgroundDrawable=new ShapeDrawable(new OvalShape()); 67 | roundedBackgroundDrawable.getPaint().setColor(mBackgroundColor); 68 | roundedBackgroundDrawable.setIntrinsicHeight(viewHeight); 69 | roundedBackgroundDrawable.setIntrinsicWidth(viewWidth); 70 | roundedBackgroundDrawable.setBounds(new Rect(0,0,viewWidth,viewHeight)); 71 | } 72 | if(mImageResId!=0) 73 | { 74 | mImageView.setBackgroundDrawable(roundedBackgroundDrawable); 75 | mImageView.setImageResource(mImageResId); 76 | mImageView.setScaleType(ScaleType.CENTER_INSIDE); 77 | } 78 | else if(mText!=null) 79 | { 80 | mTextView.setText(mText); 81 | mTextView.setBackgroundDrawable(roundedBackgroundDrawable); 82 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, viewHeight / 2); 83 | } 84 | else if(mBitmap!=null) 85 | { 86 | mImageView.setScaleType(ScaleType.FIT_CENTER); 87 | mImageView.setBackgroundDrawable(roundedBackgroundDrawable); 88 | if(mBitmap.getWidth()!=mBitmap.getHeight()) 89 | mBitmap=ThumbnailUtils.extractThumbnail(mBitmap,viewWidth,viewHeight); 90 | final RoundedBitmapDrawable roundedBitmapDrawable=RoundedBitmapDrawableFactory.create(getResources(), 91 | mBitmap); 92 | roundedBitmapDrawable.setCornerRadius((mBitmap.getWidth()+mBitmap.getHeight())/4); 93 | mImageView.setImageDrawable(roundedBitmapDrawable); 94 | } 95 | resetValuesState(false); 96 | } 97 | 98 | public void setTextAndBackgroundColor(final CharSequence text,final int backgroundColor) 99 | { 100 | resetValuesState(true); 101 | while(getCurrentView()!=mTextView) 102 | showNext(); 103 | this.mBackgroundColor=backgroundColor; 104 | mText=text; 105 | drawContent(mContentSize,mContentSize); 106 | } 107 | 108 | public void setImageResource(final int imageResId,final int backgroundColor) 109 | { 110 | resetValuesState(true); 111 | while(getCurrentView()!=mImageView) 112 | showNext(); 113 | mImageResId=imageResId; 114 | this.mBackgroundColor=backgroundColor; 115 | drawContent(mContentSize,mContentSize); 116 | } 117 | 118 | public void setImageBitmap(final Bitmap bitmap) 119 | { 120 | setImageBitmapAndBackgroundColor(bitmap,0); 121 | } 122 | 123 | public void setImageBitmapAndBackgroundColor(final Bitmap bitmap,final int backgroundColor) 124 | { 125 | resetValuesState(true); 126 | while(getCurrentView()!=mImageView) 127 | showNext(); 128 | this.mBackgroundColor=backgroundColor; 129 | mBitmap=bitmap; 130 | drawContent(mContentSize,mContentSize); 131 | } 132 | 133 | private void resetValuesState(final boolean alsoResetViews) 134 | { 135 | mBackgroundColor=mImageResId=0; 136 | mBitmap=null; 137 | mText=null; 138 | if(alsoResetViews) 139 | { 140 | mTextView.setText(null); 141 | mTextView.setBackgroundDrawable(null); 142 | mImageView.setImageBitmap(null); 143 | mImageView.setBackgroundDrawable(null); 144 | } 145 | } 146 | 147 | public ImageView getImageView() 148 | { 149 | return mImageView; 150 | } 151 | 152 | public TextView getTextView() 153 | { 154 | return mTextView; 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /app/src/main/java/lb/listviewvariants/utils/ContactImageUtil.java: -------------------------------------------------------------------------------- 1 | package lb.listviewvariants.utils; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.ContentResolver; 5 | import android.content.Context; 6 | import android.content.res.AssetFileDescriptor; 7 | import android.graphics.Bitmap; 8 | import android.graphics.BitmapFactory; 9 | import android.net.Uri; 10 | import android.os.Build; 11 | import android.provider.ContactsContract; 12 | 13 | import java.io.FileDescriptor; 14 | import java.io.FileNotFoundException; 15 | import java.io.IOException; 16 | 17 | import lb.listviewvariants.BuildConfig; 18 | 19 | public class ContactImageUtil 20 | { 21 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 22 | public static Bitmap loadContactPhoto(Context context,Uri contactUri,int imageSize) 23 | { 24 | // Instantiates a ContentResolver for retrieving the Uri of the image 25 | final ContentResolver contentResolver=context.getContentResolver(); 26 | 27 | // Instantiates an AssetFileDescriptor. Given a content Uri pointing to an image file, the 28 | // ContentResolver can return an AssetFileDescriptor for the file. 29 | AssetFileDescriptor afd=null; 30 | if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.ICE_CREAM_SANDWICH) 31 | { 32 | // On platforms running Android 4.0 (API version 14) and later, a high resolution image 33 | // is available from Photo.DISPLAY_PHOTO. 34 | try 35 | { 36 | // Constructs the content Uri for the image 37 | Uri displayImageUri=Uri.withAppendedPath(contactUri,ContactsContract.Contacts.Photo.DISPLAY_PHOTO); 38 | 39 | // Retrieves an AssetFileDescriptor from the Contacts Provider, using the 40 | // constructed Uri 41 | afd=contentResolver.openAssetFileDescriptor(displayImageUri,"r"); 42 | // If the file exists 43 | if(afd!=null) 44 | { 45 | // Reads and decodes the file to a Bitmap and scales it to the desired size 46 | return decodeSampledBitmapFromDescriptor( 47 | afd.getFileDescriptor(),imageSize,imageSize); 48 | } 49 | } 50 | catch(FileNotFoundException e) 51 | { 52 | // Catches file not found exceptions 53 | if(BuildConfig.DEBUG) 54 | { 55 | // Log debug message, this is not an error message as this exception is thrown 56 | // when a contact is legitimately missing a contact photo (which will be quite 57 | // frequently in a long contacts list). 58 | // Log.d(TAG,"Contact photo not found for contact "+contactUri.toString() 59 | // +": "+e.toString()); 60 | } 61 | } 62 | finally 63 | { 64 | // Once the decode is complete, this closes the file. You must do this each time 65 | // you access an AssetFileDescriptor; otherwise, every image load you do will open 66 | // a new descriptor. 67 | if(afd!=null) 68 | { 69 | try 70 | { 71 | afd.close(); 72 | } 73 | catch(IOException e) 74 | { 75 | // Closing a file descriptor might cause an IOException if the file is 76 | // already closed. Nothing extra is needed to handle this. 77 | } 78 | } 79 | } 80 | } 81 | 82 | // If the platform version is less than Android 4.0 (API Level 14), use the only available 83 | // image URI, which points to a normal-sized image. 84 | try 85 | { 86 | // Constructs the image Uri from the contact Uri and the directory twig from the 87 | // Contacts.Photo table 88 | Uri imageUri=Uri.withAppendedPath(contactUri,ContactsContract.Contacts.Photo.CONTENT_DIRECTORY); 89 | 90 | // Retrieves an AssetFileDescriptor from the Contacts Provider, using the constructed 91 | // Uri 92 | afd=contentResolver.openAssetFileDescriptor(imageUri,"r"); 93 | 94 | // If the file exists 95 | if(afd!=null) 96 | { 97 | // Reads the image from the file, decodes it, and scales it to the available screen 98 | // area 99 | return decodeSampledBitmapFromDescriptor( 100 | afd.getFileDescriptor(),imageSize,imageSize); 101 | } 102 | } 103 | catch(FileNotFoundException e) 104 | { 105 | // Catches file not found exceptions 106 | if(BuildConfig.DEBUG) 107 | { 108 | // Log.d(TAG,"Contact photo not found for contact "+contactUri.toString() 109 | // +": "+e.toString()); 110 | } 111 | } 112 | finally 113 | { 114 | // Once the decode is complete, this closes the file. You must do this each time you 115 | // access an AssetFileDescriptor; otherwise, every image load you do will open a new 116 | // descriptor. 117 | if(afd!=null) 118 | { 119 | try 120 | { 121 | afd.close(); 122 | } 123 | catch(IOException e) 124 | { 125 | // Closing a file descriptor might cause an IOException if the file is 126 | // already closed. Ignore this. 127 | } 128 | } 129 | } 130 | 131 | // If none of the case selectors match, returns null. 132 | return null; 133 | } 134 | 135 | /** 136 | * Decodes and scales a contact's image from a file pointed to by a Uri in the contact's data, 137 | * and returns the result as a Bitmap. The column that contains the Uri varies according to the 138 | * platform version. 139 | * 140 | * @param photoData For platforms prior to Android 3.0, provide the Contact._ID column value. 141 | * For Android 3.0 and later, provide the Contact.PHOTO_THUMBNAIL_URI value. 142 | * @param imageSize The desired target width and height of the output image in pixels. 143 | * @return A Bitmap containing the contact's image, resized to fit the provided image size. If 144 | * no thumbnail exists, returns null. 145 | */ 146 | public static Bitmap loadContactPhotoThumbnail(Context context,String photoData,int imageSize) 147 | { 148 | // Instantiates an AssetFileDescriptor. Given a content Uri pointing to an image file, the 149 | // ContentResolver can return an AssetFileDescriptor for the file. 150 | AssetFileDescriptor afd=null; 151 | // This "try" block catches an Exception if the file descriptor returned from the Contacts 152 | // Provider doesn't point to an existing file. 153 | try 154 | { 155 | Uri thumbUri; 156 | // If Android 3.0 or later, converts the Uri passed as a string to a Uri object. 157 | if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB) 158 | thumbUri=Uri.parse(photoData); 159 | else 160 | { 161 | // For versions prior to Android 3.0, appends the string argument to the content 162 | // Uri for the Contacts table. 163 | final Uri contactUri=Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI,photoData); 164 | // Appends the content Uri for the Contacts.Photo table to the previously 165 | // constructed contact Uri to yield a content URI for the thumbnail image 166 | thumbUri=Uri.withAppendedPath(contactUri,ContactsContract.Contacts.Photo.CONTENT_DIRECTORY); 167 | } 168 | // Retrieves a file descriptor from the Contacts Provider. To learn more about this 169 | // feature, read the reference documentation for 170 | // ContentResolver#openAssetFileDescriptor. 171 | afd=context.getContentResolver().openAssetFileDescriptor(thumbUri,"r"); 172 | 173 | // Gets a FileDescriptor from the AssetFileDescriptor. A BitmapFactory object can 174 | // decode the contents of a file pointed to by a FileDescriptor into a Bitmap. 175 | FileDescriptor fileDescriptor=afd.getFileDescriptor(); 176 | 177 | if(fileDescriptor!=null) 178 | { 179 | // Decodes a Bitmap from the image pointed to by the FileDescriptor, and scales it 180 | // to the specified width and height 181 | return decodeSampledBitmapFromDescriptor( 182 | fileDescriptor,imageSize,imageSize); 183 | } 184 | } 185 | catch(FileNotFoundException e) 186 | { 187 | // If the file pointed to by the thumbnail URI doesn't exist, or the file can't be 188 | // opened in "read" mode, ContentResolver.openAssetFileDescriptor throws a 189 | // FileNotFoundException. 190 | // if(BuildConfig.DEBUG) 191 | // { 192 | // Log.d(TAG,"Contact photo thumbnail not found for contact "+photoData 193 | // +": "+e.toString()); 194 | // } 195 | } 196 | finally 197 | { 198 | // If an AssetFileDescriptor was returned, try to close it 199 | if(afd!=null) 200 | { 201 | try 202 | { 203 | afd.close(); 204 | } 205 | catch(IOException e) 206 | { 207 | // Closing a file descriptor might cause an IOException if the file is 208 | // already closed. Nothing extra is needed to handle this. 209 | } 210 | } 211 | } 212 | 213 | // If the decoding failed, returns null 214 | return null; 215 | } 216 | 217 | 218 | public static Bitmap decodeSampledBitmapFromDescriptor( 219 | FileDescriptor fileDescriptor,int reqWidth,int reqHeight) 220 | { 221 | 222 | // First decode with inJustDecodeBounds=true to check dimensions 223 | final BitmapFactory.Options options=new BitmapFactory.Options(); 224 | options.inJustDecodeBounds=true; 225 | BitmapFactory.decodeFileDescriptor(fileDescriptor,null,options); 226 | 227 | // Calculate inSampleSize 228 | options.inSampleSize=calculateInSampleSize(options,reqWidth,reqHeight); 229 | 230 | // Decode bitmap with inSampleSize set 231 | options.inJustDecodeBounds=false; 232 | return BitmapFactory.decodeFileDescriptor(fileDescriptor,null,options); 233 | } 234 | 235 | /** 236 | * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding 237 | * bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates 238 | * the closest inSampleSize that will result in the final decoded bitmap having a width and 239 | * height equal to or larger than the requested width and height. This implementation does not 240 | * ensure a power of 2 is returned for inSampleSize which can be faster when decoding but 241 | * results in a larger bitmap which isn't as useful for caching purposes. 242 | * 243 | * @param options An options object with out* params already populated (run through a decode* 244 | * method with inJustDecodeBounds==true 245 | * @param reqWidth The requested width of the resulting bitmap 246 | * @param reqHeight The requested height of the resulting bitmap 247 | * @return The value to be used for inSampleSize 248 | */ 249 | public static int calculateInSampleSize(BitmapFactory.Options options, 250 | int reqWidth,int reqHeight) 251 | { 252 | // Raw height and width of image 253 | final int height=options.outHeight; 254 | final int width=options.outWidth; 255 | int inSampleSize=1; 256 | 257 | if(height>reqHeight||width>reqWidth) 258 | { 259 | 260 | // Calculate ratios of height and width to requested height and width 261 | final int heightRatio=Math.round((float)height/(float)reqHeight); 262 | final int widthRatio=Math.round((float)width/(float)reqWidth); 263 | 264 | // Choose the smallest ratio as inSampleSize value, this will guarantee a final image 265 | // with both dimensions larger than or equal to the requested height and width. 266 | inSampleSize=heightRatiototalReqPixelsCap) 280 | { 281 | inSampleSize++; 282 | } 283 | } 284 | return inSampleSize; 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /app/src/main/java/lb/listviewvariants/utils/ContactsQuery.java: -------------------------------------------------------------------------------- 1 | package lb.listviewvariants.utils; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.net.Uri; 5 | import android.os.Build; 6 | import android.provider.ContactsContract.Contacts; 7 | 8 | public interface ContactsQuery 9 | { 10 | 11 | // An identifier for the loader 12 | final static int QUERY_ID=1; 13 | 14 | // A content URI for the Contacts table 15 | final static Uri CONTENT_URI=Contacts.CONTENT_URI; 16 | 17 | // The search/filter query Uri 18 | final static Uri FILTER_URI=Contacts.CONTENT_FILTER_URI; 19 | 20 | // The selection clause for the CursorLoader query. The search criteria defined here 21 | // restrict results to contacts that have a display name and are linked to visible groups. 22 | // Notice that the search on the string provided by the user is implemented by appending 23 | // the search string to CONTENT_FILTER_URI. 24 | @SuppressLint("InlinedApi") 25 | final static String SELECTION= 26 | (Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB?Contacts.DISPLAY_NAME_PRIMARY:Contacts.DISPLAY_NAME)+ 27 | "<>''"+" AND "+Contacts.IN_VISIBLE_GROUP+"=1"; 28 | 29 | // The desired sort order for the returned Cursor. In Android 3.0 and later, the primary 30 | // sort key allows for localization. In earlier versions. use the display name as the sort 31 | // key. 32 | @SuppressLint("InlinedApi") 33 | final static String SORT_ORDER= 34 | Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB?Contacts.SORT_KEY_PRIMARY:Contacts.DISPLAY_NAME; 35 | 36 | // The projection for the CursorLoader query. This is a list of columns that the Contacts 37 | // Provider should return in the Cursor. 38 | @SuppressLint("InlinedApi") 39 | final static String[] PROJECTION={ 40 | 41 | // The contact's row id 42 | Contacts._ID, 43 | 44 | // A pointer to the contact that is guaranteed to be more permanent than _ID. Given 45 | // a contact's current _ID value and LOOKUP_KEY, the Contacts Provider can generate 46 | // a "permanent" contact URI. 47 | Contacts.LOOKUP_KEY, 48 | 49 | // In platform version 3.0 and later, the Contacts table contains 50 | // DISPLAY_NAME_PRIMARY, which either contains the contact's displayable name or 51 | // some other useful identifier such as an email address. This column isn't 52 | // available in earlier versions of Android, so you must use Contacts.DISPLAY_NAME 53 | // instead. 54 | Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB?Contacts.DISPLAY_NAME_PRIMARY:Contacts.DISPLAY_NAME, 55 | 56 | // In Android 3.0 and later, the thumbnail image is pointed to by 57 | // PHOTO_THUMBNAIL_URI. In earlier versions, there is no direct pointer; instead, 58 | // you generate the pointer from the contact's ID value and constants defined in 59 | // android.provider.ContactsContract.Contacts. 60 | Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB?Contacts.PHOTO_THUMBNAIL_URI:Contacts._ID, 61 | 62 | // The sort order column for the returned Cursor, used by the AlphabetIndexer 63 | SORT_ORDER, 64 | }; 65 | 66 | // The query column numbers which map to each value in the projection 67 | final static int ID=0; 68 | final static int LOOKUP_KEY=1; 69 | final static int DISPLAY_NAME=2; 70 | final static int PHOTO_THUMBNAIL_DATA=3; 71 | final static int SORT_KEY=4; 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/lb/listviewvariants/utils/ImageCache.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 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 | 17 | package lb.listviewvariants.utils; 18 | 19 | import android.graphics.Bitmap; 20 | import android.os.Build; 21 | import android.support.v4.util.LruCache; 22 | import android.util.Log; 23 | 24 | import lb.listviewvariants.BuildConfig; 25 | 26 | /** 27 | * This class holds our bitmap caches (memory and disk). 28 | */ 29 | public class ImageCache 30 | { 31 | private static final float CACHE_PERCENTAGE=0.1f; 32 | private static final String TAG="ImageCache"; 33 | private LruCache mMemoryCache; 34 | public static final ImageCache INSTANCE=new ImageCache(CACHE_PERCENTAGE); 35 | 36 | private ImageCache(float memCacheSizePercent) 37 | { 38 | init(memCacheSizePercent); 39 | } 40 | 41 | private void init(float memCacheSizePercent) 42 | { 43 | int memCacheSize=calculateMemCacheSize(memCacheSizePercent); 44 | // Set up memory cache 45 | if(BuildConfig.DEBUG) 46 | Log.d(TAG,"Memory cache created (size = "+memCacheSize+")"); 47 | mMemoryCache=new LruCache(memCacheSize) 48 | { 49 | @Override 50 | protected int sizeOf(String key,Bitmap bitmap) 51 | { 52 | final int bitmapSize=getBitmapSize(bitmap)/1024; 53 | return bitmapSize==0?1:bitmapSize; 54 | } 55 | }; 56 | } 57 | 58 | public void addBitmapToCache(String data,Bitmap bitmap) 59 | { 60 | if(data==null||bitmap==null) 61 | return; 62 | // Add to memory cache 63 | if(mMemoryCache!=null&&mMemoryCache.get(data)==null) 64 | mMemoryCache.put(data,bitmap); 65 | } 66 | 67 | public Bitmap getBitmapFromMemCache(String data) 68 | { 69 | if(mMemoryCache!=null) 70 | { 71 | final Bitmap memBitmap=mMemoryCache.get(data); 72 | if(memBitmap!=null) 73 | { 74 | if(BuildConfig.DEBUG) 75 | Log.d(TAG,"Memory cache hit"); 76 | return memBitmap; 77 | } 78 | } 79 | return null; 80 | } 81 | 82 | public static int getBitmapSize(Bitmap bitmap) 83 | { 84 | if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB_MR1) 85 | return bitmap.getByteCount(); 86 | // Pre HC-MR1 87 | return bitmap.getRowBytes()*bitmap.getHeight(); 88 | } 89 | 90 | public static int calculateMemCacheSize(float percent) 91 | { 92 | if(percent<0.05f||percent>0.8f) 93 | { 94 | throw new IllegalArgumentException("setMemCacheSizePercent - percent must be " 95 | +"between 0.05 and 0.8 (inclusive)"); 96 | } 97 | return Math.round(percent*Runtime.getRuntime().maxMemory()/1024); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/lb/listviewvariants/utils/async_task_thread_pool/AsyncTaskEx.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2008 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 lb.listviewvariants.utils.async_task_thread_pool; 17 | import java.io.File; 18 | import java.io.FileFilter; 19 | import java.util.HashSet; 20 | import java.util.Set; 21 | import java.util.concurrent.BlockingQueue; 22 | import java.util.concurrent.Callable; 23 | import java.util.concurrent.CancellationException; 24 | import java.util.concurrent.ExecutionException; 25 | import java.util.concurrent.Executor; 26 | import java.util.concurrent.FutureTask; 27 | import java.util.concurrent.LinkedBlockingQueue; 28 | import java.util.concurrent.ThreadFactory; 29 | import java.util.concurrent.ThreadPoolExecutor; 30 | import java.util.concurrent.TimeUnit; 31 | import java.util.concurrent.TimeoutException; 32 | import java.util.concurrent.atomic.AtomicBoolean; 33 | import java.util.concurrent.atomic.AtomicInteger; 34 | import java.util.regex.Pattern; 35 | 36 | import android.os.Handler; 37 | import android.os.Message; 38 | 39 | /** 40 | *

41 | * CustomAsyncTask enables proper and easy use of the UI thread. This class allows to perform background operations and 42 | * publish results on the UI thread without having to manipulate threads and/or handlers. 43 | *

44 | *

45 | * CustomAsyncTask is designed to be a helper class around {@link Thread} and {@link Handler} and does not constitute a 46 | * generic threading framework. CustomAsyncTasks should ideally be used for short operations (a few seconds at the 47 | * most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various 48 | * APIs provided by the java.util.concurrent pacakge such as {@link Executor}, {@link ThreadPoolExecutor} 49 | * and {@link FutureTask}. 50 | *

51 | *

52 | * An asynchronous task is defined by a computation that runs on a background thread and whose result is published on 53 | * the UI thread. An asynchronous task is defined by 3 generic types, called Params, Progress 54 | * and Result, and 4 steps, called onPreExecute, doInBackground, 55 | * onProgressUpdate and onPostExecute. 56 | *

57 | *

Developer Guides

58 | *

59 | * For more information about using tasks and threads, read the Processes and Threads developer guide. 61 | *

62 | *

Usage

63 | *

64 | * CustomAsyncTask must be subclassed to be used. The subclass will override at least one method ( 65 | * {@link #doInBackground}), and most often will override a second one ({@link #onPostExecute}.) 66 | *

67 | *

68 | * Here is an example of subclassing: 69 | *

70 | * 71 | *
 72 |  * private class DownloadFilesTask extends CustomAsyncTask<URL, Integer, Long> {
 73 |  * 	protected Long doInBackground(URL... urls) {
 74 |  * 		int count = urls.length;
 75 |  * 		long totalSize = 0;
 76 |  * 		for (int i = 0; i < count; i++) {
 77 |  * 			totalSize += Downloader.downloadFile(urls[i]);
 78 |  * 			publishProgress((int) ((i / (float) count) * 100));
 79 |  * 			// Escape early if cancel() is called
 80 |  * 			if (isCancelled())
 81 |  * 				break;
 82 |  * 		}
 83 |  * 		return totalSize;
 84 |  * 	}
 85 |  *
 86 |  * 	protected void onProgressUpdate(Integer... progress) {
 87 |  * 		setProgressPercent(progress[0]);
 88 |  * 	}
 89 |  *
 90 |  * 	protected void onPostExecute(Long result) {
 91 |  * 		showDialog("Downloaded " + result + " bytes");
 92 |  * 	}
 93 |  * }
 94 |  * 
95 | *

96 | * Once created, a task is executed very simply: 97 | *

98 | * 99 | *
100 |  * new DownloadFilesTask().execute(url1, url2, url3);
101 |  * 
102 | * 103 | *

CustomAsyncTask's generic types

104 | *

105 | * The three types used by an asynchronous task are the following: 106 | *

107 | *
    108 | *
  1. Params, the type of the parameters sent to the task upon execution.
  2. 109 | *
  3. Progress, the type of the progress units published during the background computation.
  4. 110 | *
  5. Result, the type of the result of the background computation.
  6. 111 | *
112 | *

113 | * Not all types are always used by an asynchronous task. To mark a type as unused, simply use the type {@link Void}: 114 | *

115 | * 116 | *
117 |  * private class MyTask extends CustomAsyncTask<Void, Void, Void> { ... }
118 |  * 
119 | * 120 | *

The 4 steps

121 | *

122 | * When an asynchronous task is executed, the task goes through 4 steps: 123 | *

124 | *
    125 | *
  1. {@link #onPreExecute()}, invoked on the UI thread immediately after the task is executed. This step is normally 126 | * used to setup the task, for instance by showing a progress bar in the user interface.
  2. 127 | *
  3. {@link #doInBackground}, invoked on the background thread immediately after {@link #onPreExecute()} finishes 128 | * executing. This step is used to perform background computation that can take a long time. The parameters of the 129 | * asynchronous task are passed to this step. The result of the computation must be returned by this step and will be 130 | * passed back to the last step. This step can also use {@link #publishProgress} to publish one or more units of 131 | * progress. These values are published on the UI thread, in the {@link #onProgressUpdate} step.
  4. 132 | *
  5. {@link #onProgressUpdate}, invoked on the UI thread after a call to {@link #publishProgress}. The timing of the 133 | * execution is undefined. This method is used to display any form of progress in the user interface while the 134 | * background computation is still executing. For instance, it can be used to animate a progress bar or show logs in a 135 | * text field.
  6. 136 | *
  7. {@link #onPostExecute}, invoked on the UI thread after the background computation finishes. The result of the 137 | * background computation is passed to this step as a parameter.
  8. 138 | *
139 | *

Cancelling a task

140 | *

141 | * A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking this method will cause subsequent 142 | * calls to {@link #isCancelled()} to return true. After invoking this method, {@link #onCancelled(Object)}, instead of 143 | * {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])} returns. To ensure that a task 144 | * is cancelled as quickly as possible, you should always check the return value of {@link #isCancelled()} periodically 145 | * from {@link #doInBackground(Object[])}, if possible (inside a loop for instance.) 146 | *

147 | *

Threading rules

148 | *

149 | * There are a few threading rules that must be followed for this class to work properly: 150 | *

151 | *
    152 | *
  • The CustomAsyncTask class must be loaded on the UI thread. This is done automatically as of 153 | * {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.
  • 154 | *
  • The task instance must be created on the UI thread.
  • 155 | *
  • {@link #execute} must be invoked on the UI thread.
  • 156 | *
  • Do not call {@link #onPreExecute()}, {@link #onPostExecute}, {@link #doInBackground}, {@link #onProgressUpdate} 157 | * manually.
  • 158 | *
  • The task can be executed only once (an exception will be thrown if a second execution is attempted.)
  • 159 | *
160 | *

Memory observability

161 | *

162 | * CustomAsyncTask guarantees that all callback calls are synchronized in such a way that the following operations are 163 | * safe without explicit synchronizations. 164 | *

165 | *
    166 | *
  • Set member fields in the constructor or {@link #onPreExecute}, and refer to them in {@link #doInBackground}. 167 | *
  • Set member fields in {@link #doInBackground}, and refer to them in {@link #onProgressUpdate} and 168 | * {@link #onPostExecute}. 169 | *
170 | *

Order of execution

171 | *

172 | * When first introduced, CustomAsyncTasks were executed serially on a single background thread. Starting with 173 | * {@link android.os.Build.VERSION_CODES#DONUT}, this was changed to a pool of threads allowing multiple tasks to 174 | * operate in parallel. Starting with {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single 175 | * thread to avoid common application errors caused by parallel execution. 176 | *

177 | *

178 | * If you truly want parallel execution, you can invoke 179 | * {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with {@link #THREAD_POOL_EXECUTOR}. 180 | *

181 | */ 182 | /** 183 | * same as {@link android.os.AsyncTask} but doesn't have a limit on the number of tasks,plus it uses threads as many as the
184 | * number of cores , minus 1 (or a single thread if it's a single core device) 185 | */ 186 | public abstract class AsyncTaskEx 187 | { 188 | public interface IOnFinishedListener 189 | { 190 | void onFinished(); 191 | } 192 | 193 | private static final String LOG_TAG ="CustomAsyncTask"; 194 | private static final int CORE_POOL_SIZE =2; 195 | private static final int MAXIMUM_POOL_SIZE =Math.max(CORE_POOL_SIZE,getCoresCount()-1); 196 | private static final int KEEP_ALIVE =1; 197 | private static final ThreadFactory sThreadFactory =new ThreadFactory() 198 | { 199 | private final AtomicInteger mCount =new AtomicInteger(1); 200 | 201 | @Override 202 | public Thread newThread(final Runnable r) 203 | { 204 | return new Thread(r,"CustomAsyncTask #"+mCount.getAndIncrement()); 205 | } 206 | }; 207 | private static final BlockingQueue sPoolWorkQueue =new LinkedBlockingQueue<>(); 208 | /** 209 | * An {@link java.util.concurrent.Executor} that can be used to execute tasks in parallel. 210 | */ 211 | public static final Executor THREAD_POOL_EXECUTOR =new ThreadPoolExecutor(CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,KEEP_ALIVE,TimeUnit.SECONDS,sPoolWorkQueue,sThreadFactory); 212 | /** 213 | * An {@link java.util.concurrent.Executor} that executes tasks one at a time in serial order. This serialization is global to a 214 | * particular process. 215 | */ 216 | // public static final Executor SERIAL_EXECUTOR =new SerialExecutor(); 217 | private static final int MESSAGE_POST_RESULT =0x1; 218 | private static final int MESSAGE_POST_PROGRESS =0x2; 219 | private static final InternalHandler sHandler =new InternalHandler(); 220 | private static volatile Executor sDefaultExecutor =THREAD_POOL_EXECUTOR; 221 | private final WorkerRunnable mWorker; 222 | private final FutureTask mFuture; 223 | private volatile Status mStatus =Status.PENDING; 224 | private final AtomicBoolean mCancelled =new AtomicBoolean(); 225 | private final AtomicBoolean mTaskInvoked =new AtomicBoolean(); 226 | private final Set mOnFinishedListeners =new HashSet<>(); 227 | 228 | /** 229 | * return the number of cores of the device.
230 | * based on : http://stackoverflow.com/a/10377934/878126 231 | */ 232 | private static int getCoresCount() 233 | { 234 | class CpuFilter implements FileFilter 235 | { 236 | @Override 237 | public boolean accept(final File pathname) 238 | { 239 | return Pattern.matches("cpu[0-9]+",pathname.getName()); 240 | } 241 | } 242 | try 243 | { 244 | final File dir=new File("/sys/devices/system/cpu/"); 245 | final File[] files=dir.listFiles(new CpuFilter()); 246 | return files.length; 247 | } 248 | catch(final Exception e) 249 | { 250 | return Math.max(1,Runtime.getRuntime().availableProcessors()); 251 | } 252 | } 253 | 254 | /** 255 | * Indicates the current status of the task. Each status will be set only once during the lifetime of a task. 256 | */ 257 | public enum Status 258 | { 259 | /** 260 | * Indicates that the task has not been executed yet. 261 | */ 262 | PENDING, 263 | /** 264 | * Indicates that the task is running. 265 | */ 266 | RUNNING, 267 | /** 268 | * Indicates that {@link CustomAsyncTask#onPostExecute} has finished. 269 | */ 270 | FINISHED, 271 | } 272 | 273 | // /** @hide Used to force static handler to be created. */ 274 | // public static void init() { 275 | // sHandler.getLooper(); 276 | // } 277 | // /** @hide */ 278 | // public static void setDefaultExecutor(final Executor exec) { 279 | // sDefaultExecutor = exec; 280 | // } 281 | /** 282 | * Creates a new asynchronous task. This constructor must be invoked on the UI thread. 283 | */ 284 | public AsyncTaskEx() 285 | { 286 | mWorker=new WorkerRunnable() 287 | { 288 | @Override 289 | public Result call() throws Exception 290 | { 291 | mTaskInvoked.set(true); 292 | android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); 293 | // noinspection unchecked 294 | return postResult(doInBackground(mParams)); 295 | } 296 | }; 297 | mFuture=new FutureTask(mWorker) 298 | { 299 | @Override 300 | protected void done() 301 | { 302 | try 303 | { 304 | postResultIfNotInvoked(get()); 305 | } 306 | catch(final InterruptedException e) 307 | { 308 | android.util.Log.w(LOG_TAG,e); 309 | } 310 | catch(final ExecutionException e) 311 | { 312 | throw new RuntimeException("An error occured while executing doInBackground()",e.getCause()); 313 | } 314 | catch(final CancellationException e) 315 | { 316 | postResultIfNotInvoked(null); 317 | } 318 | } 319 | }; 320 | } 321 | 322 | private void postResultIfNotInvoked(final Result result) 323 | { 324 | final boolean wasTaskInvoked=mTaskInvoked.get(); 325 | if(!wasTaskInvoked) 326 | postResult(result); 327 | } 328 | 329 | private Result postResult(final Result result) 330 | { 331 | @SuppressWarnings("unchecked") 332 | final Message message=sHandler.obtainMessage(MESSAGE_POST_RESULT,new AsyncTaskExResult<>(this,result)); 333 | message.sendToTarget(); 334 | return result; 335 | } 336 | 337 | /** 338 | * Returns the current status of this task. 339 | * 340 | * @return The current status. 341 | */ 342 | public final Status getStatus() 343 | { 344 | return mStatus; 345 | } 346 | 347 | /** 348 | * Override this method to perform a computation on a background thread. The specified parameters are the parameters 349 | * passed to {@link #execute} by the caller of this task. This method can call {@link #publishProgress} to publish 350 | * updates on the UI thread. 351 | * 352 | * @param params 353 | * The parameters of the task. 354 | * @return A result, defined by the subclass of this task. 355 | * @see #onPreExecute() 356 | * @see #onPostExecute 357 | * @see #publishProgress 358 | */ 359 | public abstract Result doInBackground(@SuppressWarnings("unchecked") Params... params); 360 | 361 | /** 362 | * Runs on the UI thread before {@link #doInBackground}. 363 | * 364 | * @see #onPostExecute 365 | * @see #doInBackground 366 | */ 367 | protected void onPreExecute() 368 | {} 369 | 370 | /** 371 | *

372 | * Runs on the UI thread after {@link #doInBackground}. The specified result is the value returned by {@link #doInBackground}. 373 | *

374 | *

375 | * This method won't be invoked if the task was cancelled. 376 | *

377 | * 378 | * @param result 379 | * The result of the operation computed by {@link #doInBackground}. 380 | * @see #onPreExecute 381 | * @see #doInBackground 382 | * @see #onCancelled(Object) 383 | */ 384 | public void onPostExecute(final Result result) 385 | {} 386 | 387 | /** 388 | * Runs on the UI thread after {@link #publishProgress} is invoked. The specified values are the values passed to {@link #publishProgress}. 389 | * 390 | * @param values 391 | * The values indicating progress. 392 | * @see #publishProgress 393 | * @see #doInBackground 394 | */ 395 | protected void onProgressUpdate(@SuppressWarnings("unchecked") final Progress... values) 396 | {} 397 | 398 | /** 399 | *

400 | * Runs on the UI thread after {@link #cancel(boolean)} is invoked and {@link #doInBackground(Object[])} has finished. 401 | *

402 | *

403 | * The default implementation simply invokes {@link #onCancelled()} and ignores the result. If you write your own implementation, do not call super.onCancelled(result). 404 | *

405 | * 406 | * @param result 407 | * The result, if any, computed in {@link #doInBackground(Object[])}, can be null 408 | * @see #cancel(boolean) 409 | * @see #isCancelled() 410 | */ 411 | protected void onCancelled(final Result result) 412 | { 413 | onCancelled(); 414 | } 415 | 416 | /** 417 | *

418 | * Applications should preferably override {@link #onCancelled(Object)}. This method is invoked by the default implementation of {@link #onCancelled(Object)}. 419 | *

420 | *

421 | * Runs on the UI thread after {@link #cancel(boolean)} is invoked and {@link #doInBackground(Object[])} has finished. 422 | *

423 | * 424 | * @see #onCancelled(Object) 425 | * @see #cancel(boolean) 426 | * @see #isCancelled() 427 | */ 428 | protected void onCancelled() 429 | {} 430 | 431 | /** 432 | * Returns true if this task was cancelled before it completed normally. If you are calling {@link #cancel(boolean)} on the task, the value returned by this method should be checked periodically from {@link #doInBackground(Object[])} to end the task as soon as possible. 433 | * 434 | * @return true if task was cancelled before it completed 435 | * @see #cancel(boolean) 436 | */ 437 | public final boolean isCancelled() 438 | { 439 | return mCancelled.get(); 440 | } 441 | 442 | /** 443 | *

444 | * Attempts to cancel execution of this task. This attempt will fail if the task has already completed, already been cancelled, or could not be cancelled for some other reason. If successful, and this task has not started when cancel is called, this task should never run. If the task has already started, then the mayInterruptIfRunning parameter determines whether the thread executing this task should be interrupted in an attempt to stop the task. 445 | *

446 | *

447 | * Calling this method will result in {@link #onCancelled(Object)} being invoked on the UI thread after {@link #doInBackground(Object[])} returns. Calling this method guarantees that {@link #onPostExecute(Object)} is never invoked. After invoking this method, you should check the value returned by {@link #isCancelled()} periodically from {@link #doInBackground(Object[])} to finish the task as early as possible. 448 | *

449 | * 450 | * @param mayInterruptIfRunning 451 | * true if the thread executing this task should be interrupted; otherwise, in-progress tasks 452 | * are allowed to complete. 453 | * @return false if the task could not be cancelled, typically because it has already completed normally; true otherwise 454 | * @see #isCancelled() 455 | * @see #onCancelled(Object) 456 | */ 457 | public final boolean cancel(final boolean mayInterruptIfRunning) 458 | { 459 | mCancelled.set(true); 460 | return mFuture.cancel(mayInterruptIfRunning); 461 | } 462 | 463 | /** 464 | * Waits if necessary for the computation to complete, and then retrieves its result. 465 | * 466 | * @return The computed result. 467 | * @throws java.util.concurrent.CancellationException 468 | * If the computation was cancelled. 469 | * @throws java.util.concurrent.ExecutionException 470 | * If the computation threw an exception. 471 | * @throws InterruptedException 472 | * If the current thread was interrupted while waiting. 473 | */ 474 | public final Result get() throws InterruptedException,ExecutionException 475 | { 476 | return mFuture.get(); 477 | } 478 | 479 | /** 480 | * Waits if necessary for at most the given time for the computation to complete, and then retrieves its result. 481 | * 482 | * @param timeout 483 | * Time to wait before cancelling the operation. 484 | * @param unit 485 | * The time unit for the timeout. 486 | * @return The computed result. 487 | * @throws java.util.concurrent.CancellationException 488 | * If the computation was cancelled. 489 | * @throws java.util.concurrent.ExecutionException 490 | * If the computation threw an exception. 491 | * @throws InterruptedException 492 | * If the current thread was interrupted while waiting. 493 | * @throws java.util.concurrent.TimeoutException 494 | * If the wait timed out. 495 | */ 496 | public final Result get(final long timeout,final TimeUnit unit) throws InterruptedException,ExecutionException,TimeoutException 497 | { 498 | return mFuture.get(timeout,unit); 499 | } 500 | 501 | /** 502 | * Executes the task with the specified parameters. The task returns itself (this) so that the caller can keep a 503 | * reference to it. 504 | *

505 | * Note: this function schedules the task on a queue for a single background thread or pool of threads depending on the platform version. When first introduced, CustomAsyncTasks were executed serially on a single background thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed to a pool of threads allowing multiple tasks to operate in parallel. Starting {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being executed on a single thread to avoid common application errors caused by parallel execution. If you truly want parallel execution, you can use the {@link #executeOnExecutor} version of this method with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings on its use. 506 | *

507 | * This method must be invoked on the UI thread. 508 | * 509 | * @param params 510 | * The parameters of the task. 511 | * @return This instance of CustomAsyncTask. 512 | * @throws IllegalStateException 513 | * If {@link #getStatus()} returns either {@link lb.listviewvariants.utils.async_task_thread_pool.AsyncTaskEx.Status#RUNNING} or {@link lb.listviewvariants.utils.async_task_thread_pool.AsyncTaskEx.Status#FINISHED}. 514 | * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) 515 | * @see #execute(Runnable) 516 | */ 517 | public final AsyncTaskEx execute(@SuppressWarnings("unchecked") final Params... params) 518 | { 519 | return executeOnExecutor(sDefaultExecutor,params); 520 | } 521 | 522 | /** 523 | * Executes the task with the specified parameters. The task returns itself (this) so that the caller can keep a 524 | * reference to it. 525 | *

526 | * This method is typically used with {@link #THREAD_POOL_EXECUTOR} to allow multiple tasks to run in parallel on a pool of threads managed by CustomAsyncTask, however you can also use your own {@link java.util.concurrent.Executor} for custom behavior. 527 | *

528 | * Warning: Allowing multiple tasks to run in parallel from a thread pool is generally not what one wants, because the order of their operation is not defined. For example, if these tasks are used to modify any state in common (such as writing a file due to a button click), there are no guarantees on the order of the modifications. Without careful work it is possible in rare cases for the newer version of the data to be over-written by an older one, leading to obscure data loss and stability issues. Such changes are best executed in serial; to guarantee such work is serialized regardless of platform version you can use this function with {@link #SERIAL_EXECUTOR}. 529 | *

530 | * This method must be invoked on the UI thread. 531 | * 532 | * @param exec 533 | * The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a convenient process-wide thread 534 | * pool for tasks that are loosely coupled. 535 | * @param params 536 | * The parameters of the task. 537 | * @return This instance of CustomAsyncTask. 538 | * @throws IllegalStateException 539 | * If {@link #getStatus()} returns either {@link lb.listviewvariants.utils.async_task_thread_pool.AsyncTaskEx.Status#RUNNING} or {@link lb.listviewvariants.utils.async_task_thread_pool.AsyncTaskEx.Status#FINISHED}. 540 | * @see #execute(Object[]) 541 | */ 542 | public final AsyncTaskEx executeOnExecutor(final Executor exec,@SuppressWarnings("unchecked") final Params... params) 543 | { 544 | if(mStatus!=Status.PENDING) 545 | switch(mStatus) 546 | { 547 | case RUNNING: 548 | throw new IllegalStateException("Cannot execute task:"+" the task is already running."); 549 | case FINISHED: 550 | throw new IllegalStateException("Cannot execute task:"+" the task has already been executed "+"(a task can be executed only once)"); 551 | case PENDING: 552 | break; 553 | default: 554 | break; 555 | } 556 | mStatus=Status.RUNNING; 557 | onPreExecute(); 558 | mWorker.mParams=params; 559 | exec.execute(mFuture); 560 | return this; 561 | } 562 | 563 | /** 564 | * Convenience version of {@link #execute(Object...)} for use with a simple Runnable object. See {@link #execute(Object[])} for more information on the order of execution. 565 | * 566 | * @see #execute(Object[]) 567 | * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) 568 | */ 569 | public static void execute(final Runnable runnable) 570 | { 571 | sDefaultExecutor.execute(runnable); 572 | } 573 | 574 | /** 575 | * This method can be invoked from {@link #doInBackground} to publish updates on the UI thread while the background 576 | * computation is still running. Each call to this method will trigger the execution of {@link #onProgressUpdate} on 577 | * the UI thread. {@link #onProgressUpdate} will note be called if the task has been canceled. 578 | * 579 | * @param values 580 | * The progress values to update the UI with. 581 | * @see #onProgressUpdate 582 | * @see #doInBackground 583 | */ 584 | public final void publishProgress(@SuppressWarnings("unchecked") final Progress... values) 585 | { 586 | if(!isCancelled()) 587 | sHandler.obtainMessage(MESSAGE_POST_PROGRESS,new AsyncTaskExResult<>(this,values)).sendToTarget(); 588 | } 589 | 590 | private void finish(final Result result) 591 | { 592 | if(isCancelled()) 593 | onCancelled(result); 594 | else onPostExecute(result); 595 | for(final IOnFinishedListener listener : mOnFinishedListeners) 596 | listener.onFinished(); 597 | mStatus=Status.FINISHED; 598 | } 599 | 600 | public void addOnFinishedListener(final IOnFinishedListener onFinishedListener) 601 | { 602 | this.mOnFinishedListeners.add(onFinishedListener); 603 | } 604 | 605 | public void removeOnFinishedListener(final IOnFinishedListener onFinishedListener) 606 | { 607 | this.mOnFinishedListeners.remove(onFinishedListener); 608 | } 609 | 610 | private static class InternalHandler extends Handler 611 | { 612 | @SuppressWarnings({"unchecked","rawtypes"}) 613 | @Override 614 | public void handleMessage(final Message msg) 615 | { 616 | final AsyncTaskExResult result=(AsyncTaskExResult)msg.obj; 617 | switch(msg.what) 618 | { 619 | case MESSAGE_POST_RESULT: 620 | // There is only one result 621 | result.mTask.finish(result.mData[0]); 622 | break; 623 | case MESSAGE_POST_PROGRESS: 624 | result.mTask.onProgressUpdate(result.mData); 625 | break; 626 | } 627 | } 628 | } 629 | 630 | private static abstract class WorkerRunnable implements Callable 631 | { 632 | Params[] mParams; 633 | } 634 | 635 | @SuppressWarnings({}) 636 | private static class AsyncTaskExResult 637 | { 638 | final AsyncTaskEx mTask; 639 | final Data[] mData; 640 | 641 | AsyncTaskExResult(final AsyncTaskEx task,@SuppressWarnings("unchecked") final Data... data) 642 | { 643 | mTask=task; 644 | mData=data; 645 | } 646 | } 647 | } 648 | -------------------------------------------------------------------------------- /app/src/main/java/lb/listviewvariants/utils/async_task_thread_pool/AsyncTaskThreadPool.java: -------------------------------------------------------------------------------- 1 | package lb.listviewvariants.utils.async_task_thread_pool; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | import java.util.concurrent.ThreadFactory; 6 | import java.util.concurrent.ThreadPoolExecutor; 7 | import java.util.concurrent.TimeUnit; 8 | import java.util.concurrent.atomic.AtomicInteger; 9 | 10 | /** 11 | * a thread pool that contains tasks to run like on asyncTask.
12 | * also has the ability to use a stack instead of a queue for the order of tasks 13 | */ 14 | public class AsyncTaskThreadPool 15 | { 16 | // classes used for the threadPool were created because of an issue with customized parameters of 17 | // ThreadPoolExecutor: 18 | // https://github.com/kimchy/kimchy.github.com/blob/master/_posts/2008-11-23-juc-executorservice-gotcha.textile 19 | private static final int TIME_TO_KEEP_ALIVE_IN_SECONDS =10; 20 | private static final int CORE_POOL_SIZE =1; 21 | private static final int MAXIMUM_POOL_SIZE =Math.max(CORE_POOL_SIZE,Runtime.getRuntime().availableProcessors()-1); 22 | private final ThreadPoolExecutor mExecutor; 23 | // private final Executor mExecutor = Executors.newSingleThreadExecutor(); 24 | // private final Executor mExecutor = Executors.newCachedThreadPool(); 25 | // private final Executor mExecutor = Executors.newFixedThreadPool(MAXIMUM_POOL_SIZE); 26 | // private final Executor mExecutor = Executors.newScheduledThreadPool(corePoolSize, threadFactory) 27 | private final Set> mTasks =new HashSet<>(); 28 | 29 | public AsyncTaskThreadPool() 30 | { 31 | this(CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,TIME_TO_KEEP_ALIVE_IN_SECONDS); 32 | } 33 | 34 | public AsyncTaskThreadPool(final int minNumberOfThread,final int maxNumberOfThread,final int keepAliveTimeInSeconds) 35 | { 36 | final ScalingQueue poolWorkQueue=new ScalingQueue<>(); 37 | final ThreadFactory threadFactory=new ThreadFactory() 38 | { 39 | private final AtomicInteger mCount =new AtomicInteger(1); 40 | 41 | @Override 42 | public Thread newThread(final Runnable r) 43 | { 44 | final Thread thread=new Thread(r,"thread #"+mCount.getAndIncrement()); 45 | thread.setPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); 46 | return thread; 47 | } 48 | }; 49 | // needed because normal ThreadPoolExecutor always uses a single thread. 50 | mExecutor=new ScalingThreadPoolExecutor(minNumberOfThread,maxNumberOfThread,keepAliveTimeInSeconds,TimeUnit.SECONDS,poolWorkQueue,threadFactory); 51 | mExecutor.setRejectedExecutionHandler(new ForceQueuePolicy()); 52 | poolWorkQueue.setThreadPoolExecutor(mExecutor); 53 | } 54 | 55 | /** runs the task and remembers it till it finishes. */ 56 | public void executeAsyncTask(final AsyncTaskEx task,@SuppressWarnings("unchecked") final Params... params) 57 | { 58 | task.addOnFinishedListener(new AsyncTaskEx.IOnFinishedListener() 59 | { 60 | @Override 61 | public void onFinished() 62 | { 63 | mTasks.remove(task); 64 | task.removeOnFinishedListener(this); 65 | } 66 | }); 67 | mTasks.add(task); 68 | task.executeOnExecutor(mExecutor,params); 69 | } 70 | 71 | public void executeAsyncTask(final AsyncTaskEx task) 72 | { 73 | //noinspection unchecked 74 | executeAsyncTask(task,(Params[])null); 75 | } 76 | 77 | public void cancelAllTasks(final boolean alsoInterrupt) 78 | { 79 | for(final AsyncTaskEx task : mTasks) 80 | task.cancel(alsoInterrupt); 81 | mTasks.clear(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/lb/listviewvariants/utils/async_task_thread_pool/ForceQueuePolicy.java: -------------------------------------------------------------------------------- 1 | package lb.listviewvariants.utils.async_task_thread_pool; 2 | import java.util.concurrent.RejectedExecutionException; 3 | import java.util.concurrent.RejectedExecutionHandler; 4 | import java.util.concurrent.ThreadPoolExecutor; 5 | 6 | class ForceQueuePolicy implements RejectedExecutionHandler 7 | { 8 | @Override 9 | public void rejectedExecution(final Runnable r,final ThreadPoolExecutor executor) 10 | { 11 | try 12 | { 13 | executor.getQueue().put(r); 14 | } 15 | catch(final InterruptedException e) 16 | { 17 | // should never happen since we never wait 18 | throw new RejectedExecutionException(e); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/lb/listviewvariants/utils/async_task_thread_pool/ScalingQueue.java: -------------------------------------------------------------------------------- 1 | package lb.listviewvariants.utils.async_task_thread_pool; 2 | import java.util.concurrent.LinkedBlockingQueue; 3 | import java.util.concurrent.ThreadPoolExecutor; 4 | 5 | public class ScalingQueue extends LinkedBlockingQueue 6 | { 7 | private static final long serialVersionUID =2868771663367097439L; 8 | /** 9 | * The executor this Queue belongs to 10 | */ 11 | private ThreadPoolExecutor executor; 12 | 13 | /** 14 | * Creates a TaskQueue with a capacity of {@link Integer#MAX_VALUE}. 15 | */ 16 | public ScalingQueue() 17 | { 18 | super(); 19 | } 20 | 21 | /** 22 | * Creates a TaskQueue with the given (fixed) capacity. 23 | * 24 | * @param capacity 25 | * the capacity of this queue. 26 | */ 27 | public ScalingQueue(final int capacity) 28 | { 29 | super(capacity); 30 | } 31 | 32 | /** 33 | * Sets the executor this queue belongs to. 34 | */ 35 | public void setThreadPoolExecutor(final ThreadPoolExecutor executor) 36 | { 37 | this.executor=executor; 38 | } 39 | 40 | /** 41 | * Inserts the specified element at the tail of this queue if there is at least one available thread to run the 42 | * current task. If all pool threads are actively busy, it rejects the offer. 43 | * 44 | * @param o 45 | * the element to add. 46 | * @return true if it was possible to add the element to this queue, else false 47 | * @see java.util.concurrent.ThreadPoolExecutor#execute(Runnable) 48 | */ 49 | @Override 50 | public boolean offer(final T o) 51 | { 52 | final int allWorkingThreads=executor.getActiveCount()+super.size(); 53 | return allWorkingThreads 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/listview_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_action_info_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/ListViewVariants/bca271a97bee485172cd094ed927a40ef8ae2644/app/src/main/res/drawable-xhdpi/ic_action_action_info_outline.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/ListViewVariants/bca271a97bee485172cd094ed927a40ef8ae2644/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_person_white_120dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/ListViewVariants/bca271a97bee485172cd094ed927a40ef8ae2644/app/src/main/res/drawable-xhdpi/ic_person_white_120dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_action_info_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/ListViewVariants/bca271a97bee485172cd094ed927a40ef8ae2644/app/src/main/res/drawable-xxhdpi/ic_action_action_info_outline.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/ListViewVariants/bca271a97bee485172cd094ed927a40ef8ae2644/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_action_info_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/ListViewVariants/bca271a97bee485172cd094ed927a40ef8ae2644/app/src/main/res/drawable-xxxhdpi/ic_action_action_info_outline.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/listview_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/listview_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 28 | 29 | 34 | 35 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/pinned_header_listview_side_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/menu/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 |

4 | 10 | 14 | 15 | 18 | 21 | 22 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/values/color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0xFF9C27B0 5 | 0xFFFF5722 6 | 0xFF3F51B5 7 | 0xFFDB4437 8 | 0xFFEF6C00 9 | 0xFF689F38 10 | 0xFF757575 11 | 0xFF0F9D58 12 | 0xFF3F51B5 13 | 0xFF0097A7 14 | 15 | 16 | 17 | 18 | #FFdddddd 19 | #FF7dbcd3 20 | #FFededed 21 | 22 | #FF3c3c3c 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 55dp 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ListViewVariants 5 | Repository website 6 | All my repositories 7 | All my apps 8 | More info 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 17 | 18 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.1.0-alpha5' 9 | // NOTE: Do not place your application dependencies here; they belong 10 | // in the individual module build.gradle files 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | jcenter() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/ListViewVariants/bca271a97bee485172cd094ed927a40ef8ae2644/demo.gif -------------------------------------------------------------------------------- /device-2014-12-28-230610.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/ListViewVariants/bca271a97bee485172cd094ed927a40ef8ae2644/device-2014-12-28-230610.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/ListViewVariants/bca271a97bee485172cd094ed927a40ef8ae2644/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Apr 02 22:32:53 IDT 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "21.1.2" 6 | 7 | defaultConfig { 8 | minSdkVersion 8 9 | targetSdkVersion 22 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | compile 'com.android.support:appcompat-v7:23.2.1' 24 | } 25 | -------------------------------------------------------------------------------- /library/library.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:/android/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /library/src/main/java/lb/library/BasePinnedHeaderListViewAdapter.java: -------------------------------------------------------------------------------- 1 | package lb.library; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | import android.widget.AbsListView; 6 | import android.widget.AbsListView.OnScrollListener; 7 | import android.widget.BaseAdapter; 8 | import android.widget.SectionIndexer; 9 | import android.widget.TextView; 10 | 11 | 12 | public abstract class BasePinnedHeaderListViewAdapter extends BaseAdapter implements SectionIndexer, OnScrollListener, 13 | PinnedHeaderListView.PinnedHeaderAdapter 14 | { 15 | private SectionIndexer _sectionIndexer; 16 | private boolean mHeaderViewVisible = true; 17 | 18 | public void setSectionIndexer(final SectionIndexer sectionIndexer) { 19 | _sectionIndexer = sectionIndexer; 20 | } 21 | 22 | /** remember to call bindSectionHeader(v,position); before calling return */ 23 | @Override 24 | public abstract View getView(final int position, final View convertView, final ViewGroup parent); 25 | 26 | public abstract CharSequence getSectionTitle(int sectionIndex); 27 | 28 | protected void bindSectionHeader(final TextView headerView, final View dividerView, final int position) { 29 | final int sectionIndex = getSectionForPosition(position); 30 | if (getPositionForSection(sectionIndex) == position) { 31 | final CharSequence title = getSectionTitle(sectionIndex); 32 | headerView.setText(title); 33 | headerView.setVisibility(View.VISIBLE); 34 | if (dividerView != null) 35 | dividerView.setVisibility(View.GONE); 36 | } else { 37 | headerView.setVisibility(View.GONE); 38 | if (dividerView != null) 39 | dividerView.setVisibility(View.VISIBLE); 40 | } 41 | // move the divider for the last item in a section 42 | if (dividerView != null) 43 | if (getPositionForSection(sectionIndex + 1) - 1 == position) 44 | dividerView.setVisibility(View.GONE); 45 | else 46 | dividerView.setVisibility(View.VISIBLE); 47 | if (!mHeaderViewVisible) 48 | headerView.setVisibility(View.GONE); 49 | } 50 | 51 | @Override 52 | public int getPinnedHeaderState(final int position) { 53 | if (_sectionIndexer == null || getCount() == 0 || !mHeaderViewVisible) 54 | return PINNED_HEADER_GONE; 55 | if (position < 0) 56 | return PINNED_HEADER_GONE; 57 | // The header should get pushed up if the top item shown 58 | // is the last item in a section for a particular letter. 59 | final int section = getSectionForPosition(position); 60 | final int nextSectionPosition = getPositionForSection(section + 1); 61 | if (nextSectionPosition != -1 && position == nextSectionPosition - 1) 62 | return PINNED_HEADER_PUSHED_UP; 63 | return PINNED_HEADER_VISIBLE; 64 | } 65 | 66 | public void setHeaderViewVisible(final boolean isHeaderViewVisible) { 67 | mHeaderViewVisible = isHeaderViewVisible; 68 | } 69 | 70 | public boolean isHeaderViewVisible() { 71 | return this.mHeaderViewVisible; 72 | } 73 | 74 | @Override 75 | public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount, 76 | final int totalItemCount) { 77 | ((PinnedHeaderListView) view).configureHeaderView(firstVisibleItem); 78 | } 79 | 80 | @Override 81 | public void onScrollStateChanged(final AbsListView arg0, final int arg1) { 82 | } 83 | 84 | @Override 85 | public int getPositionForSection(final int sectionIndex) { 86 | if (_sectionIndexer == null) 87 | return -1; 88 | return _sectionIndexer.getPositionForSection(sectionIndex); 89 | } 90 | 91 | @Override 92 | public int getSectionForPosition(final int position) { 93 | if (_sectionIndexer == null) 94 | return -1; 95 | return _sectionIndexer.getSectionForPosition(position); 96 | } 97 | 98 | @Override 99 | public Object[] getSections() { 100 | if (_sectionIndexer == null) 101 | return new String[] { " " }; 102 | return _sectionIndexer.getSections(); 103 | } 104 | 105 | @Override 106 | public long getItemId(final int position) { 107 | return position; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /library/src/main/java/lb/library/BaseSectionedAdapter.java: -------------------------------------------------------------------------------- 1 | package lb.library; 2 | 3 | import android.widget.BaseAdapter; 4 | import android.widget.SectionIndexer; 5 | 6 | /** 7 | * a base adapter that allows having multiple sections. each section has its own items. items don't have to be of the 8 | * same type 9 | */ 10 | public abstract class BaseSectionedAdapter extends BaseAdapter implements SectionIndexer { 11 | private SectionedSectionIndexer mSectionIndexer; 12 | 13 | public void setSectionIndexer(final SectionedSectionIndexer sectionIndexer) { 14 | mSectionIndexer = sectionIndexer; 15 | } 16 | 17 | public SectionIndexer getSectionIndexer() { 18 | return this.mSectionIndexer; 19 | } 20 | 21 | @Override 22 | public int getPositionForSection(final int sectionIndex) { 23 | if (mSectionIndexer == null) 24 | return -1; 25 | return mSectionIndexer.getPositionForSection(sectionIndex); 26 | } 27 | 28 | @Override 29 | public int getSectionForPosition(final int position) { 30 | if (mSectionIndexer == null) 31 | return -1; 32 | return mSectionIndexer.getSectionForPosition(position); 33 | } 34 | 35 | @Override 36 | public Object[] getSections() { 37 | if (mSectionIndexer == null) 38 | return new String[] { " " }; 39 | return mSectionIndexer.getSections(); 40 | } 41 | 42 | @Override 43 | public long getItemId(final int position) { 44 | return position; 45 | } 46 | 47 | @Override 48 | public int getCount() { 49 | return mSectionIndexer.getItemsCount(); 50 | } 51 | 52 | @Override 53 | public Object getItem(final int position) { 54 | return mSectionIndexer.getItem(position); 55 | } 56 | 57 | @Override 58 | public int getViewTypeCount() { 59 | return mSectionIndexer.getSections().length; 60 | } 61 | 62 | @Override 63 | public int getItemViewType(final int position) { 64 | return mSectionIndexer.getSectionForPosition(position); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /library/src/main/java/lb/library/IndexedPinnedHeaderListViewAdapter.java: -------------------------------------------------------------------------------- 1 | package lb.library; 2 | 3 | import android.graphics.Color; 4 | import android.os.Build.VERSION; 5 | import android.os.Build.VERSION_CODES; 6 | import android.view.View; 7 | import android.widget.TextView; 8 | 9 | public abstract class IndexedPinnedHeaderListViewAdapter extends BasePinnedHeaderListViewAdapter 10 | { 11 | private int _pinnedHeaderBackgroundColor; 12 | private int _pinnedHeaderTextColor; 13 | 14 | public void setPinnedHeaderBackgroundColor(final int pinnedHeaderBackgroundColor) 15 | { 16 | _pinnedHeaderBackgroundColor=pinnedHeaderBackgroundColor; 17 | } 18 | 19 | public void setPinnedHeaderTextColor(final int pinnedHeaderTextColor) 20 | { 21 | _pinnedHeaderTextColor=pinnedHeaderTextColor; 22 | } 23 | 24 | @Override 25 | public CharSequence getSectionTitle(final int sectionIndex) 26 | { 27 | return getSections()[sectionIndex].toString(); 28 | } 29 | 30 | @Override 31 | public void configurePinnedHeader(final View v,final int position,final int alpha) 32 | { 33 | final TextView header=(TextView)v; 34 | final int sectionIndex=getSectionForPosition(position); 35 | final Object[] sections=getSections(); 36 | if(sections!=null&§ions.length!=0) 37 | { 38 | final CharSequence title=getSectionTitle(sectionIndex); 39 | header.setText(title); 40 | } 41 | if(VERSION.SDK_INT extends IndexedPinnedHeaderListViewAdapter implements 14 | Filterable { 15 | private ArrayList mFilterListCopy; 16 | private final Filter mFilter; 17 | 18 | public SearchablePinnedHeaderListViewAdapter() { 19 | mFilter = new Filter() { 20 | CharSequence lastConstraint = null; 21 | 22 | @Override 23 | protected FilterResults performFiltering(final CharSequence constraint) { 24 | if (constraint == null || constraint.length() == 0) 25 | return null; 26 | final ArrayList newFilterArray = new ArrayList(); 27 | final FilterResults results = new FilterResults(); 28 | for (final T item : getOriginalList()) 29 | if (doFilter(item, constraint)) 30 | newFilterArray.add(item); 31 | results.values = newFilterArray; 32 | results.count = newFilterArray.size(); 33 | return results; 34 | } 35 | 36 | @SuppressWarnings("unchecked") 37 | @Override 38 | protected void publishResults(final CharSequence constraint, final FilterResults results) { 39 | mFilterListCopy = results == null ? null : (ArrayList) results.values; 40 | final boolean needRefresh = !TextUtils.equals(constraint,lastConstraint); 41 | lastConstraint = constraint == null ? null : constraint; 42 | if (needRefresh) 43 | notifyDataSetChanged(); 44 | } 45 | }; 46 | } 47 | 48 | @Override 49 | public Filter getFilter() { 50 | return mFilter; 51 | } 52 | 53 | /** returns true iff the item can "pass" the filtering process and should be shown */ 54 | public abstract boolean doFilter(T item, CharSequence constraint); 55 | 56 | public abstract ArrayList getOriginalList(); 57 | 58 | @Override 59 | public T getItem(final int position) { 60 | if (position < 0) 61 | return null; 62 | final ArrayList listCopy = getFilterListCopy(); 63 | if (listCopy != null) { 64 | if (position < listCopy.size()) 65 | return listCopy.get(position); 66 | else 67 | return null; 68 | } else { 69 | final ArrayList originalList = getOriginalList(); 70 | if (position < originalList.size()) 71 | return originalList.get(position); 72 | else 73 | return null; 74 | } 75 | } 76 | 77 | @Override 78 | public int getCount() { 79 | final ArrayList listCopy = getFilterListCopy(); 80 | if (listCopy != null) 81 | return listCopy.size(); 82 | else 83 | return getOriginalList().size(); 84 | } 85 | 86 | private ArrayList getFilterListCopy() { 87 | return mFilterListCopy; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /library/src/main/java/lb/library/SectionedSectionIndexer.java: -------------------------------------------------------------------------------- 1 | package lb.library; 2 | 3 | import android.widget.SectionIndexer; 4 | 5 | public class SectionedSectionIndexer implements SectionIndexer { 6 | private final SimpleSection[] mSectionArray; 7 | 8 | public SectionedSectionIndexer(final SimpleSection[] sections) { 9 | mSectionArray = sections; 10 | // 11 | int previousIndex = 0; 12 | for (int i = 0; i < mSectionArray.length; ++i) { 13 | mSectionArray[i].startIndex = previousIndex; 14 | previousIndex += mSectionArray[i].getItemsCount(); 15 | mSectionArray[i].endIndex = previousIndex - 1; 16 | } 17 | } 18 | 19 | @Override 20 | public int getPositionForSection(final int section) { 21 | final int result = section < 0 || section >= mSectionArray.length ? -1 : mSectionArray[section].startIndex; 22 | return result; 23 | } 24 | 25 | /** given a flat position, returns the position within the section */ 26 | public int getPositionInSection(final int flatPos) { 27 | final int sectionForPosition = getSectionForPosition(flatPos); 28 | final SimpleSection simpleSection = mSectionArray[sectionForPosition]; 29 | return flatPos - simpleSection.startIndex; 30 | } 31 | 32 | @Override 33 | public int getSectionForPosition(final int flatPos) { 34 | if (flatPos < 0) 35 | return -1; 36 | int start = 0, end = mSectionArray.length - 1; 37 | int piv = (start + end) / 2; 38 | while (true) { 39 | final SimpleSection section = mSectionArray[piv]; 40 | if (flatPos >= section.startIndex && flatPos <= section.endIndex) 41 | return piv; 42 | if (piv == start && start == end) 43 | return -1; 44 | if (flatPos < section.startIndex) 45 | end = piv - 1; 46 | else 47 | start = piv + 1; 48 | piv = (start + end) / 2; 49 | } 50 | } 51 | 52 | @Override 53 | public SimpleSection[] getSections() { 54 | return mSectionArray; 55 | } 56 | 57 | public Object getItem(final int flatPos) { 58 | final int sectionIndex = getSectionForPosition(flatPos); 59 | final SimpleSection section = mSectionArray[sectionIndex]; 60 | final Object result = section.getItem(flatPos - section.startIndex); 61 | return result; 62 | } 63 | 64 | public Object getItem(final int sectionIndex, final int positionInSection) { 65 | final SimpleSection section = mSectionArray[sectionIndex]; 66 | final Object result = section.getItem(positionInSection); 67 | return result; 68 | } 69 | 70 | public int getRawPosition(final int sectionIndex, final int positionInSection) { 71 | final SimpleSection section = mSectionArray[sectionIndex]; 72 | return section.startIndex + positionInSection; 73 | } 74 | 75 | public int getItemsCount() { 76 | if (mSectionArray.length == 0) 77 | return 0; 78 | return mSectionArray[mSectionArray.length - 1].endIndex + 1; 79 | } 80 | 81 | // ///////////////////////////////////////////// 82 | // Section // 83 | // ////////// 84 | public static abstract class SimpleSection { 85 | private String name; 86 | private int startIndex, endIndex; 87 | 88 | public SimpleSection() { 89 | } 90 | 91 | public SimpleSection(final String sectionName) { 92 | this.name = sectionName; 93 | } 94 | 95 | public String getName() { 96 | return name; 97 | } 98 | 99 | public void setName(final String name) { 100 | this.name = name; 101 | } 102 | 103 | public abstract int getItemsCount(); 104 | 105 | public abstract Object getItem(int posInSection); 106 | 107 | @Override 108 | public String toString() 109 | { 110 | return name; 111 | } 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /library/src/main/java/lb/library/StringArrayAlphabetIndexer.java: -------------------------------------------------------------------------------- 1 | package lb.library; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.Locale; 8 | import java.util.Map; 9 | import java.util.Set; 10 | import java.util.TreeSet; 11 | 12 | public class StringArrayAlphabetIndexer extends SectionedSectionIndexer 13 | { 14 | /** 15 | * @param items each of the items. Note that they must be sorted in a way that each chunk will belong to 16 | * a specific header. For example, chunk with anything that starts with "A"/"a", then a chunk 17 | * that all of its items start with "B"/"b" , etc... 18 | * @param useOnlyUppercaseHeaders whether the header will be in uppercase or not. 19 | * if true, you must order the items so that each chunk will have its items start with either the lowercase or uppercase letter 20 | */ 21 | public StringArrayAlphabetIndexer(String[] items,boolean useOnlyUppercaseHeaders) 22 | { 23 | super(createSectionsFromStrings(items,useOnlyUppercaseHeaders)); 24 | } 25 | 26 | private static SimpleSection[] createSectionsFromStrings(String[] items,boolean useOnlyUppercaseHeaders) 27 | { 28 | //get all of the headers of the sections and their sections-items: 29 | Map> headerToSectionItemsMap=new HashMap>(); 30 | Set alphabetSet=new TreeSet(); 31 | for(String item : items) 32 | { 33 | String firstLetter=TextUtils.isEmpty(item)?" ":useOnlyUppercaseHeaders?item.substring(0,1).toUpperCase(Locale.getDefault()): 34 | item.substring(0,1); 35 | ArrayList sectionItems=headerToSectionItemsMap.get(firstLetter); 36 | if(sectionItems==null) 37 | headerToSectionItemsMap.put(firstLetter,sectionItems=new ArrayList()); 38 | sectionItems.add(item); 39 | alphabetSet.add(firstLetter); 40 | } 41 | //prepare the sections, and also sort each section's items : 42 | SimpleSection[] sections=new SimpleSection[alphabetSet.size()]; 43 | int i=0; 44 | for(String headerTitle : alphabetSet) 45 | { 46 | ArrayList sectionItems=headerToSectionItemsMap.get(headerTitle); 47 | SimpleSection simpleSection=new AlphaBetSection(sectionItems); 48 | simpleSection.setName(headerTitle); 49 | sections[i++]=simpleSection; 50 | } 51 | return sections; 52 | } 53 | 54 | public static class AlphaBetSection extends SimpleSection 55 | { 56 | private ArrayList items; 57 | 58 | private AlphaBetSection(ArrayList items) 59 | { 60 | this.items=items; 61 | } 62 | 63 | @Override 64 | public int getItemsCount() 65 | { 66 | return items.size(); 67 | } 68 | 69 | @Override 70 | public String getItem(int posInSection) 71 | { 72 | return items.get(posInSection); 73 | } 74 | 75 | } 76 | 77 | 78 | } 79 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':library' 2 | --------------------------------------------------------------------------------