├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle ├── circle.yml ├── environmentSetup.sh ├── expandablecheckrecyclerview ├── .gitignore ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── thoughtbot │ │ └── expandablecheckrecyclerview │ │ ├── CheckableChildRecyclerViewAdapter.java │ │ ├── ChildCheckController.java │ │ ├── listeners │ │ ├── OnCheckChildClickListener.java │ │ ├── OnChildCheckChangedListener.java │ │ └── OnChildrenCheckStateChangedListener.java │ │ ├── models │ │ ├── CheckedExpandableGroup.java │ │ ├── MultiCheckExpandableGroup.java │ │ └── SingleCheckExpandableGroup.java │ │ └── viewholders │ │ └── CheckableChildViewHolder.java │ └── res │ └── values │ └── strings.xml ├── expandablerecyclerview ├── .gitignore ├── build.gradle └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── thoughtbot │ │ │ └── expandablerecyclerview │ │ │ ├── ExpandCollapseController.java │ │ │ ├── ExpandableRecyclerViewAdapter.java │ │ │ ├── MultiTypeExpandableRecyclerViewAdapter.java │ │ │ ├── listeners │ │ │ ├── ExpandCollapseListener.java │ │ │ ├── GroupExpandCollapseListener.java │ │ │ └── OnGroupClickListener.java │ │ │ ├── models │ │ │ ├── ExpandableGroup.java │ │ │ ├── ExpandableList.java │ │ │ └── ExpandableListPosition.java │ │ │ └── viewholders │ │ │ ├── ChildViewHolder.java │ │ │ └── GroupViewHolder.java │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── thoughtbot │ └── expandablerecyclerview │ ├── ExpandableListTest.java │ └── testUtils │ └── TestDataFactory.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── thoughtbot │ │ └── expandablerecyclerview │ │ └── sample │ │ ├── ApplicationTest.java │ │ └── ExpandActivityTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── thoughtbot │ │ │ └── expandablerecyclerview │ │ │ └── sample │ │ │ ├── Artist.java │ │ │ ├── Genre.java │ │ │ ├── GenreDataFactory.java │ │ │ ├── MainActivity.java │ │ │ ├── expand │ │ │ ├── ArtistViewHolder.java │ │ │ ├── ExpandActivity.java │ │ │ ├── GenreAdapter.java │ │ │ └── GenreViewHolder.java │ │ │ ├── multicheck │ │ │ ├── MultiCheckActivity.java │ │ │ ├── MultiCheckArtistViewHolder.java │ │ │ ├── MultiCheckGenre.java │ │ │ └── MultiCheckGenreAdapter.java │ │ │ ├── multitype │ │ │ ├── FavoriteArtistViewHolder.java │ │ │ ├── MultiTypeActivity.java │ │ │ └── MultiTypeGenreAdapter.java │ │ │ ├── multitypeandcheck │ │ │ ├── MultiTypeCheckGenreActivity.java │ │ │ └── MultiTypeCheckGenreAdapter.java │ │ │ └── singlecheck │ │ │ ├── SingleCheckActivity.java │ │ │ ├── SingleCheckArtistViewHolder.java │ │ │ ├── SingleCheckGenre.java │ │ │ └── SingleCheckGenreAdapter.java │ └── res │ │ ├── drawable │ │ ├── ic_arrow_down.xml │ │ ├── ic_banjo.xml │ │ ├── ic_electric_guitar.xml │ │ ├── ic_maracas.xml │ │ ├── ic_saxaphone.xml │ │ ├── ic_star.xml │ │ └── ic_violin.xml │ │ ├── layout │ │ ├── activity_expand.xml │ │ ├── activity_main.xml │ │ ├── activity_multi_check.xml │ │ ├── activity_multi_type.xml │ │ ├── activity_multi_type_and_check.xml │ │ ├── activity_single_check.xml │ │ ├── list_item_artist.xml │ │ ├── list_item_favorite_artist.xml │ │ ├── list_item_genre.xml │ │ ├── list_item_multicheck_artist.xml │ │ └── list_item_singlecheck_arist.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── thoughtbot │ └── expandablerecyclerview │ └── sample │ ├── GenreAdapterTest.java │ └── MultiTypeGenreAdapterTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | ### Android ### 2 | # Built application files 3 | *.apk 4 | *.ap_ 5 | 6 | # Files for the Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Log Files 24 | *.log 25 | ### Intellij ### 26 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 27 | 28 | *.iml 29 | build 30 | 31 | ## Directory-based project format: 32 | .idea/ 33 | 34 | ## File-based project format: 35 | *.ipr 36 | *.iws 37 | 38 | ## Plugin-specific files: 39 | 40 | # IntelliJ 41 | /out/ 42 | -------------------------------------------------------------------------------- /.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 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 52 | 55 | 56 | 57 | 58 | 59 | 60 | 1.8 61 | 62 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 1.5 - September 11th, 2020 4 | 5 | - BUGFIX: fix writeToParcel IndexOutOfBounds (#183). 6 | - Feature: explicitly expand/collapse state instead of just toggle (#146). 7 | - Documentation improvements (#48, #66, #143). 8 | - New maintainer. Hi, I'm Mike. 9 | 10 | ## Version 1.4 - January 17th 2017 11 | - Upgrade Robolectric version to `3.2.1` 12 | - Fix how `GroupViewHolder`'s expand and collapse method are called 13 | - Avoid unnecessary conversion between `ExpandableListPosition` and group index 14 | - Add layout for each sample activity 15 | - Only update children check states if group is expanded 16 | - Update how `clearChoices()` propagates view updates 17 | - Remove unnecessary check for only `CheckedTextView` and toggle everything in `CheckableChildViewHolder` 18 | - Update `ExpandActivity` sample to turn off flash animation 19 | 20 | ## Version 1.3 - December 15th 2016 21 | - Add ability to programmatically toggle group using group 22 | - Only expand / collapse groups if they have items 23 | - Properly deserialize expandable group from Parcel 24 | 25 | ## Version 1.2 - November 14th 2016 26 | - Ensure onSaveInstanceState is saving a `java.util.ArrayList` 27 | - Replace `SparseBooleanArray` with Boolean Array in `CheckedExpandableGroup` 28 | - Replace SparseBooleanArray in favor of Boolean array in `ExpandCollapseController` 29 | 30 | 31 | ## Version 1.1 - October 13th 2016 32 | - Upgrade Gradle build tools to version `2.2.0` 33 | - Fix bug where expand and collapse were reversed in `GroupViewHolder` 34 | 35 | ## Version 1.0 - August 16th 2016 36 | Initial Release 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Expandable Recycler View 2 | 3 | We love pull requests from everyone. By participating in this project, you 4 | agree to abide by the thoughtbot [code of conduct]. 5 | 6 | [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct 7 | 8 | Here are some ways *you* can contribute: 9 | 10 | * by reporting bugs 11 | * by suggesting new features 12 | * by writing or editing documentation 13 | * by writing specifications 14 | * by writing code ( **no patch is too small** : fix typos, add comments, clean up inconsistent whitespace ) 15 | * by refactoring code 16 | * by closing [issues][] 17 | * by reviewing patches 18 | 19 | [issues]: https://github.com/thoughtbot/expandable-recycler-view/issues 20 | 21 | ## Submitting an Issue 22 | We use the [GitHub issue tracker][issues] to track bugs and features. Before 23 | submitting a bug report or feature request, check to make sure it hasn't 24 | already been submitted. When submitting a bug report, please include a [Gist][] 25 | that includes a stack trace and any details that may be necessary to reproduce 26 | the bug, including your Java version, Android Studio version, and operating system. 27 | Ideally, a bug report should include a pull request with failing specs. 28 | 29 | [gist]: https://gist.github.com/ 30 | 31 | ## Submitting a Pull Request 32 | 1. [Fork][fork] the [official repository][repo]. 33 | 2. [Create a topic branch.][branch] 34 | 3. Implement your feature or bug fix 35 | 4. Add, commit, and push your changes. 36 | 5. [Submit a pull request.][pr] 37 | 38 | ## Notes 39 | * Please add tests if you changed code. 40 | * Please make sure your code follows the [thoughtbot java style guide][styles] 41 | 42 | [repo]: https://github.com/thoughtbot/expandable-recycler-view/tree/master/expandablerecyclerview 43 | [fork]: https://help.github.com/articles/fork-a-repo/ 44 | [branch]: https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/ 45 | [pr]: https://help.github.com/articles/using-pull-requests/ 46 | [styles]: https://github.com/thoughtbot/guides/tree/master/style/android/java 47 | 48 | Inspired by https://github.com/middleman/middleman-heroku/blob/master/CONTRIBUTING.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Amanda Hill and thoughtbot, inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /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.2.3' 9 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' 10 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1' 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | jcenter() 20 | } 21 | } 22 | 23 | task clean(type: Delete) { 24 | delete rootProject.buildDir 25 | } 26 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Build configuration for Circle CI 3 | # 4 | general: 5 | artifacts: 6 | - "app/build/outputs/apk" 7 | - "app/build/reports/tests" 8 | - "app/build/outputs/androidTest-results" 9 | 10 | machine: 11 | timezone: 12 | America/Los_Angeles 13 | java: 14 | version: oraclejdk8 15 | environment: 16 | ANDROID_HOME: /usr/local/android-sdk-linux 17 | 18 | dependencies: 19 | pre: 20 | - source environmentSetup.sh && get_android_sdk_24 21 | - mkdir -p $ANDROID_HOME"/licenses" 22 | - echo $ANDROID_SDK_LICENSE > $ANDROID_HOME"/licenses/android-sdk-license" 23 | override: 24 | - ./gradlew clean assembleRelease -PdisablePreDex 25 | 26 | test: 27 | override: 28 | - ./gradlew testRelease 29 | -------------------------------------------------------------------------------- /environmentSetup.sh: -------------------------------------------------------------------------------- 1 | # Environment Setup which is required for Circle CI 2 | 3 | function affirmative_android_update { 4 | echo y | android update sdk --no-ui --all --filter "$1" 5 | } 6 | 7 | function get_android_sdk_24 { 8 | # fix the CircleCI path 9 | # export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools:$PATH" 10 | 11 | DEPS="$ANDROID_HOME/installed-dependencies" 12 | 13 | if [ ! -e $DEPS ]; then 14 | echo Fetch and install Android SDK 24 15 | echo y | android update sdk --no-ui --all --filter tools,platform-tools,build-tools-24.0.2,android-24,extra-google-m2repository,extra-google-google_play_services,extra-android-m2repository,extra-android-support 16 | # create the folder so we won't do this again (this is soooo Apache Ant right here) 17 | touch $DEPS 18 | fi 19 | } 20 | -------------------------------------------------------------------------------- /expandablecheckrecyclerview/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /expandablecheckrecyclerview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | //ext { 4 | // bintrayRepo = 'maven' 5 | // bintrayName = 'expandablecheckrecyclerview' 6 | // 7 | // publishedGroupId = 'com.thoughtbot' 8 | // libraryName = 'expandablecheckrecyclerview' 9 | // artifact = 'expandablecheckrecyclerview' 10 | // 11 | // libraryDescription = 12 | // 'Custom Android RecyclerViewAdapters that collapse and expand and support checking and unchecking children' 13 | // 14 | // siteUrl = 'https://github.com/thoughtbot/expandable-recycler-view' 15 | // gitUrl = 'https://github.com/thoughtbot/expandable-recycler-view.git' 16 | // 17 | // libraryVersion = '1.5' 18 | // 19 | // developerId = 'mandybess' 20 | // developerName = 'Amanda Hill' 21 | // developerEmail = 'amandabesshill@gmail.com' 22 | // 23 | // licenseName = 'The Apache Software License, Version 2.0' 24 | // licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 25 | // allLicenses = ["Apache-2.0"] 26 | //} 27 | 28 | android { 29 | compileSdkVersion 23 30 | buildToolsVersion "23.0.3" 31 | 32 | defaultConfig { 33 | minSdkVersion 16 34 | targetSdkVersion 23 35 | versionCode 5 36 | versionName "1.5" 37 | } 38 | buildTypes { 39 | release { 40 | minifyEnabled false 41 | } 42 | } 43 | } 44 | 45 | dependencies { 46 | compile fileTree(dir: 'libs', include: ['*.jar']) 47 | compile 'com.thoughtbot:expandablerecyclerview:1.3' 48 | 49 | //android 50 | compile 'com.android.support:appcompat-v7:23.4.0' 51 | compile 'com.android.support:recyclerview-v7:23.4.0' 52 | 53 | //unit tests 54 | testCompile 'junit:junit:4.12' 55 | testCompile 'org.robolectric:robolectric:3.2.1' 56 | testCompile 'org.mockito:mockito-core:1.10.5' 57 | } 58 | 59 | //apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle' 60 | //apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle' 61 | -------------------------------------------------------------------------------- /expandablecheckrecyclerview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /expandablecheckrecyclerview/src/main/java/com/thoughtbot/expandablecheckrecyclerview/CheckableChildRecyclerViewAdapter.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablecheckrecyclerview; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import com.thoughtbot.expandablecheckrecyclerview.listeners.OnCheckChildClickListener; 8 | import com.thoughtbot.expandablecheckrecyclerview.listeners.OnChildCheckChangedListener; 9 | import com.thoughtbot.expandablecheckrecyclerview.listeners.OnChildrenCheckStateChangedListener; 10 | import com.thoughtbot.expandablecheckrecyclerview.models.CheckedExpandableGroup; 11 | import com.thoughtbot.expandablecheckrecyclerview.viewholders.CheckableChildViewHolder; 12 | import com.thoughtbot.expandablerecyclerview.ExpandableRecyclerViewAdapter; 13 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 14 | import com.thoughtbot.expandablerecyclerview.models.ExpandableListPosition; 15 | import com.thoughtbot.expandablerecyclerview.viewholders.GroupViewHolder; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | public abstract class CheckableChildRecyclerViewAdapter 20 | extends ExpandableRecyclerViewAdapter 21 | implements OnChildCheckChangedListener, OnChildrenCheckStateChangedListener { 22 | 23 | private static final String CHECKED_STATE_MAP = "child_check_controller_checked_state_map"; 24 | 25 | private ChildCheckController childCheckController; 26 | private OnCheckChildClickListener childClickListener; 27 | 28 | public CheckableChildRecyclerViewAdapter(List groups) { 29 | super(groups); 30 | childCheckController = new ChildCheckController(expandableList, this); 31 | } 32 | 33 | @Override 34 | public CCVH onCreateChildViewHolder(ViewGroup parent, int viewType) { 35 | CCVH CCVH = onCreateCheckChildViewHolder(parent, viewType); 36 | CCVH.setOnChildCheckedListener(this); 37 | return CCVH; 38 | } 39 | 40 | @Override 41 | public void onBindChildViewHolder(CCVH holder, int flatPosition, ExpandableGroup group, 42 | int childIndex) { 43 | ExpandableListPosition listPosition = expandableList.getUnflattenedPosition(flatPosition); 44 | holder.onBindViewHolder(flatPosition, childCheckController.isChildChecked(listPosition)); 45 | onBindCheckChildViewHolder(holder, flatPosition, (CheckedExpandableGroup) group, childIndex); 46 | } 47 | 48 | @Override 49 | public void onChildCheckChanged(View view, boolean checked, int flatPos) { 50 | ExpandableListPosition listPos = expandableList.getUnflattenedPosition(flatPos); 51 | childCheckController.onChildCheckChanged(checked, listPos); 52 | if (childClickListener != null) { 53 | childClickListener.onCheckChildCLick(view, checked, 54 | (CheckedExpandableGroup) expandableList.getExpandableGroup(listPos), listPos.childPos); 55 | } 56 | } 57 | 58 | @Override 59 | public void updateChildrenCheckState(int firstChildFlattenedIndex, int numChildren) { 60 | notifyItemRangeChanged(firstChildFlattenedIndex, numChildren); 61 | } 62 | 63 | public void setChildClickListener(OnCheckChildClickListener listener) { 64 | childClickListener = listener; 65 | } 66 | 67 | /** 68 | * Stores the checked state map across state loss. 69 | *

70 | * Should be called from whatever {@link Activity} that hosts the RecyclerView that {@link 71 | * CheckableChildRecyclerViewAdapter} is attached to. 72 | *

73 | * This will make sure to add the checked state map as an extra to the 74 | * instance state bundle to be used in {@link #onRestoreInstanceState(Bundle)}. 75 | * 76 | * @param outState The {@code Bundle} into which to store the 77 | * chekced state map 78 | */ 79 | @Override 80 | public void onSaveInstanceState(Bundle outState) { 81 | outState.putParcelableArrayList(CHECKED_STATE_MAP, new ArrayList(expandableList.groups)); 82 | super.onSaveInstanceState(outState); 83 | } 84 | 85 | /** 86 | * Fetches the checked state map from the saved instance state {@link Bundle} 87 | * and restores the checked states of all of the child list items. 88 | *

89 | * Should be called from {@link Activity#onRestoreInstanceState(Bundle)} in 90 | * the {@link Activity} that hosts the RecyclerView that this 91 | * {@link CheckableChildRecyclerViewAdapter} is attached to. 92 | *

93 | * 94 | * @param savedInstanceState The {@code Bundle} from which the expanded 95 | * state map is loaded 96 | */ 97 | @Override 98 | public void onRestoreInstanceState(Bundle savedInstanceState) { 99 | if (savedInstanceState == null || !savedInstanceState.containsKey(CHECKED_STATE_MAP)) { 100 | return; 101 | } 102 | expandableList.groups = savedInstanceState.getParcelableArrayList(CHECKED_STATE_MAP); 103 | super.onRestoreInstanceState(savedInstanceState); 104 | } 105 | 106 | /** 107 | * Manually (programmatically) update the check state of a child 108 | * 109 | * @param checked the desired check state, true will check the item, false will uncheck it if 110 | * possible 111 | * @param groupIndex the index of the {@code ExpandableGroup} within {@code getGroups()} 112 | * @param childIndex the index of the child within it's group 113 | */ 114 | public void checkChild(boolean checked, int groupIndex, int childIndex) { 115 | childCheckController.checkChild(checked, groupIndex, childIndex); 116 | if (childClickListener != null) { 117 | childClickListener.onCheckChildCLick(null, checked, 118 | (CheckedExpandableGroup) expandableList.groups.get(groupIndex), childIndex); 119 | } 120 | } 121 | 122 | /** 123 | * Clear any choices previously checked 124 | */ 125 | public void clearChoices() { 126 | childCheckController.clearCheckStates(); 127 | 128 | //only update the child views that are visible (i.e. their group is expanded) 129 | for (int i = 0; i < getGroups().size(); i++) { 130 | ExpandableGroup group = getGroups().get(i); 131 | if (isGroupExpanded(group)) { 132 | notifyItemRangeChanged(expandableList.getFlattenedFirstChildIndex(i), group.getItemCount()); 133 | } 134 | } 135 | } 136 | 137 | /** 138 | * Called from #onCreateViewHolder(ViewGroup, int) when the list item created is a child 139 | * 140 | * @param parent the {@link ViewGroup} in the list for which a {@link CCVH} is being created 141 | * @return A {@link CCVH} corresponding to child list item with the {@code ViewGroup} parent 142 | */ 143 | public abstract CCVH onCreateCheckChildViewHolder(ViewGroup parent, int viewType); 144 | 145 | /** 146 | * Called from onBindViewHolder(RecyclerView.ViewHolder, int) when the list item 147 | * bound to is a child. 148 | *

149 | * Bind data to the {@link CCVH} here. 150 | * 151 | * @param holder The {@code CCVH} to bind data to 152 | * @param flatPosition the flat position (raw index) in the list at which to bind the child 153 | * @param group The {@link CheckedExpandableGroup} that the the child list item belongs to 154 | * @param childIndex the index of this child within it's {@link CheckedExpandableGroup} 155 | */ 156 | public abstract void onBindCheckChildViewHolder(CCVH holder, int flatPosition, 157 | CheckedExpandableGroup group, int childIndex); 158 | } 159 | -------------------------------------------------------------------------------- /expandablecheckrecyclerview/src/main/java/com/thoughtbot/expandablecheckrecyclerview/ChildCheckController.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablecheckrecyclerview; 2 | 3 | import android.widget.Checkable; 4 | import android.widget.ExpandableListView; 5 | import com.thoughtbot.expandablecheckrecyclerview.listeners.OnChildrenCheckStateChangedListener; 6 | import com.thoughtbot.expandablecheckrecyclerview.models.CheckedExpandableGroup; 7 | import com.thoughtbot.expandablecheckrecyclerview.viewholders.CheckableChildViewHolder; 8 | import com.thoughtbot.expandablerecyclerview.models.ExpandableList; 9 | import com.thoughtbot.expandablerecyclerview.models.ExpandableListPosition; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class ChildCheckController { 14 | 15 | private ExpandableList expandableList; 16 | private OnChildrenCheckStateChangedListener childrenUpdateListener; 17 | private List initialCheckedPositions; 18 | 19 | public ChildCheckController(ExpandableList expandableList, 20 | OnChildrenCheckStateChangedListener listener) { 21 | this.expandableList = expandableList; 22 | this.childrenUpdateListener = listener; 23 | initialCheckedPositions = getCheckedPositions(); 24 | } 25 | 26 | /** 27 | * Triggered by a click event on a {@link CheckableChildViewHolder} causing the {@link Checkable} 28 | * object to change checked states 29 | * 30 | * @param checked The current checked state of the view 31 | * @param listPosition The flat position (raw index) of the {@link CheckableChildViewHolder} 32 | */ 33 | public void onChildCheckChanged(boolean checked, ExpandableListPosition listPosition) { 34 | CheckedExpandableGroup group = 35 | (CheckedExpandableGroup) expandableList.groups.get(listPosition.groupPos); 36 | group.onChildClicked(listPosition.childPos, checked); 37 | if (childrenUpdateListener != null) { 38 | childrenUpdateListener.updateChildrenCheckState( 39 | expandableList.getFlattenedFirstChildIndex(listPosition), 40 | expandableList.getExpandableGroupItemCount(listPosition)); 41 | } 42 | } 43 | 44 | public void checkChild(boolean checked, int groupIndex, int childIndex) { 45 | CheckedExpandableGroup group = (CheckedExpandableGroup) expandableList.groups.get(groupIndex); 46 | group.onChildClicked(childIndex, checked); 47 | if (childrenUpdateListener != null) { 48 | //only update children check states if group is expanded 49 | boolean isGroupExpanded = expandableList.expandedGroupIndexes[groupIndex]; 50 | if (isGroupExpanded) { 51 | childrenUpdateListener.updateChildrenCheckState( 52 | expandableList.getFlattenedFirstChildIndex(groupIndex), group.getItemCount()); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * @param listPosition the ExpandableListPosition representation of a child list item 59 | * @return The current checked state of the view 60 | */ 61 | public boolean isChildChecked(ExpandableListPosition listPosition) { 62 | CheckedExpandableGroup group = 63 | (CheckedExpandableGroup) expandableList.groups.get(listPosition.groupPos); 64 | return group.isChildChecked(listPosition.childPos); 65 | } 66 | 67 | /** 68 | * @return list of indexes of all checked child items 69 | */ 70 | public List getCheckedPositions() { 71 | List selected = new ArrayList<>(); 72 | for (int i = 0; i < expandableList.groups.size(); i++) { 73 | if (expandableList.groups.get(i) instanceof CheckedExpandableGroup) { 74 | CheckedExpandableGroup group = (CheckedExpandableGroup) expandableList.groups.get(i); 75 | for (int j = 0; j < group.getItemCount(); j++) { 76 | if (group.isChildChecked(j)) { 77 | long packedPosition = ExpandableListView.getPackedPositionForChild(i, j); 78 | selected.add(expandableList.getFlattenedChildIndex(packedPosition)); 79 | } 80 | } 81 | } 82 | } 83 | return selected; 84 | } 85 | 86 | /** 87 | * @return true if the checked state of any of the child items has changed since this class was 88 | * initialized 89 | */ 90 | public boolean checksChanged() { 91 | return !initialCheckedPositions.equals(getCheckedPositions()); 92 | } 93 | 94 | /** 95 | * Clear any choices previously checked 96 | */ 97 | public void clearCheckStates() { 98 | for (int i = 0; i < expandableList.groups.size(); i++) { 99 | CheckedExpandableGroup group = (CheckedExpandableGroup) expandableList.groups.get(i); 100 | group.clearSelections(); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /expandablecheckrecyclerview/src/main/java/com/thoughtbot/expandablecheckrecyclerview/listeners/OnCheckChildClickListener.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablecheckrecyclerview.listeners; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.View; 5 | import com.thoughtbot.expandablecheckrecyclerview.CheckableChildRecyclerViewAdapter; 6 | import com.thoughtbot.expandablecheckrecyclerview.models.CheckedExpandableGroup; 7 | import com.thoughtbot.expandablecheckrecyclerview.viewholders.CheckableChildViewHolder; 8 | 9 | /** 10 | * Interface definition for a callback to be invoked when a {@link CheckableChildViewHolder} 11 | * has been clicked. 12 | */ 13 | public interface OnCheckChildClickListener { 14 | /** 15 | * Callback method to be invoked when a child in the {@link CheckableChildRecyclerViewAdapter} 16 | * has been clicked. 17 | * 18 | * @param checked the current check state of the child 19 | * @param v The view within the RecyclerView that was clicked 20 | * @param group The {@link CheckedExpandableGroup} that contains the child that was clicked 21 | * @param childIndex The child position within the {@link CheckedExpandableGroup} 22 | */ 23 | void onCheckChildCLick(View v, boolean checked, CheckedExpandableGroup group, int childIndex); 24 | } 25 | -------------------------------------------------------------------------------- /expandablecheckrecyclerview/src/main/java/com/thoughtbot/expandablecheckrecyclerview/listeners/OnChildCheckChangedListener.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablecheckrecyclerview.listeners; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.View; 5 | import com.thoughtbot.expandablecheckrecyclerview.viewholders.CheckableChildViewHolder; 6 | 7 | /** 8 | * Interface definition for a callback to be invoked when a CheckableChildViewHolder#checkable 9 | * has been clicked. 10 | */ 11 | public interface OnChildCheckChangedListener { 12 | 13 | /** 14 | * @param checked The current checked state of the view 15 | * @param flatPos The flat position (raw index) of the the child within the RecyclerView 16 | */ 17 | void onChildCheckChanged(View view, boolean checked, int flatPos); 18 | 19 | } -------------------------------------------------------------------------------- /expandablecheckrecyclerview/src/main/java/com/thoughtbot/expandablecheckrecyclerview/listeners/OnChildrenCheckStateChangedListener.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablecheckrecyclerview.listeners; 2 | 3 | import com.thoughtbot.expandablecheckrecyclerview.models.CheckedExpandableGroup; 4 | 5 | public interface OnChildrenCheckStateChangedListener { 6 | 7 | /** 8 | * @param firstChildFlattenedIndex the flat position of the first child in the {@link 9 | * CheckedExpandableGroup} 10 | * @param numChildren the total number of children in the {@link CheckedExpandableGroup} 11 | */ 12 | void updateChildrenCheckState(int firstChildFlattenedIndex, int numChildren); 13 | } 14 | -------------------------------------------------------------------------------- /expandablecheckrecyclerview/src/main/java/com/thoughtbot/expandablecheckrecyclerview/models/CheckedExpandableGroup.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablecheckrecyclerview.models; 2 | 3 | import android.os.Parcel; 4 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | /** 9 | * An extension of ExpandableGroup that holds onto the checked state of it's children 10 | */ 11 | public abstract class CheckedExpandableGroup extends ExpandableGroup { 12 | 13 | public boolean[] selectedChildren; 14 | 15 | public CheckedExpandableGroup(String title, List items) { 16 | super(title, items); 17 | selectedChildren = new boolean[items.size()]; 18 | for (int i = 0; i < items.size(); i++) { 19 | selectedChildren[i] = false; 20 | } 21 | } 22 | 23 | public void checkChild(int childIndex) { 24 | selectedChildren[childIndex] = true; 25 | } 26 | 27 | public void unCheckChild(int childIndex) { 28 | selectedChildren[childIndex] = false; 29 | } 30 | 31 | public boolean isChildChecked(int childIndex) { 32 | return selectedChildren[childIndex]; 33 | } 34 | 35 | public void clearSelections() { 36 | if (selectedChildren != null) { 37 | Arrays.fill(selectedChildren, false); 38 | } 39 | } 40 | 41 | protected CheckedExpandableGroup(Parcel in) { 42 | super(in); 43 | selectedChildren = in.createBooleanArray(); 44 | } 45 | 46 | @Override 47 | public void writeToParcel(Parcel dest, int flags) { 48 | super.writeToParcel(dest, flags); 49 | dest.writeBooleanArray(selectedChildren); 50 | } 51 | 52 | @Override 53 | public int describeContents() { 54 | return 0; 55 | } 56 | 57 | public abstract void onChildClicked(int childIndex, boolean checked); 58 | 59 | } 60 | -------------------------------------------------------------------------------- /expandablecheckrecyclerview/src/main/java/com/thoughtbot/expandablecheckrecyclerview/models/MultiCheckExpandableGroup.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablecheckrecyclerview.models; 2 | 3 | import android.os.Parcel; 4 | import java.util.List; 5 | 6 | /** 7 | * A subclass of {@link CheckedExpandableGroup} that allows for multiple children to be checked at 8 | * one time 9 | */ 10 | public class MultiCheckExpandableGroup extends CheckedExpandableGroup { 11 | 12 | public MultiCheckExpandableGroup(String title, List items) { 13 | super(title, items); 14 | } 15 | 16 | @Override 17 | public void onChildClicked(int childIndex, boolean checked) { 18 | if (checked) { 19 | checkChild(childIndex); 20 | } else { 21 | unCheckChild(childIndex); 22 | } 23 | } 24 | 25 | protected MultiCheckExpandableGroup(Parcel in) { 26 | super(in); 27 | } 28 | 29 | @Override 30 | public void writeToParcel(Parcel dest, int flags) { 31 | super.writeToParcel(dest, flags); 32 | } 33 | 34 | @Override 35 | public int describeContents() { 36 | return 0; 37 | } 38 | 39 | @SuppressWarnings("unused") 40 | public static final Creator CREATOR = 41 | new Creator() { 42 | @Override 43 | public MultiCheckExpandableGroup createFromParcel(Parcel in) { 44 | return new MultiCheckExpandableGroup(in); 45 | } 46 | 47 | @Override 48 | public MultiCheckExpandableGroup[] newArray(int size) { 49 | return new MultiCheckExpandableGroup[size]; 50 | } 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /expandablecheckrecyclerview/src/main/java/com/thoughtbot/expandablecheckrecyclerview/models/SingleCheckExpandableGroup.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablecheckrecyclerview.models; 2 | 3 | import android.os.Parcel; 4 | import java.util.List; 5 | 6 | /** 7 | * A subclass of {@link CheckedExpandableGroup} that allows for only *one* child to be checked at 8 | * one time 9 | */ 10 | public class SingleCheckExpandableGroup extends CheckedExpandableGroup { 11 | 12 | public SingleCheckExpandableGroup(String title, List items) { 13 | super(title, items); 14 | } 15 | 16 | @Override 17 | public void onChildClicked(int childIndex, boolean checked) { 18 | if (checked) { 19 | for (int i = 0; i < getItemCount(); i++) { 20 | unCheckChild(i); 21 | } 22 | checkChild(childIndex); 23 | } 24 | } 25 | 26 | protected SingleCheckExpandableGroup(Parcel in) { 27 | super(in); 28 | } 29 | 30 | @Override 31 | public void writeToParcel(Parcel dest, int flags) { 32 | super.writeToParcel(dest, flags); 33 | } 34 | 35 | @Override 36 | public int describeContents() { 37 | return 0; 38 | } 39 | 40 | @SuppressWarnings("unused") 41 | public static final Creator CREATOR = 42 | new Creator() { 43 | @Override 44 | public SingleCheckExpandableGroup createFromParcel(Parcel in) { 45 | return new SingleCheckExpandableGroup(in); 46 | } 47 | 48 | @Override 49 | public SingleCheckExpandableGroup[] newArray(int size) { 50 | return new SingleCheckExpandableGroup[size]; 51 | } 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /expandablecheckrecyclerview/src/main/java/com/thoughtbot/expandablecheckrecyclerview/viewholders/CheckableChildViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablecheckrecyclerview.viewholders; 2 | 3 | import android.view.View; 4 | import android.view.View.OnClickListener; 5 | import android.widget.Checkable; 6 | import com.thoughtbot.expandablecheckrecyclerview.listeners.OnChildCheckChangedListener; 7 | import com.thoughtbot.expandablerecyclerview.viewholders.ChildViewHolder; 8 | 9 | /** 10 | * An instance of ChildViewHolder that has a Checkable widget so that this view may 11 | * have a checked and unchecked state 12 | */ 13 | public abstract class CheckableChildViewHolder extends ChildViewHolder implements OnClickListener { 14 | 15 | private OnChildCheckChangedListener listener; 16 | private Checkable checkable; 17 | 18 | public CheckableChildViewHolder(View itemView) { 19 | super(itemView); 20 | itemView.setOnClickListener(this); 21 | } 22 | 23 | /** 24 | * @param flatPos the raw index of this CheckableChildViewHolder in the 25 | * RecyclerView 26 | * @param checked the state to set the Checkable widget to 27 | * see ChildCheckController#isChildChecked(ExpandableListPosition) 28 | */ 29 | public void onBindViewHolder(int flatPos, boolean checked) { 30 | checkable = getCheckable(); 31 | checkable.setChecked(checked); 32 | } 33 | 34 | @Override 35 | public void onClick(View v) { 36 | checkable.toggle(); 37 | if (listener != null) { 38 | listener.onChildCheckChanged(v, checkable.isChecked(), getAdapterPosition()); 39 | } 40 | } 41 | 42 | public void setOnChildCheckedListener(OnChildCheckChangedListener listener) { 43 | this.listener = listener; 44 | } 45 | 46 | /** 47 | * Called during {@link #onBindViewHolder(int, boolean)} 48 | * 49 | * return a {@link Checkable} widget associated with this ViewHolder 50 | */ 51 | public abstract Checkable getCheckable(); 52 | } -------------------------------------------------------------------------------- /expandablecheckrecyclerview/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ExpandableCheckRecyclerView 3 | 4 | -------------------------------------------------------------------------------- /expandablerecyclerview/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /expandablerecyclerview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | //ext { 4 | // bintrayRepo = 'maven' 5 | // bintrayName = 'expandablerecyclerview' 6 | // 7 | // publishedGroupId = 'com.thoughtbot' 8 | // libraryName = 'expandablerecyclerview' 9 | // artifact = 'expandablerecyclerview' 10 | // 11 | // libraryDescription = 'Custom Android RecyclerViewAdapters that collapse and expand' 12 | // 13 | // siteUrl = 'https://github.com/thoughtbot/expandable-recycler-view' 14 | // gitUrl = 'https://github.com/thoughtbot/expandable-recycler-view.git' 15 | // 16 | // libraryVersion = '1.4' 17 | // 18 | // developerId = 'mandybess' 19 | // developerName = 'Amanda Hill' 20 | // developerEmail = 'amandabesshill@gmail.com' 21 | // 22 | // licenseName = 'The Apache Software License, Version 2.0' 23 | // licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 24 | // allLicenses = ["Apache-2.0"] 25 | //} 26 | 27 | android { 28 | compileSdkVersion 23 29 | buildToolsVersion "23.0.3" 30 | 31 | defaultConfig { 32 | minSdkVersion 16 33 | targetSdkVersion 23 34 | versionCode 2 35 | versionName "1.4" 36 | } 37 | buildTypes { 38 | release { 39 | minifyEnabled false 40 | } 41 | } 42 | } 43 | 44 | dependencies { 45 | //android 46 | compile 'com.android.support:appcompat-v7:23.4.0' 47 | compile 'com.android.support:recyclerview-v7:23.4.0' 48 | 49 | //unit tests 50 | testCompile 'junit:junit:4.12' 51 | testCompile 'org.robolectric:robolectric:3.2.1' 52 | testCompile 'org.mockito:mockito-core:1.10.5' 53 | } 54 | //apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle' 55 | //apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle' 56 | -------------------------------------------------------------------------------- /expandablerecyclerview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /expandablerecyclerview/src/main/java/com/thoughtbot/expandablerecyclerview/ExpandCollapseController.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview; 2 | 3 | import com.thoughtbot.expandablerecyclerview.listeners.ExpandCollapseListener; 4 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 5 | import com.thoughtbot.expandablerecyclerview.models.ExpandableList; 6 | import com.thoughtbot.expandablerecyclerview.models.ExpandableListPosition; 7 | 8 | /** 9 | * This class is the sits between the backing {@link ExpandableList} and the {@link 10 | * ExpandableRecyclerViewAdapter} and mediates the expanding and collapsing of {@link 11 | * ExpandableGroup} 12 | */ 13 | public class ExpandCollapseController { 14 | 15 | private ExpandCollapseListener listener; 16 | private ExpandableList expandableList; 17 | 18 | public ExpandCollapseController(ExpandableList expandableList, ExpandCollapseListener listener) { 19 | this.expandableList = expandableList; 20 | this.listener = listener; 21 | } 22 | 23 | /** 24 | * Collapse a group 25 | * 26 | * @param listPosition position of the group to collapse 27 | */ 28 | private void collapseGroup(ExpandableListPosition listPosition) { 29 | expandableList.expandedGroupIndexes[listPosition.groupPos] = false; 30 | if (listener != null) { 31 | listener.onGroupCollapsed(expandableList.getFlattenedGroupIndex(listPosition) + 1, 32 | expandableList.groups.get(listPosition.groupPos).getItemCount()); 33 | } 34 | } 35 | 36 | /** 37 | * Expand a group 38 | * 39 | * @param listPosition the group to be expanded 40 | */ 41 | private void expandGroup(ExpandableListPosition listPosition) { 42 | expandableList.expandedGroupIndexes[listPosition.groupPos] = true; 43 | if (listener != null) { 44 | listener.onGroupExpanded(expandableList.getFlattenedGroupIndex(listPosition) + 1, 45 | expandableList.groups.get(listPosition.groupPos).getItemCount()); 46 | } 47 | } 48 | 49 | /** 50 | * @param group the {@link ExpandableGroup} being checked for its collapsed state 51 | * @return true if {@code group} is expanded, false if it is collapsed 52 | */ 53 | public boolean isGroupExpanded(ExpandableGroup group) { 54 | int groupIndex = expandableList.groups.indexOf(group); 55 | return expandableList.expandedGroupIndexes[groupIndex]; 56 | } 57 | 58 | /** 59 | * @param flatPos the flattened position of an item in the list 60 | * @return true if {@code group} is expanded, false if it is collapsed 61 | */ 62 | public boolean isGroupExpanded(int flatPos) { 63 | ExpandableListPosition listPosition = expandableList.getUnflattenedPosition(flatPos); 64 | return expandableList.expandedGroupIndexes[listPosition.groupPos]; 65 | } 66 | 67 | /** 68 | * @param flatPos The flat list position of the group 69 | * @return false if the group is expanded, *after* the toggle, true if the group is now collapsed 70 | */ 71 | public boolean toggleGroup(int flatPos) { 72 | ExpandableListPosition listPos = expandableList.getUnflattenedPosition(flatPos); 73 | boolean expanded = expandableList.expandedGroupIndexes[listPos.groupPos]; 74 | if (expanded) { 75 | collapseGroup(listPos); 76 | } else { 77 | expandGroup(listPos); 78 | } 79 | return expanded; 80 | } 81 | 82 | public boolean toggleGroup(ExpandableGroup group) { 83 | ExpandableListPosition listPos = 84 | expandableList.getUnflattenedPosition(expandableList.getFlattenedGroupIndex(group)); 85 | boolean expanded = expandableList.expandedGroupIndexes[listPos.groupPos]; 86 | if (expanded) { 87 | collapseGroup(listPos); 88 | } else { 89 | expandGroup(listPos); 90 | } 91 | return expanded; 92 | } 93 | 94 | /** 95 | * @param group the {@link ExpandableGroup} being expanded 96 | */ 97 | void expandGroup(ExpandableGroup group) { 98 | ExpandableListPosition listPos = 99 | expandableList.getUnflattenedPosition(expandableList.getFlattenedGroupIndex(group)); 100 | boolean isExpanded = expandableList.expandedGroupIndexes[listPos.groupPos]; 101 | // No-op on repeating calls 102 | if (!isExpanded) { 103 | expandGroup(listPos); 104 | } 105 | } 106 | 107 | /** 108 | * @param flatPos The flat list position of the group 109 | */ 110 | void expandGroup(int flatPos) { 111 | ExpandableListPosition listPos = expandableList.getUnflattenedPosition(flatPos); 112 | boolean isExpanded = expandableList.expandedGroupIndexes[listPos.groupPos]; 113 | // No-op on repeating calls 114 | if (!isExpanded) { 115 | expandGroup(listPos); 116 | } 117 | } 118 | 119 | /** 120 | * @param group the {@link ExpandableGroup} being collapsed 121 | */ 122 | void collapseGroup(ExpandableGroup group) { 123 | ExpandableListPosition listPos = 124 | expandableList.getUnflattenedPosition(expandableList.getFlattenedGroupIndex(group)); 125 | boolean isExpanded = expandableList.expandedGroupIndexes[listPos.groupPos]; 126 | // No-op on repeating calls 127 | if (isExpanded) { 128 | collapseGroup(listPos); 129 | } 130 | } 131 | 132 | /** 133 | * @param flatPos The flat list position of the group 134 | */ 135 | void collapseGroup(int flatPos) { 136 | ExpandableListPosition listPos = expandableList.getUnflattenedPosition(flatPos); 137 | boolean isExpanded = expandableList.expandedGroupIndexes[listPos.groupPos]; 138 | // No-op on repeating calls 139 | if (isExpanded) { 140 | collapseGroup(listPos); 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /expandablerecyclerview/src/main/java/com/thoughtbot/expandablerecyclerview/ExpandableRecyclerViewAdapter.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.support.v7.widget.RecyclerView.ViewHolder; 7 | import android.view.ViewGroup; 8 | import com.thoughtbot.expandablerecyclerview.listeners.ExpandCollapseListener; 9 | import com.thoughtbot.expandablerecyclerview.listeners.GroupExpandCollapseListener; 10 | import com.thoughtbot.expandablerecyclerview.listeners.OnGroupClickListener; 11 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 12 | import com.thoughtbot.expandablerecyclerview.models.ExpandableList; 13 | import com.thoughtbot.expandablerecyclerview.models.ExpandableListPosition; 14 | import com.thoughtbot.expandablerecyclerview.viewholders.ChildViewHolder; 15 | import com.thoughtbot.expandablerecyclerview.viewholders.GroupViewHolder; 16 | import java.util.List; 17 | 18 | public abstract class ExpandableRecyclerViewAdapter 19 | extends RecyclerView.Adapter implements ExpandCollapseListener, OnGroupClickListener { 20 | 21 | private static final String EXPAND_STATE_MAP = "expandable_recyclerview_adapter_expand_state_map"; 22 | 23 | protected ExpandableList expandableList; 24 | private ExpandCollapseController expandCollapseController; 25 | 26 | private OnGroupClickListener groupClickListener; 27 | private GroupExpandCollapseListener expandCollapseListener; 28 | 29 | public ExpandableRecyclerViewAdapter(List groups) { 30 | this.expandableList = new ExpandableList(groups); 31 | this.expandCollapseController = new ExpandCollapseController(expandableList, this); 32 | } 33 | 34 | /** 35 | * Implementation of Adapter.onCreateViewHolder(ViewGroup, int) 36 | * that determines if the list item is a group or a child and calls through 37 | * to the appropriate implementation of either {@link #onCreateGroupViewHolder(ViewGroup, int)} 38 | * or {@link #onCreateChildViewHolder(ViewGroup, int)}}. 39 | * 40 | * @param parent The {@link ViewGroup} into which the new {@link android.view.View} 41 | * will be added after it is bound to an adapter position. 42 | * @param viewType The view type of the new {@code android.view.View}. 43 | * @return Either a new {@link GroupViewHolder} or a new {@link ChildViewHolder} 44 | * that holds a {@code android.view.View} of the given view type. 45 | */ 46 | @Override 47 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 48 | switch (viewType) { 49 | case ExpandableListPosition.GROUP: 50 | GVH gvh = onCreateGroupViewHolder(parent, viewType); 51 | gvh.setOnGroupClickListener(this); 52 | return gvh; 53 | case ExpandableListPosition.CHILD: 54 | CVH cvh = onCreateChildViewHolder(parent, viewType); 55 | return cvh; 56 | default: 57 | throw new IllegalArgumentException("viewType is not valid"); 58 | } 59 | } 60 | 61 | /** 62 | * Implementation of Adapter.onBindViewHolder(RecyclerView.ViewHolder, int) 63 | * that determines if the list item is a group or a child and calls through 64 | * to the appropriate implementation of either {@link #onBindGroupViewHolder(GroupViewHolder, 65 | * int, 66 | * ExpandableGroup)} 67 | * or {@link #onBindChildViewHolder(ChildViewHolder, int, ExpandableGroup, int)}. 68 | * 69 | * @param holder Either the GroupViewHolder or the ChildViewHolder to bind data to 70 | * @param position The flat position (or index in the list of {@link 71 | * ExpandableList#getVisibleItemCount()} in the list at which to bind 72 | */ 73 | @Override 74 | public void onBindViewHolder(ViewHolder holder, int position) { 75 | ExpandableListPosition listPos = expandableList.getUnflattenedPosition(position); 76 | ExpandableGroup group = expandableList.getExpandableGroup(listPos); 77 | switch (listPos.type) { 78 | case ExpandableListPosition.GROUP: 79 | onBindGroupViewHolder((GVH) holder, position, group); 80 | 81 | if (isGroupExpanded(group)) { 82 | ((GVH) holder).expand(); 83 | } else { 84 | ((GVH) holder).collapse(); 85 | } 86 | break; 87 | case ExpandableListPosition.CHILD: 88 | onBindChildViewHolder((CVH) holder, position, group, listPos.childPos); 89 | break; 90 | } 91 | } 92 | 93 | /** 94 | * @return the number of group and child objects currently expanded 95 | * @see ExpandableList#getVisibleItemCount() 96 | */ 97 | @Override 98 | public int getItemCount() { 99 | return expandableList.getVisibleItemCount(); 100 | } 101 | 102 | /** 103 | * Gets the view type of the item at the given position. 104 | * 105 | * @param position The flat position in the list to get the view type of 106 | * @return {@value ExpandableListPosition#CHILD} or {@value ExpandableListPosition#GROUP} 107 | * @throws RuntimeException if the item at the given position in the list is not found 108 | */ 109 | @Override 110 | public int getItemViewType(int position) { 111 | return expandableList.getUnflattenedPosition(position).type; 112 | } 113 | 114 | /** 115 | * Called when a group is expanded 116 | * 117 | * @param positionStart the flat position of the first child in the {@link ExpandableGroup} 118 | * @param itemCount the total number of children in the {@link ExpandableGroup} 119 | */ 120 | @Override 121 | public void onGroupExpanded(int positionStart, int itemCount) { 122 | //update header 123 | int headerPosition = positionStart - 1; 124 | notifyItemChanged(headerPosition); 125 | 126 | // only insert if there items to insert 127 | if (itemCount > 0) { 128 | notifyItemRangeInserted(positionStart, itemCount); 129 | if (expandCollapseListener != null) { 130 | int groupIndex = expandableList.getUnflattenedPosition(positionStart).groupPos; 131 | expandCollapseListener.onGroupExpanded(getGroups().get(groupIndex)); 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * Called when a group is collapsed 138 | * 139 | * @param positionStart the flat position of the first child in the {@link ExpandableGroup} 140 | * @param itemCount the total number of children in the {@link ExpandableGroup} 141 | */ 142 | @Override 143 | public void onGroupCollapsed(int positionStart, int itemCount) { 144 | //update header 145 | int headerPosition = positionStart - 1; 146 | notifyItemChanged(headerPosition); 147 | 148 | // only remote if there items to remove 149 | if (itemCount > 0) { 150 | notifyItemRangeRemoved(positionStart, itemCount); 151 | if (expandCollapseListener != null) { 152 | //minus one to return the position of the header, not first child 153 | int groupIndex = expandableList.getUnflattenedPosition(positionStart - 1).groupPos; 154 | expandCollapseListener.onGroupCollapsed(getGroups().get(groupIndex)); 155 | } 156 | } 157 | } 158 | 159 | /** 160 | * Triggered by a click on a {@link GroupViewHolder} 161 | * 162 | * @param flatPos the flat position of the {@link GroupViewHolder} that was clicked 163 | * @return false if click expanded group, true if click collapsed group 164 | */ 165 | @Override 166 | public boolean onGroupClick(int flatPos) { 167 | if (groupClickListener != null) { 168 | groupClickListener.onGroupClick(flatPos); 169 | } 170 | return expandCollapseController.toggleGroup(flatPos); 171 | } 172 | 173 | /** 174 | * @param flatPos The flat list position of the group 175 | * @return true if the group is expanded, *after* the toggle, false if the group is now collapsed 176 | */ 177 | public boolean toggleGroup(int flatPos) { 178 | return expandCollapseController.toggleGroup(flatPos); 179 | } 180 | 181 | /** 182 | * @param group the {@link ExpandableGroup} being toggled 183 | * @return true if the group is expanded, *after* the toggle, false if the group is now collapsed 184 | */ 185 | public boolean toggleGroup(ExpandableGroup group) { 186 | return expandCollapseController.toggleGroup(group); 187 | } 188 | 189 | /** 190 | * Explicitly expand a group. Expanding already expandend groups does nothing. 191 | * 192 | * @param group the {@link ExpandableGroup} being expanded 193 | */ 194 | public void expandGroup(ExpandableGroup group) { 195 | expandCollapseController.expandGroup(group); 196 | } 197 | 198 | /** 199 | * Explicitly collapse a group. Collapsing already collapsed groups does nothing. 200 | * 201 | * @param group the {@link ExpandableGroup} being expanded 202 | */ 203 | public void collapseGroup(ExpandableGroup group) { 204 | expandCollapseController.collapseGroup(group); 205 | } 206 | 207 | /** 208 | * @param flatPos the flattened position of an item in the list 209 | * @return true if {@code group} is expanded, false if it is collapsed 210 | */ 211 | public boolean isGroupExpanded(int flatPos) { 212 | return expandCollapseController.isGroupExpanded(flatPos); 213 | } 214 | 215 | /** 216 | * @param group the {@link ExpandableGroup} being checked for its collapsed state 217 | * @return true if {@code group} is expanded, false if it is collapsed 218 | */ 219 | public boolean isGroupExpanded(ExpandableGroup group) { 220 | return expandCollapseController.isGroupExpanded(group); 221 | } 222 | 223 | /** 224 | * Stores the expanded state map across state loss. 225 | *

226 | * Should be called from whatever {@link Activity} that hosts the RecyclerView that {@link 227 | * ExpandableRecyclerViewAdapter} is attached to. 228 | *

229 | * This will make sure to add the expanded state map as an extra to the 230 | * instance state bundle to be used in {@link #onRestoreInstanceState(Bundle)}. 231 | * 232 | * @param savedInstanceState The {@code Bundle} into which to store the 233 | * expanded state map 234 | */ 235 | public void onSaveInstanceState(Bundle savedInstanceState) { 236 | savedInstanceState.putBooleanArray(EXPAND_STATE_MAP, expandableList.expandedGroupIndexes); 237 | } 238 | 239 | /** 240 | * Fetches the expandable state map from the saved instance state {@link Bundle} 241 | * and restores the expanded states of all of the list items. 242 | *

243 | * Should be called from {@link Activity#onRestoreInstanceState(Bundle)} in 244 | * the {@link Activity} that hosts the RecyclerView that this 245 | * {@link ExpandableRecyclerViewAdapter} is attached to. 246 | *

247 | * 248 | * @param savedInstanceState The {@code Bundle} from which the expanded 249 | * state map is loaded 250 | */ 251 | public void onRestoreInstanceState(Bundle savedInstanceState) { 252 | if (savedInstanceState == null || !savedInstanceState.containsKey(EXPAND_STATE_MAP)) { 253 | return; 254 | } 255 | expandableList.expandedGroupIndexes = savedInstanceState.getBooleanArray(EXPAND_STATE_MAP); 256 | notifyDataSetChanged(); 257 | } 258 | 259 | public void setOnGroupClickListener(OnGroupClickListener listener) { 260 | groupClickListener = listener; 261 | } 262 | 263 | public void setOnGroupExpandCollapseListener(GroupExpandCollapseListener listener) { 264 | expandCollapseListener = listener; 265 | } 266 | 267 | /** 268 | * The full list of {@link ExpandableGroup} backing this RecyclerView 269 | * 270 | * @return the list of {@link ExpandableGroup} that this object was instantiated with 271 | */ 272 | public List getGroups() { 273 | return expandableList.groups; 274 | } 275 | 276 | /** 277 | * Called from {@link #onCreateViewHolder(ViewGroup, int)} when the list item created is a group 278 | * 279 | * @param viewType an int returned by {@link ExpandableRecyclerViewAdapter#getItemViewType(int)} 280 | * @param parent the {@link ViewGroup} in the list for which a {@link GVH} is being created 281 | * @return A {@link GVH} corresponding to the group list item with the {@code ViewGroup} parent 282 | */ 283 | public abstract GVH onCreateGroupViewHolder(ViewGroup parent, int viewType); 284 | 285 | /** 286 | * Called from {@link #onCreateViewHolder(ViewGroup, int)} when the list item created is a child 287 | * 288 | * @param viewType an int returned by {@link ExpandableRecyclerViewAdapter#getItemViewType(int)} 289 | * @param parent the {@link ViewGroup} in the list for which a {@link CVH} is being created 290 | * @return A {@link CVH} corresponding to child list item with the {@code ViewGroup} parent 291 | */ 292 | public abstract CVH onCreateChildViewHolder(ViewGroup parent, int viewType); 293 | 294 | /** 295 | * Called from onBindViewHolder(RecyclerView.ViewHolder, int) when the list item 296 | * bound to is a child. 297 | *

298 | * Bind data to the {@link CVH} here. 299 | * 300 | * @param holder The {@code CVH} to bind data to 301 | * @param flatPosition the flat position (raw index) in the list at which to bind the child 302 | * @param group The {@link ExpandableGroup} that the the child list item belongs to 303 | * @param childIndex the index of this child within it's {@link ExpandableGroup} 304 | */ 305 | public abstract void onBindChildViewHolder(CVH holder, int flatPosition, ExpandableGroup group, 306 | int childIndex); 307 | 308 | /** 309 | * Called from onBindViewHolder(RecyclerView.ViewHolder, int) when the list item bound to is a 310 | * group 311 | *

312 | * Bind data to the {@link GVH} here. 313 | * 314 | * @param holder The {@code GVH} to bind data to 315 | * @param flatPosition the flat position (raw index) in the list at which to bind the group 316 | * @param group The {@link ExpandableGroup} to be used to bind data to this {@link GVH} 317 | */ 318 | public abstract void onBindGroupViewHolder(GVH holder, int flatPosition, ExpandableGroup group); 319 | } 320 | -------------------------------------------------------------------------------- /expandablerecyclerview/src/main/java/com/thoughtbot/expandablerecyclerview/MultiTypeExpandableRecyclerViewAdapter.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview; 2 | 3 | import android.support.v7.widget.RecyclerView.ViewHolder; 4 | import android.view.ViewGroup; 5 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 6 | import com.thoughtbot.expandablerecyclerview.models.ExpandableList; 7 | import com.thoughtbot.expandablerecyclerview.models.ExpandableListPosition; 8 | import com.thoughtbot.expandablerecyclerview.viewholders.ChildViewHolder; 9 | import com.thoughtbot.expandablerecyclerview.viewholders.GroupViewHolder; 10 | import java.util.List; 11 | 12 | public abstract class MultiTypeExpandableRecyclerViewAdapter 13 | extends ExpandableRecyclerViewAdapter { 14 | 15 | public MultiTypeExpandableRecyclerViewAdapter(List groups) { 16 | super(groups); 17 | } 18 | 19 | /** 20 | * Implementation of RecyclerView.Adapter.onCreateViewHolder(ViewGroup, int) 21 | * that determines if the list item is a group or a child and calls through 22 | * to the appropriate implementation of either {@link #onCreateGroupViewHolder(ViewGroup, int)} 23 | * or {@link #onCreateChildViewHolder(ViewGroup, int)}}. 24 | * 25 | * @param parent The {@link ViewGroup} into which the new {@link android.view.View} 26 | * will be added after it is bound to an adapter position. 27 | * @param viewType The view type of the new {@code android.view.View}. 28 | * @return Either a new {@link GroupViewHolder} or a new {@link ChildViewHolder} 29 | * that holds a {@code android.view.View} of the given view type. 30 | */ 31 | @Override 32 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 33 | if (isGroup(viewType)) { 34 | GVH gvh = onCreateGroupViewHolder(parent, viewType); 35 | gvh.setOnGroupClickListener(this); 36 | return gvh; 37 | } else if (isChild(viewType)) { 38 | CVH cvh = onCreateChildViewHolder(parent, viewType); 39 | return cvh; 40 | } 41 | throw new IllegalArgumentException("viewType is not valid"); 42 | } 43 | 44 | /** 45 | * Implementation of Adapter.onBindViewHolder(RecyclerView.ViewHolder, int) 46 | * that determines if the list item is a group or a child and calls through 47 | * to the appropriate implementation of either {@link #onBindGroupViewHolder(GroupViewHolder, 48 | * int, 49 | * ExpandableGroup)} 50 | * or {@link #onBindChildViewHolder(ChildViewHolder, int, ExpandableGroup, int)}. 51 | * 52 | * @param holder Either the GroupViewHolder or the ChildViewHolder to bind data to 53 | * @param position The flat position (or index in the list of {@link 54 | * ExpandableList#getVisibleItemCount()} in the list at which to bind 55 | */ 56 | @Override 57 | public void onBindViewHolder(ViewHolder holder, int position) { 58 | ExpandableListPosition listPos = expandableList.getUnflattenedPosition(position); 59 | ExpandableGroup group = expandableList.getExpandableGroup(listPos); 60 | if (isGroup(getItemViewType(position))) { 61 | onBindGroupViewHolder((GVH) holder, position, group); 62 | 63 | if (isGroupExpanded(group)) { 64 | ((GVH) holder).expand(); 65 | } else { 66 | ((GVH) holder).collapse(); 67 | } 68 | } else if (isChild(getItemViewType(position))) { 69 | onBindChildViewHolder((CVH) holder, position, group, listPos.childPos); 70 | } 71 | } 72 | 73 | /** 74 | * Gets the view type of the item at the given position. 75 | * 76 | * @param position The flat position in the list to get the view type of 77 | * @return if the flat position corresponds to a child item, this will return the value returned 78 | * by {@code getChildViewType}. if the flat position refers to a group item this will return the 79 | * value returned by {@code getGroupViewType} 80 | */ 81 | @Override 82 | public int getItemViewType(int position) { 83 | ExpandableListPosition listPosition = expandableList.getUnflattenedPosition(position); 84 | ExpandableGroup group = expandableList.getExpandableGroup(listPosition); 85 | 86 | int viewType = listPosition.type; 87 | switch (viewType) { 88 | case ExpandableListPosition.GROUP: 89 | return getGroupViewType(position, group); 90 | case ExpandableListPosition.CHILD: 91 | return getChildViewType(position, group, listPosition.childPos); 92 | default: 93 | return viewType; 94 | } 95 | } 96 | 97 | /** 98 | * Used to allow subclasses to have multiple view types for children 99 | * 100 | * @param position the flat position in the list 101 | * @param group the group that this child belongs to 102 | * @param childIndex the index of the child within the group 103 | * @return any int representing the viewType for a child within the {@code group} *EXCEPT* 104 | * for {@link ExpandableListPosition#CHILD} and {@link ExpandableListPosition#GROUP}. 105 | * 106 | * If you do *not* override this method, the default viewType for a group is {@link 107 | * ExpandableListPosition#CHILD} 108 | * 109 | *

110 | * A subclass may use any number *EXCEPT* for {@link ExpandableListPosition#CHILD} and {@link 111 | * ExpandableListPosition#GROUP} as those are already being used by the adapter 112 | *

113 | */ 114 | public int getChildViewType(int position, ExpandableGroup group, int childIndex) { 115 | return super.getItemViewType(position); 116 | } 117 | 118 | /** 119 | * Used to allow subclasses to have multiple view types for groups 120 | * 121 | * @param position the flat position in the list 122 | * @param group the group at this position 123 | * @return any int representing the viewType for this {@code group} *EXCEPT* 124 | * for {@link ExpandableListPosition#CHILD} and {@link ExpandableListPosition#GROUP}. 125 | * 126 | * If you do not override this method, the default viewType for a group is {@link 127 | * ExpandableListPosition#GROUP} 128 | * 129 | *

130 | * A subclass may use any number *EXCEPT* for {@link ExpandableListPosition#CHILD} and {@link 131 | * ExpandableListPosition#GROUP} as those are already being used by the adapter 132 | *

133 | */ 134 | public int getGroupViewType(int position, ExpandableGroup group) { 135 | return super.getItemViewType(position); 136 | } 137 | 138 | /** 139 | * @param viewType the int corresponding to the viewType of a {@code ExpandableGroup} 140 | * @return if a subclasses has *NOT* overridden {@code getGroupViewType} than the viewType for 141 | * the group is defaulted to {@link ExpandableListPosition#GROUP} 142 | */ 143 | public boolean isGroup(int viewType) { 144 | return viewType == ExpandableListPosition.GROUP; 145 | } 146 | 147 | /** 148 | * @param viewType the int corresponding to the viewType of a child of a {@code ExpandableGroup} 149 | * @return if a subclasses has *NOT* overridden {@code getChildViewType} than the viewType for 150 | * the child is defaulted to {@link ExpandableListPosition#CHILD} 151 | */ 152 | public boolean isChild(int viewType) { 153 | return viewType == ExpandableListPosition.CHILD; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /expandablerecyclerview/src/main/java/com/thoughtbot/expandablerecyclerview/listeners/ExpandCollapseListener.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.listeners; 2 | 3 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 4 | 5 | public interface ExpandCollapseListener { 6 | 7 | /** 8 | * Called when a group is expanded 9 | * 10 | * @param positionStart the flat position of the first child in the {@link ExpandableGroup} 11 | * @param itemCount the total number of children in the {@link ExpandableGroup} 12 | */ 13 | void onGroupExpanded(int positionStart, int itemCount); 14 | 15 | /** 16 | * Called when a group is collapsed 17 | * 18 | * @param positionStart the flat position of the first child in the {@link ExpandableGroup} 19 | * @param itemCount the total number of children in the {@link ExpandableGroup} 20 | */ 21 | void onGroupCollapsed(int positionStart, int itemCount); 22 | } 23 | -------------------------------------------------------------------------------- /expandablerecyclerview/src/main/java/com/thoughtbot/expandablerecyclerview/listeners/GroupExpandCollapseListener.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.listeners; 2 | 3 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 4 | 5 | public interface GroupExpandCollapseListener { 6 | 7 | /** 8 | * Called when a group is expanded 9 | * @param group the {@link ExpandableGroup} being expanded 10 | */ 11 | void onGroupExpanded(ExpandableGroup group); 12 | 13 | /** 14 | * Called when a group is collapsed 15 | * @param group the {@link ExpandableGroup} being collapsed 16 | */ 17 | void onGroupCollapsed(ExpandableGroup group); 18 | } 19 | -------------------------------------------------------------------------------- /expandablerecyclerview/src/main/java/com/thoughtbot/expandablerecyclerview/listeners/OnGroupClickListener.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.listeners; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import com.thoughtbot.expandablerecyclerview.viewholders.GroupViewHolder; 5 | 6 | public interface OnGroupClickListener { 7 | 8 | /** 9 | * @param flatPos the flat position (raw index within the list of visible items in the 10 | * RecyclerView of a GroupViewHolder) 11 | * @return false if click expanded group, true if click collapsed group 12 | */ 13 | boolean onGroupClick(int flatPos); 14 | } -------------------------------------------------------------------------------- /expandablerecyclerview/src/main/java/com/thoughtbot/expandablerecyclerview/models/ExpandableGroup.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.models; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * The backing data object for an {@link ExpandableGroup} 10 | */ 11 | public class ExpandableGroup implements Parcelable { 12 | private String title; 13 | private List items; 14 | 15 | public ExpandableGroup(String title, List items) { 16 | this.title = title; 17 | this.items = items; 18 | } 19 | 20 | public String getTitle() { 21 | return title; 22 | } 23 | 24 | public List getItems() { 25 | return items; 26 | } 27 | 28 | public int getItemCount() { 29 | return items == null ? 0 : items.size(); 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "ExpandableGroup{" + 35 | "title='" + title + '\'' + 36 | ", items=" + items + 37 | '}'; 38 | } 39 | 40 | protected ExpandableGroup(Parcel in) { 41 | title = in.readString(); 42 | byte hasItems = in.readByte(); 43 | int size = in.readInt(); 44 | if (hasItems == 0x01) { 45 | items = new ArrayList(size); 46 | Class type = (Class) in.readSerializable(); 47 | in.readList(items, type.getClassLoader()); 48 | } else { 49 | items = null; 50 | } 51 | } 52 | 53 | @Override 54 | public int describeContents() { 55 | return 0; 56 | } 57 | 58 | @Override 59 | public void writeToParcel(Parcel dest, int flags) { 60 | dest.writeString(title); 61 | if (items == null) { 62 | dest.writeByte((byte) (0x00)); 63 | dest.writeInt(0); 64 | } else { 65 | dest.writeByte((byte) (0x01)); 66 | dest.writeInt(items.size()); 67 | if (items.size() > 0) { 68 | final Class objectsType = items.get(0).getClass(); 69 | dest.writeSerializable(objectsType); 70 | } 71 | dest.writeList(items); 72 | } 73 | } 74 | 75 | @SuppressWarnings("unused") 76 | public static final Creator CREATOR = 77 | new Creator() { 78 | @Override 79 | public ExpandableGroup createFromParcel(Parcel in) { 80 | return new ExpandableGroup(in); 81 | } 82 | 83 | @Override 84 | public ExpandableGroup[] newArray(int size) { 85 | return new ExpandableGroup[size]; 86 | } 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /expandablerecyclerview/src/main/java/com/thoughtbot/expandablerecyclerview/models/ExpandableList.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.models; 2 | 3 | import java.util.List; 4 | 5 | /* 6 | * Terminology: 7 | *
  • flat position - Flat list position, the position of an item relative to all the 8 | * other *visible* items on the screen. For example, if you have a three groups, each with 9 | * 2 children and all are collapsed, the "flat position" of the last group would be 2. And if 10 | * the first of those three groups was expanded, the flat position of the last group would now be 4. 11 | * 12 | * 13 | * This class acts as a translator between the flat list position - i.e. what groups 14 | * and children you see on the screen - to and from the full backing list of groups & their children 15 | */ 16 | public class ExpandableList { 17 | 18 | public List groups; 19 | public boolean[] expandedGroupIndexes; 20 | 21 | public ExpandableList(List groups) { 22 | this.groups = groups; 23 | 24 | expandedGroupIndexes = new boolean[groups.size()]; 25 | for (int i = 0; i < groups.size(); i++) { 26 | expandedGroupIndexes[i] = false; 27 | } 28 | } 29 | 30 | /** 31 | * @param group the index of the {@link ExpandableGroup} in the full collection {@link #groups} 32 | * @return the number of visible row items for the particular group. If the group is collapsed, 33 | * return 1 for the group header. If the group is expanded return total number of children in the 34 | * group + 1 for the group header 35 | */ 36 | private int numberOfVisibleItemsInGroup(int group) { 37 | if (expandedGroupIndexes[group]) { 38 | return groups.get(group).getItemCount() + 1; 39 | } else { 40 | return 1; 41 | } 42 | } 43 | 44 | /** 45 | * @return the total number visible rows 46 | */ 47 | public int getVisibleItemCount() { 48 | int count = 0; 49 | for (int i = 0; i < groups.size(); i++) { 50 | count += numberOfVisibleItemsInGroup(i); 51 | } 52 | return count; 53 | } 54 | 55 | /** 56 | * Translates a flat list position (the raw position of an item (child or group) in the list) to 57 | * either a) group pos if the specified flat list position corresponds to a group, or b) child 58 | * pos if it corresponds to a child. Performs a binary search on the expanded 59 | * groups list to find the flat list pos if it is an exp group, otherwise 60 | * finds where the flat list pos fits in between the exp groups. 61 | * 62 | * @param flPos the flat list position to be translated 63 | * @return the group position or child position of the specified flat list 64 | * position encompassed in a {@link ExpandableListPosition} object 65 | * that contains additional useful info for insertion, etc. 66 | */ 67 | public ExpandableListPosition getUnflattenedPosition(int flPos) { 68 | int groupItemCount; 69 | int adapted = flPos; 70 | for (int i = 0; i < groups.size(); i++) { 71 | groupItemCount = numberOfVisibleItemsInGroup(i); 72 | if (adapted == 0) { 73 | return ExpandableListPosition.obtain(ExpandableListPosition.GROUP, i, -1, flPos); 74 | } else if (adapted < groupItemCount) { 75 | return ExpandableListPosition.obtain(ExpandableListPosition.CHILD, i, adapted - 1, flPos); 76 | } 77 | adapted -= groupItemCount; 78 | } 79 | throw new RuntimeException("Unknown state"); 80 | } 81 | 82 | /** 83 | * @param listPosition representing either a child or a group 84 | * @return the index of a group within the {@link #getVisibleItemCount()} 85 | */ 86 | public int getFlattenedGroupIndex(ExpandableListPosition listPosition) { 87 | int groupIndex = listPosition.groupPos; 88 | int runningTotal = 0; 89 | 90 | for (int i = 0; i < groupIndex; i++) { 91 | runningTotal += numberOfVisibleItemsInGroup(i); 92 | } 93 | return runningTotal; 94 | } 95 | 96 | /** 97 | * @param groupIndex representing the index of a group within {@link #groups} 98 | * @return the index of a group within the {@link #getVisibleItemCount()} 99 | */ 100 | public int getFlattenedGroupIndex(int groupIndex) { 101 | int runningTotal = 0; 102 | 103 | for (int i = 0; i < groupIndex; i++) { 104 | runningTotal += numberOfVisibleItemsInGroup(i); 105 | } 106 | return runningTotal; 107 | } 108 | 109 | /** 110 | * @param group an {@link ExpandableGroup} within {@link #groups} 111 | * @return the index of a group within the {@link #getVisibleItemCount()} or 0 if the 112 | * groups.indexOf cannot find the group 113 | */ 114 | public int getFlattenedGroupIndex(ExpandableGroup group) { 115 | int groupIndex = groups.indexOf(group); 116 | int runningTotal = 0; 117 | 118 | for (int i = 0; i < groupIndex; i++) { 119 | runningTotal += numberOfVisibleItemsInGroup(i); 120 | } 121 | return runningTotal; 122 | } 123 | 124 | /** 125 | * Converts a child position to a flat list position. 126 | * 127 | * @param packedPosition The child positions to be converted in it's 128 | * packed position representation. 129 | * @return The flat list position for the given child 130 | */ 131 | public int getFlattenedChildIndex(long packedPosition) { 132 | ExpandableListPosition listPosition = ExpandableListPosition.obtainPosition(packedPosition); 133 | return getFlattenedChildIndex(listPosition); 134 | } 135 | 136 | /** 137 | * Converts a child position to a flat list position. 138 | * 139 | * @param listPosition The child positions to be converted in it's 140 | * {@link ExpandableListPosition} representation. 141 | * @return The flat list position for the given child 142 | */ 143 | public int getFlattenedChildIndex(ExpandableListPosition listPosition) { 144 | int groupIndex = listPosition.groupPos; 145 | int childIndex = listPosition.childPos; 146 | int runningTotal = 0; 147 | 148 | for (int i = 0; i < groupIndex; i++) { 149 | runningTotal += numberOfVisibleItemsInGroup(i); 150 | } 151 | return runningTotal + childIndex + 1; 152 | } 153 | 154 | /** 155 | * Converts the details of a child's position to a flat list position. 156 | * 157 | * @param groupIndex The index of a group within {@link #groups} 158 | * @param childIndex the index of a child within it's {@link ExpandableGroup} 159 | * @return The flat list position for the given child 160 | */ 161 | public int getFlattenedChildIndex(int groupIndex, int childIndex) { 162 | int runningTotal = 0; 163 | 164 | for (int i = 0; i < groupIndex; i++) { 165 | runningTotal += numberOfVisibleItemsInGroup(i); 166 | } 167 | return runningTotal + childIndex + 1; 168 | } 169 | 170 | /** 171 | * @param groupIndex The index of a group within {@link #groups} 172 | * @return The flat list position for the first child in a group 173 | */ 174 | public int getFlattenedFirstChildIndex(int groupIndex) { 175 | return getFlattenedGroupIndex(groupIndex) + 1; 176 | } 177 | 178 | /** 179 | * @param listPosition The child positions to be converted in it's 180 | * {@link ExpandableListPosition} representation. 181 | * @return The flat list position for the first child in a group 182 | */ 183 | public int getFlattenedFirstChildIndex(ExpandableListPosition listPosition) { 184 | return getFlattenedGroupIndex(listPosition) + 1; 185 | } 186 | 187 | /** 188 | * @param listPosition An {@link ExpandableListPosition} representing either a child or group 189 | * @return the total number of children within the group associated with the @param listPosition 190 | */ 191 | public int getExpandableGroupItemCount(ExpandableListPosition listPosition) { 192 | return groups.get(listPosition.groupPos).getItemCount(); 193 | } 194 | 195 | /** 196 | * Translates either a group pos or a child pos to an {@link ExpandableGroup}. 197 | * If the {@link ExpandableListPosition} is a child position, it returns the {@link 198 | * ExpandableGroup} it belongs to 199 | * 200 | * @param listPosition a {@link ExpandableListPosition} representing either a group position 201 | * or child position 202 | * @return the {@link ExpandableGroup} object that contains the listPosition 203 | */ 204 | public ExpandableGroup getExpandableGroup(ExpandableListPosition listPosition) { 205 | return groups.get(listPosition.groupPos); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /expandablerecyclerview/src/main/java/com/thoughtbot/expandablerecyclerview/models/ExpandableListPosition.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.models; 2 | 3 | import android.widget.ExpandableListView; 4 | import java.util.ArrayList; 5 | 6 | /** 7 | * Exact copy of android.widget.ExpandableListPosition because 8 | * android.widget.ExpandableListPosition has package local scope 9 | * 10 | * 11 | * ExpandableListPosition can refer to either a group's position or a child's 12 | * position. Referring to a child's position requires both a group position (the 13 | * group containing the child) and a child position (the child's position within 14 | * that group). To create objects, use {@link #obtainChildPosition(int, int)} or 15 | * {@link #obtainGroupPosition(int)}. 16 | */ 17 | 18 | public class ExpandableListPosition { 19 | 20 | private static final int MAX_POOL_SIZE = 5; 21 | private static ArrayList sPool = 22 | new ArrayList(MAX_POOL_SIZE); 23 | 24 | /** 25 | * This data type represents a child position 26 | */ 27 | public final static int CHILD = 1; 28 | 29 | /** 30 | * This data type represents a group position 31 | */ 32 | public final static int GROUP = 2; 33 | 34 | /** 35 | * The position of either the group being referred to, or the parent 36 | * group of the child being referred to 37 | */ 38 | public int groupPos; 39 | 40 | /** 41 | * The position of the child within its parent group 42 | */ 43 | public int childPos; 44 | 45 | /** 46 | * The position of the item in the flat list (optional, used internally when 47 | * the corresponding flat list position for the group or child is known) 48 | */ 49 | int flatListPos; 50 | 51 | /** 52 | * What type of position this ExpandableListPosition represents 53 | */ 54 | public int type; 55 | 56 | private void resetState() { 57 | groupPos = 0; 58 | childPos = 0; 59 | flatListPos = 0; 60 | type = 0; 61 | } 62 | 63 | private ExpandableListPosition() { 64 | } 65 | 66 | public long getPackedPosition() { 67 | if (type == CHILD) { 68 | return ExpandableListView.getPackedPositionForChild(groupPos, childPos); 69 | } else { 70 | return ExpandableListView.getPackedPositionForGroup(groupPos); 71 | } 72 | } 73 | 74 | static ExpandableListPosition obtainGroupPosition(int groupPosition) { 75 | return obtain(GROUP, groupPosition, 0, 0); 76 | } 77 | 78 | static ExpandableListPosition obtainChildPosition(int groupPosition, int childPosition) { 79 | return obtain(CHILD, groupPosition, childPosition, 0); 80 | } 81 | 82 | static ExpandableListPosition obtainPosition(long packedPosition) { 83 | if (packedPosition == ExpandableListView.PACKED_POSITION_VALUE_NULL) { 84 | return null; 85 | } 86 | 87 | ExpandableListPosition elp = getRecycledOrCreate(); 88 | elp.groupPos = ExpandableListView.getPackedPositionGroup(packedPosition); 89 | if (ExpandableListView.getPackedPositionType(packedPosition) == 90 | ExpandableListView.PACKED_POSITION_TYPE_CHILD) { 91 | elp.type = CHILD; 92 | elp.childPos = ExpandableListView.getPackedPositionChild(packedPosition); 93 | } else { 94 | elp.type = GROUP; 95 | } 96 | return elp; 97 | } 98 | 99 | public static ExpandableListPosition obtain(int type, int groupPos, int childPos, 100 | int flatListPos) { 101 | ExpandableListPosition elp = getRecycledOrCreate(); 102 | elp.type = type; 103 | elp.groupPos = groupPos; 104 | elp.childPos = childPos; 105 | elp.flatListPos = flatListPos; 106 | return elp; 107 | } 108 | 109 | private static ExpandableListPosition getRecycledOrCreate() { 110 | ExpandableListPosition elp; 111 | synchronized (sPool) { 112 | if (sPool.size() > 0) { 113 | elp = sPool.remove(0); 114 | } else { 115 | return new ExpandableListPosition(); 116 | } 117 | } 118 | elp.resetState(); 119 | return elp; 120 | } 121 | 122 | /** 123 | * Do not call this unless you obtained this via ExpandableListPosition.obtain(). 124 | * PositionMetadata will handle recycling its own children. 125 | */ 126 | public void recycle() { 127 | synchronized (sPool) { 128 | if (sPool.size() < MAX_POOL_SIZE) { 129 | sPool.add(this); 130 | } 131 | } 132 | } 133 | 134 | @Override 135 | public boolean equals(Object o) { 136 | if (this == o) return true; 137 | if (o == null || getClass() != o.getClass()) return false; 138 | 139 | ExpandableListPosition that = (ExpandableListPosition) o; 140 | 141 | if (groupPos != that.groupPos) return false; 142 | if (childPos != that.childPos) return false; 143 | if (flatListPos != that.flatListPos) return false; 144 | return type == that.type; 145 | 146 | } 147 | 148 | @Override 149 | public int hashCode() { 150 | int result = groupPos; 151 | result = 31 * result + childPos; 152 | result = 31 * result + flatListPos; 153 | result = 31 * result + type; 154 | return result; 155 | } 156 | 157 | @Override 158 | public String toString() { 159 | return "ExpandableListPosition{" + 160 | "groupPos=" + groupPos + 161 | ", childPos=" + childPos + 162 | ", flatListPos=" + flatListPos + 163 | ", type=" + type + 164 | '}'; 165 | } 166 | } 167 | 168 | -------------------------------------------------------------------------------- /expandablerecyclerview/src/main/java/com/thoughtbot/expandablerecyclerview/viewholders/ChildViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.viewholders; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.View; 5 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 6 | 7 | /** 8 | * ViewHolder for {@link ExpandableGroup#items} 9 | */ 10 | public class ChildViewHolder extends RecyclerView.ViewHolder { 11 | 12 | public ChildViewHolder(View itemView) { 13 | super(itemView); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /expandablerecyclerview/src/main/java/com/thoughtbot/expandablerecyclerview/viewholders/GroupViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.viewholders; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.View; 5 | import android.view.View.OnClickListener; 6 | import com.thoughtbot.expandablerecyclerview.listeners.OnGroupClickListener; 7 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 8 | 9 | /** 10 | * ViewHolder for the {@link ExpandableGroup#title} in a {@link ExpandableGroup} 11 | * 12 | * The current implementation does now allow for sub {@link View} of the parent view to trigger 13 | * a collapse / expand. *Only* click events on the parent {@link View} will trigger a collapse or 14 | * expand 15 | */ 16 | public abstract class GroupViewHolder extends RecyclerView.ViewHolder implements OnClickListener { 17 | 18 | private OnGroupClickListener listener; 19 | 20 | public GroupViewHolder(View itemView) { 21 | super(itemView); 22 | itemView.setOnClickListener(this); 23 | } 24 | 25 | @Override 26 | public void onClick(View v) { 27 | if (listener != null) { 28 | listener.onGroupClick(getAdapterPosition()); 29 | } 30 | } 31 | 32 | public void setOnGroupClickListener(OnGroupClickListener listener) { 33 | this.listener = listener; 34 | } 35 | 36 | public void expand() {} 37 | 38 | public void collapse() {} 39 | } 40 | -------------------------------------------------------------------------------- /expandablerecyclerview/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ExpandableRecyclerView 3 | 4 | -------------------------------------------------------------------------------- /expandablerecyclerview/src/test/java/com/thoughtbot/expandablerecyclerview/ExpandableListTest.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 6 | import com.thoughtbot.expandablerecyclerview.models.ExpandableList; 7 | import com.thoughtbot.expandablerecyclerview.models.ExpandableListPosition; 8 | import com.thoughtbot.expandablerecyclerview.testUtils.TestDataFactory; 9 | import java.util.List; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.MockitoAnnotations; 14 | import org.robolectric.RobolectricTestRunner; 15 | import org.robolectric.RuntimeEnvironment; 16 | import org.robolectric.annotation.Config; 17 | 18 | import static com.thoughtbot.expandablerecyclerview.models.ExpandableListPosition.CHILD; 19 | import static com.thoughtbot.expandablerecyclerview.models.ExpandableListPosition.GROUP; 20 | import static com.thoughtbot.expandablerecyclerview.models.ExpandableListPosition.obtain; 21 | import static junit.framework.Assert.assertEquals; 22 | import static junit.framework.Assert.assertNotNull; 23 | 24 | @RunWith(RobolectricTestRunner.class) 25 | @Config(constants = BuildConfig.class, sdk = 21) 26 | public class ExpandableListTest { 27 | 28 | private Context context; 29 | private List groups; 30 | 31 | @Before 32 | public void setUp() throws Exception { 33 | MockitoAnnotations.initMocks(this); 34 | Application application = RuntimeEnvironment.application; 35 | assertNotNull(application); 36 | 37 | context = application; 38 | groups = TestDataFactory.makeGroups(); 39 | } 40 | 41 | @Test 42 | public void test_getVisibleItemCount() { 43 | ExpandableList list = new ExpandableList(groups); 44 | 45 | //initial state 46 | int initialExpected = 6; 47 | int initialActual = list.getVisibleItemCount(); 48 | 49 | assertEquals(initialExpected, initialActual); 50 | 51 | //expand first group 52 | list.expandedGroupIndexes[0] = true; 53 | 54 | //new state 55 | int newExpected = 9; 56 | int newActual = list.getVisibleItemCount(); 57 | 58 | assertEquals(newExpected, newActual); 59 | } 60 | 61 | @Test 62 | public void test_getUnflattenedPosition() { 63 | ExpandableList list = new ExpandableList(groups); 64 | int flatPos = 3; 65 | 66 | //initial state 67 | //flatPos 3 == group at index 3 68 | ExpandableListPosition initialExpected = obtain(GROUP, 3, -1, 3); 69 | ExpandableListPosition initialActual = list.getUnflattenedPosition(flatPos); 70 | 71 | assertEquals(initialExpected, initialActual); 72 | 73 | //expand first group 74 | list.expandedGroupIndexes[0] = true; 75 | 76 | //flatPos 3 == child number 2 within group at index 0 77 | ExpandableListPosition newExpected = obtain(CHILD, 0, 2, 3); 78 | ExpandableListPosition newActual = list.getUnflattenedPosition(flatPos); 79 | 80 | assertEquals(newExpected, newActual); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /expandablerecyclerview/src/test/java/com/thoughtbot/expandablerecyclerview/testUtils/TestDataFactory.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.testUtils; 2 | 3 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | public class TestDataFactory { 9 | 10 | public static List makeGroups() { 11 | ArrayList list = new ArrayList(); 12 | for (int i = 0; i < 6; i++) { 13 | List items = Arrays.asList(i + ".0", i + ".1", i + ".2"); 14 | list.add(new ExpandableGroup("Section " + i, items)); 15 | } 16 | return list; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/expandable-recycler-view/56e45355c069ac602301da021691ea09ea7d0287/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Sep 28 11:33:07 EDT 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.14.1-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 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.thoughtbot.expandablerecyclerview.sample" 9 | minSdkVersion 16 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | compile project(':expandablecheckrecyclerview') 26 | 27 | //android 28 | compile 'com.android.support:appcompat-v7:23.4.0' 29 | compile 'com.android.support:recyclerview-v7:23.4.0' 30 | 31 | //unit tests 32 | testCompile 'junit:junit:4.12' 33 | testCompile 'org.robolectric:robolectric:3.2.1' 34 | testCompile 'org.mockito:mockito-core:1.10.5' 35 | 36 | //automation tests 37 | androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' 38 | androidTestCompile 'com.android.support.test:runner:0.5' 39 | androidTestCompile 'com.android.support:support-annotations:23.4.0' 40 | androidTestCompile 'com.android.support.test:rules:0.4.1' 41 | androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.2') { 42 | // Necessary to avoid version conflicts 43 | exclude group: 'com.android.support', module: 'appcompat' 44 | exclude group: 'com.android.support', module: 'support-v4' 45 | exclude group: 'com.android.support', module: 'support-annotations' 46 | exclude module: 'recyclerview-v7' 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sample/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 /Users/amanda/Library/Android/sdk/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 | -------------------------------------------------------------------------------- /sample/src/androidTest/java/com/thoughtbot/expandablerecyclerview/sample/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /sample/src/androidTest/java/com/thoughtbot/expandablerecyclerview/sample/ExpandActivityTest.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample; 2 | 3 | import android.support.test.espresso.contrib.RecyclerViewActions; 4 | import android.support.test.rule.ActivityTestRule; 5 | import android.support.test.runner.AndroidJUnit4; 6 | import android.support.v7.widget.RecyclerView; 7 | import com.thoughtbot.expandablerecyclerview.sample.expand.GenreAdapter; 8 | import com.thoughtbot.expandablerecyclerview.sample.expand.ExpandActivity; 9 | import org.junit.Before; 10 | import org.junit.Rule; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | 14 | import static android.support.test.espresso.Espresso.onView; 15 | import static android.support.test.espresso.action.ViewActions.click; 16 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 17 | import static junit.framework.Assert.assertTrue; 18 | 19 | @RunWith(AndroidJUnit4.class) 20 | public class ExpandActivityTest { 21 | 22 | @Rule 23 | public ActivityTestRule activityRule = 24 | new ActivityTestRule<>(ExpandActivity.class); 25 | 26 | private RecyclerView recyclerView; 27 | private GenreAdapter adapter; 28 | 29 | @Before 30 | public void setUp() { 31 | recyclerView = 32 | (RecyclerView) activityRule.getActivity().findViewById(R.id.recycler_view); 33 | 34 | adapter = activityRule.getActivity().adapter; 35 | } 36 | 37 | @Test 38 | public void testClickGroup() { 39 | onView(withId(R.id.recycler_view)) 40 | .perform(RecyclerViewActions.actionOnItemAtPosition(0, click())); 41 | 42 | assertTrue(adapter.isGroupExpanded(0)); 43 | } 44 | 45 | @Test 46 | public void testClickItem() { 47 | onView(withId(R.id.recycler_view)) 48 | .perform(RecyclerViewActions.actionOnItemAtPosition(1, click())); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 27 | 28 | 31 | 32 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/Artist.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | public class Artist implements Parcelable { 7 | 8 | private String name; 9 | private boolean isFavorite; 10 | 11 | public Artist(String name, boolean isFavorite) { 12 | this.name = name; 13 | this.isFavorite = isFavorite; 14 | } 15 | 16 | protected Artist(Parcel in) { 17 | name = in.readString(); 18 | } 19 | 20 | public String getName() { 21 | return name; 22 | } 23 | 24 | public boolean isFavorite() { 25 | return isFavorite; 26 | } 27 | 28 | @Override 29 | public boolean equals(Object o) { 30 | if (this == o) return true; 31 | if (!(o instanceof Artist)) return false; 32 | 33 | Artist artist = (Artist) o; 34 | 35 | if (isFavorite() != artist.isFavorite()) return false; 36 | return getName() != null ? getName().equals(artist.getName()) : artist.getName() == null; 37 | 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | int result = getName() != null ? getName().hashCode() : 0; 43 | result = 31 * result + (isFavorite() ? 1 : 0); 44 | return result; 45 | } 46 | 47 | @Override 48 | public void writeToParcel(Parcel dest, int flags) { 49 | dest.writeString(name); 50 | } 51 | 52 | @Override 53 | public int describeContents() { 54 | return 0; 55 | } 56 | 57 | public static final Creator CREATOR = new Creator() { 58 | @Override 59 | public Artist createFromParcel(Parcel in) { 60 | return new Artist(in); 61 | } 62 | 63 | @Override 64 | public Artist[] newArray(int size) { 65 | return new Artist[size]; 66 | } 67 | }; 68 | } 69 | 70 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/Genre.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample; 2 | 3 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 4 | import java.util.List; 5 | 6 | public class Genre extends ExpandableGroup { 7 | 8 | private int iconResId; 9 | 10 | public Genre(String title, List items, int iconResId) { 11 | super(title, items); 12 | this.iconResId = iconResId; 13 | } 14 | 15 | public int getIconResId() { 16 | return iconResId; 17 | } 18 | 19 | @Override 20 | public boolean equals(Object o) { 21 | if (this == o) return true; 22 | if (!(o instanceof Genre)) return false; 23 | 24 | Genre genre = (Genre) o; 25 | 26 | return getIconResId() == genre.getIconResId(); 27 | 28 | } 29 | 30 | @Override 31 | public int hashCode() { 32 | return getIconResId(); 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/GenreDataFactory.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample; 2 | 3 | import com.thoughtbot.expandablerecyclerview.sample.multicheck.MultiCheckGenre; 4 | import com.thoughtbot.expandablerecyclerview.sample.singlecheck.SingleCheckGenre; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | public class GenreDataFactory { 9 | 10 | public static List makeGenres() { 11 | return Arrays.asList(makeRockGenre(), 12 | makeJazzGenre(), 13 | makeClassicGenre(), 14 | makeSalsaGenre(), 15 | makeBluegrassGenre()); 16 | } 17 | 18 | public static List makeMultiCheckGenres() { 19 | return Arrays.asList(makeMultiCheckRockGenre(), 20 | makeMultiCheckJazzGenre(), 21 | makeMultiCheckClassicGenre(), 22 | makeMultiCheckSalsaGenre(), 23 | makeMulitCheckBluegrassGenre()); 24 | } 25 | 26 | public static List makeSingleCheckGenres() { 27 | return Arrays.asList(makeSingleCheckRockGenre(), 28 | makeSingleCheckJazzGenre(), 29 | makeSingleCheckClassicGenre(), 30 | makeSingleCheckSalsaGenre(), 31 | makeSingleCheckBluegrassGenre()); 32 | } 33 | 34 | public static Genre makeRockGenre() { 35 | return new Genre("Rock", makeRockArtists(), R.drawable.ic_electric_guitar); 36 | } 37 | 38 | public static MultiCheckGenre makeMultiCheckRockGenre() { 39 | return new MultiCheckGenre("Rock", makeRockArtists(), R.drawable.ic_electric_guitar); 40 | } 41 | 42 | public static SingleCheckGenre makeSingleCheckRockGenre() { 43 | return new SingleCheckGenre("Rock", makeRockArtists(), R.drawable.ic_electric_guitar); 44 | } 45 | 46 | public static List makeRockArtists() { 47 | Artist queen = new Artist("Queen", true); 48 | Artist styx = new Artist("Styx", false); 49 | Artist reoSpeedwagon = new Artist("REO Speedwagon", false); 50 | Artist boston = new Artist("Boston", true); 51 | 52 | return Arrays.asList(queen, styx, reoSpeedwagon, boston); 53 | } 54 | 55 | public static Genre makeJazzGenre() { 56 | return new Genre("Jazz", makeJazzArtists(), R.drawable.ic_saxaphone); 57 | } 58 | 59 | public static MultiCheckGenre makeMultiCheckJazzGenre() { 60 | return new MultiCheckGenre("Jazz", makeJazzArtists(), R.drawable.ic_saxaphone); 61 | } 62 | 63 | public static SingleCheckGenre makeSingleCheckJazzGenre() { 64 | return new SingleCheckGenre("Jazz", makeJazzArtists(), R.drawable.ic_saxaphone); 65 | } 66 | 67 | public static List makeJazzArtists() { 68 | Artist milesDavis = new Artist("Miles Davis", true); 69 | Artist ellaFitzgerald = new Artist("Ella Fitzgerald", true); 70 | Artist billieHoliday = new Artist("Billie Holiday", false); 71 | 72 | return Arrays.asList(milesDavis, ellaFitzgerald, billieHoliday); 73 | } 74 | 75 | public static Genre makeClassicGenre() { 76 | return new Genre("Classic", makeClassicArtists(), R.drawable.ic_violin); 77 | } 78 | 79 | public static MultiCheckGenre makeMultiCheckClassicGenre() { 80 | return new MultiCheckGenre("Classic", makeClassicArtists(), R.drawable.ic_violin); 81 | } 82 | 83 | public static SingleCheckGenre makeSingleCheckClassicGenre() { 84 | return new SingleCheckGenre("Classic", makeClassicArtists(), R.drawable.ic_violin); 85 | } 86 | 87 | public static List makeClassicArtists() { 88 | Artist beethoven = new Artist("Ludwig van Beethoven", false); 89 | Artist bach = new Artist("Johann Sebastian Bach", true); 90 | Artist brahms = new Artist("Johannes Brahms", false); 91 | Artist puccini = new Artist("Giacomo Puccini", false); 92 | 93 | return Arrays.asList(beethoven, bach, brahms, puccini); 94 | } 95 | 96 | public static Genre makeSalsaGenre() { 97 | return new Genre("Salsa", makeSalsaArtists(), R.drawable.ic_maracas); 98 | } 99 | 100 | public static MultiCheckGenre makeMultiCheckSalsaGenre() { 101 | return new MultiCheckGenre("Salsa", makeSalsaArtists(), R.drawable.ic_maracas); 102 | } 103 | 104 | public static SingleCheckGenre makeSingleCheckSalsaGenre() { 105 | return new SingleCheckGenre("Salsa", makeSalsaArtists(), R.drawable.ic_maracas); 106 | } 107 | 108 | public static List makeSalsaArtists() { 109 | Artist hectorLavoe = new Artist("Hector Lavoe", true); 110 | Artist celiaCruz = new Artist("Celia Cruz", false); 111 | Artist willieColon = new Artist("Willie Colon", false); 112 | Artist marcAnthony = new Artist("Marc Anthony", false); 113 | 114 | return Arrays.asList(hectorLavoe, celiaCruz, willieColon, marcAnthony); 115 | } 116 | 117 | public static Genre makeBluegrassGenre() { 118 | return new Genre("Bluegrass", makeBluegrassArtists(), R.drawable.ic_banjo); 119 | } 120 | 121 | public static MultiCheckGenre makeMulitCheckBluegrassGenre() { 122 | return new MultiCheckGenre("Bluegrass", makeBluegrassArtists(), R.drawable.ic_banjo); 123 | } 124 | 125 | public static SingleCheckGenre makeSingleCheckBluegrassGenre() { 126 | return new SingleCheckGenre("Bluegrass", makeBluegrassArtists(), R.drawable.ic_banjo); 127 | } 128 | 129 | public static List makeBluegrassArtists() { 130 | Artist billMonroe = new Artist("Bill Monroe", false); 131 | Artist earlScruggs = new Artist("Earl Scruggs", false); 132 | Artist osborneBrothers = new Artist("Osborne Brothers", true); 133 | Artist johnHartford = new Artist("John Hartford", false); 134 | 135 | return Arrays.asList(billMonroe, earlScruggs, osborneBrothers, johnHartford); 136 | } 137 | 138 | } 139 | 140 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.view.View.OnClickListener; 8 | import android.widget.Button; 9 | import com.thoughtbot.expandablerecyclerview.sample.expand.ExpandActivity; 10 | import com.thoughtbot.expandablerecyclerview.sample.multicheck.MultiCheckActivity; 11 | import com.thoughtbot.expandablerecyclerview.sample.multitype.MultiTypeActivity; 12 | import com.thoughtbot.expandablerecyclerview.sample.multitypeandcheck.MultiTypeCheckGenreActivity; 13 | import com.thoughtbot.expandablerecyclerview.sample.singlecheck.SingleCheckActivity; 14 | 15 | public class MainActivity extends Activity { 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_main); 21 | 22 | Button expand = (Button) findViewById(R.id.expand_button); 23 | expand.setOnClickListener(navigateTo(ExpandActivity.class)); 24 | 25 | Button multiSelect = (Button) findViewById(R.id.multi_check_button); 26 | multiSelect.setOnClickListener(navigateTo(MultiCheckActivity.class)); 27 | 28 | Button singleSelect = (Button) findViewById(R.id.single_check_button); 29 | singleSelect.setOnClickListener(navigateTo(SingleCheckActivity.class)); 30 | 31 | Button mixedSelect = (Button) findViewById(R.id.mixedtype_button); 32 | mixedSelect.setOnClickListener(navigateTo(MultiTypeActivity.class)); 33 | 34 | Button mixedTypeAndCheck = (Button) findViewById(R.id.mixedtype_check_button); 35 | mixedTypeAndCheck.setOnClickListener(navigateTo(MultiTypeCheckGenreActivity.class)); 36 | } 37 | 38 | public OnClickListener navigateTo(final Class clazz) { 39 | return new OnClickListener() { 40 | @Override 41 | public void onClick(View v) { 42 | Intent intent = new Intent(MainActivity.this, clazz); 43 | startActivity(intent); 44 | } 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/expand/ArtistViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.expand; 2 | 3 | import android.view.View; 4 | import android.widget.TextView; 5 | import com.thoughtbot.expandablerecyclerview.sample.R; 6 | import com.thoughtbot.expandablerecyclerview.viewholders.ChildViewHolder; 7 | 8 | public class ArtistViewHolder extends ChildViewHolder { 9 | 10 | private TextView childTextView; 11 | 12 | public ArtistViewHolder(View itemView) { 13 | super(itemView); 14 | childTextView = (TextView) itemView.findViewById(R.id.list_item_artist_name); 15 | } 16 | 17 | public void setArtistName(String name) { 18 | childTextView.setText(name); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/expand/ExpandActivity.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.expand; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.widget.DefaultItemAnimator; 7 | import android.support.v7.widget.LinearLayoutManager; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.view.View; 10 | import android.view.View.OnClickListener; 11 | import android.widget.Button; 12 | import com.thoughtbot.expandablerecyclerview.sample.R; 13 | 14 | import static com.thoughtbot.expandablerecyclerview.sample.GenreDataFactory.makeClassicGenre; 15 | import static com.thoughtbot.expandablerecyclerview.sample.GenreDataFactory.makeGenres; 16 | 17 | public class ExpandActivity extends AppCompatActivity { 18 | 19 | public GenreAdapter adapter; 20 | 21 | @Override 22 | protected void onCreate(@Nullable Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_expand); 25 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 26 | getSupportActionBar().setTitle(getClass().getSimpleName()); 27 | 28 | RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); 29 | LinearLayoutManager layoutManager = new LinearLayoutManager(this); 30 | 31 | // RecyclerView has some built in animations to it, using the DefaultItemAnimator. 32 | // Specifically when you call notifyItemChanged() it does a fade animation for the changing 33 | // of the data in the ViewHolder. If you would like to disable this you can use the following: 34 | RecyclerView.ItemAnimator animator = recyclerView.getItemAnimator(); 35 | if (animator instanceof DefaultItemAnimator) { 36 | ((DefaultItemAnimator) animator).setSupportsChangeAnimations(false); 37 | } 38 | 39 | adapter = new GenreAdapter(makeGenres()); 40 | recyclerView.setLayoutManager(layoutManager); 41 | recyclerView.setAdapter(adapter); 42 | 43 | Button clear = (Button) findViewById(R.id.toggle_button); 44 | clear.setOnClickListener(new OnClickListener() { 45 | @Override 46 | public void onClick(View v) { 47 | adapter.toggleGroup(makeClassicGenre()); 48 | } 49 | }); 50 | } 51 | 52 | @Override 53 | protected void onSaveInstanceState(Bundle outState) { 54 | super.onSaveInstanceState(outState); 55 | adapter.onSaveInstanceState(outState); 56 | } 57 | 58 | @Override 59 | protected void onRestoreInstanceState(Bundle savedInstanceState) { 60 | super.onRestoreInstanceState(savedInstanceState); 61 | adapter.onRestoreInstanceState(savedInstanceState); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/expand/GenreAdapter.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.expand; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import com.thoughtbot.expandablerecyclerview.ExpandableRecyclerViewAdapter; 7 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 8 | import com.thoughtbot.expandablerecyclerview.sample.Artist; 9 | import com.thoughtbot.expandablerecyclerview.sample.Genre; 10 | import com.thoughtbot.expandablerecyclerview.sample.R; 11 | import java.util.List; 12 | 13 | public class GenreAdapter extends ExpandableRecyclerViewAdapter { 14 | 15 | public GenreAdapter(List groups) { 16 | super(groups); 17 | } 18 | 19 | @Override 20 | public GenreViewHolder onCreateGroupViewHolder(ViewGroup parent, int viewType) { 21 | View view = LayoutInflater.from(parent.getContext()) 22 | .inflate(R.layout.list_item_genre, parent, false); 23 | return new GenreViewHolder(view); 24 | } 25 | 26 | @Override 27 | public ArtistViewHolder onCreateChildViewHolder(ViewGroup parent, int viewType) { 28 | View view = LayoutInflater.from(parent.getContext()) 29 | .inflate(R.layout.list_item_artist, parent, false); 30 | return new ArtistViewHolder(view); 31 | } 32 | 33 | @Override 34 | public void onBindChildViewHolder(ArtistViewHolder holder, int flatPosition, 35 | ExpandableGroup group, int childIndex) { 36 | 37 | final Artist artist = ((Genre) group).getItems().get(childIndex); 38 | holder.setArtistName(artist.getName()); 39 | } 40 | 41 | @Override 42 | public void onBindGroupViewHolder(GenreViewHolder holder, int flatPosition, 43 | ExpandableGroup group) { 44 | 45 | holder.setGenreTitle(group); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/expand/GenreViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.expand; 2 | 3 | import android.view.View; 4 | import android.view.animation.RotateAnimation; 5 | import android.widget.ImageView; 6 | import android.widget.TextView; 7 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 8 | import com.thoughtbot.expandablerecyclerview.sample.Genre; 9 | import com.thoughtbot.expandablerecyclerview.sample.R; 10 | import com.thoughtbot.expandablerecyclerview.sample.multicheck.MultiCheckGenre; 11 | import com.thoughtbot.expandablerecyclerview.sample.singlecheck.SingleCheckGenre; 12 | import com.thoughtbot.expandablerecyclerview.viewholders.GroupViewHolder; 13 | 14 | import static android.view.animation.Animation.RELATIVE_TO_SELF; 15 | 16 | public class GenreViewHolder extends GroupViewHolder { 17 | 18 | private TextView genreName; 19 | private ImageView arrow; 20 | private ImageView icon; 21 | 22 | public GenreViewHolder(View itemView) { 23 | super(itemView); 24 | genreName = (TextView) itemView.findViewById(R.id.list_item_genre_name); 25 | arrow = (ImageView) itemView.findViewById(R.id.list_item_genre_arrow); 26 | icon = (ImageView) itemView.findViewById(R.id.list_item_genre_icon); 27 | } 28 | 29 | public void setGenreTitle(ExpandableGroup genre) { 30 | if (genre instanceof Genre) { 31 | genreName.setText(genre.getTitle()); 32 | icon.setBackgroundResource(((Genre) genre).getIconResId()); 33 | } 34 | if (genre instanceof MultiCheckGenre) { 35 | genreName.setText(genre.getTitle()); 36 | icon.setBackgroundResource(((MultiCheckGenre) genre).getIconResId()); 37 | } 38 | if (genre instanceof SingleCheckGenre) { 39 | genreName.setText(genre.getTitle()); 40 | icon.setBackgroundResource(((SingleCheckGenre) genre).getIconResId()); 41 | } 42 | } 43 | 44 | @Override 45 | public void expand() { 46 | animateExpand(); 47 | } 48 | 49 | @Override 50 | public void collapse() { 51 | animateCollapse(); 52 | } 53 | 54 | private void animateExpand() { 55 | RotateAnimation rotate = 56 | new RotateAnimation(360, 180, RELATIVE_TO_SELF, 0.5f, RELATIVE_TO_SELF, 0.5f); 57 | rotate.setDuration(300); 58 | rotate.setFillAfter(true); 59 | arrow.setAnimation(rotate); 60 | } 61 | 62 | private void animateCollapse() { 63 | RotateAnimation rotate = 64 | new RotateAnimation(180, 360, RELATIVE_TO_SELF, 0.5f, RELATIVE_TO_SELF, 0.5f); 65 | rotate.setDuration(300); 66 | rotate.setFillAfter(true); 67 | arrow.setAnimation(rotate); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/multicheck/MultiCheckActivity.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.multicheck; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.View; 9 | import android.view.View.OnClickListener; 10 | import android.widget.Button; 11 | import com.thoughtbot.expandablerecyclerview.sample.R; 12 | 13 | import static com.thoughtbot.expandablerecyclerview.sample.GenreDataFactory.makeMultiCheckGenres; 14 | 15 | public class MultiCheckActivity extends AppCompatActivity { 16 | 17 | private MultiCheckGenreAdapter adapter; 18 | 19 | @Override 20 | protected void onCreate(@Nullable Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_multi_check); 23 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 24 | getSupportActionBar().setTitle(getClass().getSimpleName()); 25 | 26 | RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); 27 | LinearLayoutManager layoutManager = new LinearLayoutManager(this); 28 | 29 | adapter = new MultiCheckGenreAdapter(makeMultiCheckGenres()); 30 | recyclerView.setLayoutManager(layoutManager); 31 | recyclerView.setAdapter(adapter); 32 | 33 | Button clear = (Button) findViewById(R.id.clear_button); 34 | clear.setOnClickListener(new OnClickListener() { 35 | @Override 36 | public void onClick(View v) { 37 | adapter.clearChoices(); 38 | } 39 | }); 40 | 41 | Button check = (Button) findViewById(R.id.check_first_child); 42 | check.setOnClickListener(new OnClickListener() { 43 | @Override 44 | public void onClick(View v) { 45 | adapter.checkChild(true, 0, 3); 46 | } 47 | }); 48 | } 49 | 50 | @Override 51 | protected void onSaveInstanceState(Bundle outState) { 52 | super.onSaveInstanceState(outState); 53 | adapter.onSaveInstanceState(outState); 54 | } 55 | 56 | @Override 57 | protected void onRestoreInstanceState(Bundle savedInstanceState) { 58 | super.onRestoreInstanceState(savedInstanceState); 59 | adapter.onRestoreInstanceState(savedInstanceState); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/multicheck/MultiCheckArtistViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.multicheck; 2 | 3 | import android.view.View; 4 | import android.widget.Checkable; 5 | import android.widget.CheckedTextView; 6 | import com.thoughtbot.expandablecheckrecyclerview.viewholders.CheckableChildViewHolder; 7 | import com.thoughtbot.expandablerecyclerview.sample.R; 8 | 9 | public class MultiCheckArtistViewHolder extends CheckableChildViewHolder { 10 | 11 | private CheckedTextView childCheckedTextView; 12 | 13 | public MultiCheckArtistViewHolder(View itemView) { 14 | super(itemView); 15 | childCheckedTextView = 16 | (CheckedTextView) itemView.findViewById(R.id.list_item_multicheck_artist_name); 17 | } 18 | 19 | @Override 20 | public Checkable getCheckable() { 21 | return childCheckedTextView; 22 | } 23 | 24 | public void setArtistName(String artistName) { 25 | childCheckedTextView.setText(artistName); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/multicheck/MultiCheckGenre.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.multicheck; 2 | 3 | import com.thoughtbot.expandablecheckrecyclerview.models.MultiCheckExpandableGroup; 4 | import com.thoughtbot.expandablerecyclerview.sample.Artist; 5 | import java.util.List; 6 | 7 | public class MultiCheckGenre extends MultiCheckExpandableGroup { 8 | 9 | private int iconResId; 10 | 11 | public MultiCheckGenre(String title, List items, int iconResId) { 12 | super(title, items); 13 | this.iconResId = iconResId; 14 | } 15 | 16 | public int getIconResId() { 17 | return iconResId; 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/multicheck/MultiCheckGenreAdapter.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.multicheck; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import com.thoughtbot.expandablecheckrecyclerview.CheckableChildRecyclerViewAdapter; 7 | import com.thoughtbot.expandablecheckrecyclerview.models.CheckedExpandableGroup; 8 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 9 | import com.thoughtbot.expandablerecyclerview.sample.Artist; 10 | import com.thoughtbot.expandablerecyclerview.sample.R; 11 | import com.thoughtbot.expandablerecyclerview.sample.expand.GenreViewHolder; 12 | import java.util.List; 13 | 14 | public class MultiCheckGenreAdapter extends 15 | CheckableChildRecyclerViewAdapter { 16 | 17 | public MultiCheckGenreAdapter(List groups) { 18 | super(groups); 19 | } 20 | 21 | @Override 22 | public MultiCheckArtistViewHolder onCreateCheckChildViewHolder(ViewGroup parent, int viewType) { 23 | View view = LayoutInflater.from(parent.getContext()) 24 | .inflate(R.layout.list_item_multicheck_artist, parent, false); 25 | return new MultiCheckArtistViewHolder(view); 26 | } 27 | 28 | @Override 29 | public void onBindCheckChildViewHolder(MultiCheckArtistViewHolder holder, int position, 30 | CheckedExpandableGroup group, int childIndex) { 31 | final Artist artist = (Artist) group.getItems().get(childIndex); 32 | holder.setArtistName(artist.getName()); 33 | } 34 | 35 | @Override 36 | public GenreViewHolder onCreateGroupViewHolder(ViewGroup parent, int viewType) { 37 | View view = LayoutInflater.from(parent.getContext()) 38 | .inflate(R.layout.list_item_genre, parent, false); 39 | return new GenreViewHolder(view); 40 | } 41 | 42 | @Override 43 | public void onBindGroupViewHolder(GenreViewHolder holder, int flatPosition, 44 | ExpandableGroup group) { 45 | holder.setGenreTitle(group); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/multitype/FavoriteArtistViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.multitype; 2 | 3 | import android.view.View; 4 | import android.widget.TextView; 5 | import com.thoughtbot.expandablerecyclerview.sample.R; 6 | import com.thoughtbot.expandablerecyclerview.viewholders.ChildViewHolder; 7 | 8 | public class FavoriteArtistViewHolder extends ChildViewHolder { 9 | 10 | private TextView favoriteArtistName; 11 | 12 | public FavoriteArtistViewHolder(View itemView) { 13 | super(itemView); 14 | favoriteArtistName = (TextView) itemView.findViewById(R.id.list_item_favorite_artist_name); 15 | } 16 | 17 | public void setArtistName(String name) { 18 | favoriteArtistName.setText(name); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/multitype/MultiTypeActivity.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.multitype; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import com.thoughtbot.expandablerecyclerview.sample.R; 9 | 10 | import static com.thoughtbot.expandablerecyclerview.sample.GenreDataFactory.makeGenres; 11 | 12 | public class MultiTypeActivity extends AppCompatActivity { 13 | 14 | private MultiTypeGenreAdapter adapter; 15 | 16 | @Override 17 | protected void onCreate(@Nullable Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.activity_multi_type); 20 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 21 | getSupportActionBar().setTitle(getClass().getSimpleName()); 22 | 23 | RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); 24 | LinearLayoutManager layoutManager = new LinearLayoutManager(this); 25 | 26 | adapter = new MultiTypeGenreAdapter(makeGenres()); 27 | recyclerView.setLayoutManager(layoutManager); 28 | recyclerView.setAdapter(adapter); 29 | } 30 | 31 | @Override 32 | protected void onSaveInstanceState(Bundle outState) { 33 | super.onSaveInstanceState(outState); 34 | adapter.onSaveInstanceState(outState); 35 | } 36 | 37 | @Override 38 | protected void onRestoreInstanceState(Bundle savedInstanceState) { 39 | super.onRestoreInstanceState(savedInstanceState); 40 | adapter.onRestoreInstanceState(savedInstanceState); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/multitype/MultiTypeGenreAdapter.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.multitype; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | import com.thoughtbot.expandablerecyclerview.MultiTypeExpandableRecyclerViewAdapter; 6 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 7 | import com.thoughtbot.expandablerecyclerview.models.ExpandableListPosition; 8 | import com.thoughtbot.expandablerecyclerview.sample.Artist; 9 | import com.thoughtbot.expandablerecyclerview.sample.Genre; 10 | import com.thoughtbot.expandablerecyclerview.sample.R; 11 | import com.thoughtbot.expandablerecyclerview.sample.expand.ArtistViewHolder; 12 | import com.thoughtbot.expandablerecyclerview.sample.expand.GenreViewHolder; 13 | import com.thoughtbot.expandablerecyclerview.viewholders.ChildViewHolder; 14 | import java.util.List; 15 | 16 | import static android.view.LayoutInflater.from; 17 | 18 | public class MultiTypeGenreAdapter 19 | extends MultiTypeExpandableRecyclerViewAdapter { 20 | 21 | public static final int FAVORITE_VIEW_TYPE = 3; 22 | public static final int ARTIST_VIEW_TYPE = 4; 23 | 24 | public MultiTypeGenreAdapter(List groups) { 25 | super(groups); 26 | } 27 | 28 | @Override 29 | public GenreViewHolder onCreateGroupViewHolder(ViewGroup parent, int viewType) { 30 | View view = from(parent.getContext()) 31 | .inflate(R.layout.list_item_genre, parent, false); 32 | return new GenreViewHolder(view); 33 | } 34 | 35 | @Override 36 | public ChildViewHolder onCreateChildViewHolder(ViewGroup parent, int viewType) { 37 | switch (viewType) { 38 | case ARTIST_VIEW_TYPE: 39 | View artist = from(parent.getContext()).inflate(R.layout.list_item_artist, parent, false); 40 | return new ArtistViewHolder(artist); 41 | case FAVORITE_VIEW_TYPE: 42 | View favorite = 43 | from(parent.getContext()).inflate(R.layout.list_item_favorite_artist, parent, false); 44 | return new FavoriteArtistViewHolder(favorite); 45 | default: 46 | throw new IllegalArgumentException("Invalid viewType"); 47 | } 48 | } 49 | 50 | @Override 51 | public void onBindChildViewHolder(ChildViewHolder holder, int flatPosition, ExpandableGroup group, 52 | int childIndex) { 53 | int viewType = getItemViewType(flatPosition); 54 | Artist artist = ((Genre) group).getItems().get(childIndex); 55 | switch (viewType) { 56 | case ARTIST_VIEW_TYPE: 57 | ((ArtistViewHolder) holder).setArtistName(artist.getName()); 58 | break; 59 | case FAVORITE_VIEW_TYPE: 60 | ((FavoriteArtistViewHolder) holder).setArtistName(artist.getName()); 61 | } 62 | } 63 | 64 | @Override 65 | public void onBindGroupViewHolder(GenreViewHolder holder, int flatPosition, 66 | ExpandableGroup group) { 67 | holder.setGenreTitle(group); 68 | } 69 | 70 | @Override 71 | public int getChildViewType(int position, ExpandableGroup group, int childIndex) { 72 | if (((Genre) group).getItems().get(childIndex).isFavorite()) { 73 | return FAVORITE_VIEW_TYPE; 74 | } else { 75 | return ARTIST_VIEW_TYPE; 76 | } 77 | } 78 | 79 | @Override 80 | public boolean isGroup(int viewType) { 81 | return viewType == ExpandableListPosition.GROUP; 82 | } 83 | 84 | @Override 85 | public boolean isChild(int viewType) { 86 | return viewType == FAVORITE_VIEW_TYPE || viewType == ARTIST_VIEW_TYPE; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/multitypeandcheck/MultiTypeCheckGenreActivity.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.multitypeandcheck; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.View; 9 | import android.view.View.OnClickListener; 10 | import android.widget.Button; 11 | import com.thoughtbot.expandablerecyclerview.sample.R; 12 | 13 | import static com.thoughtbot.expandablerecyclerview.sample.GenreDataFactory.makeSingleCheckGenres; 14 | 15 | public class MultiTypeCheckGenreActivity extends AppCompatActivity { 16 | 17 | private MultiTypeCheckGenreAdapter adapter; 18 | 19 | @Override 20 | protected void onCreate(@Nullable Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_multi_type_and_check); 23 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 24 | getSupportActionBar().setTitle(getClass().getSimpleName()); 25 | 26 | RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); 27 | LinearLayoutManager layoutManager = new LinearLayoutManager(this); 28 | 29 | adapter = new MultiTypeCheckGenreAdapter(makeSingleCheckGenres()); 30 | recyclerView.setLayoutManager(layoutManager); 31 | recyclerView.setAdapter(adapter); 32 | 33 | Button clear = (Button) findViewById(R.id.clear_button); 34 | clear.setOnClickListener(new OnClickListener() { 35 | @Override 36 | public void onClick(View v) { 37 | adapter.clearChoices(); 38 | } 39 | }); 40 | } 41 | 42 | @Override 43 | protected void onSaveInstanceState(Bundle outState) { 44 | super.onSaveInstanceState(outState); 45 | adapter.onSaveInstanceState(outState); 46 | } 47 | 48 | @Override 49 | protected void onRestoreInstanceState(Bundle savedInstanceState) { 50 | super.onRestoreInstanceState(savedInstanceState); 51 | adapter.onRestoreInstanceState(savedInstanceState); 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/multitypeandcheck/MultiTypeCheckGenreAdapter.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.multitypeandcheck; 2 | 3 | import android.os.Bundle; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import com.thoughtbot.expandablecheckrecyclerview.ChildCheckController; 8 | import com.thoughtbot.expandablecheckrecyclerview.listeners.OnCheckChildClickListener; 9 | import com.thoughtbot.expandablecheckrecyclerview.listeners.OnChildCheckChangedListener; 10 | import com.thoughtbot.expandablecheckrecyclerview.listeners.OnChildrenCheckStateChangedListener; 11 | import com.thoughtbot.expandablecheckrecyclerview.models.CheckedExpandableGroup; 12 | import com.thoughtbot.expandablerecyclerview.MultiTypeExpandableRecyclerViewAdapter; 13 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 14 | import com.thoughtbot.expandablerecyclerview.models.ExpandableListPosition; 15 | import com.thoughtbot.expandablerecyclerview.sample.Artist; 16 | import com.thoughtbot.expandablerecyclerview.sample.R; 17 | import com.thoughtbot.expandablerecyclerview.sample.expand.ArtistViewHolder; 18 | import com.thoughtbot.expandablerecyclerview.sample.expand.GenreViewHolder; 19 | import com.thoughtbot.expandablerecyclerview.sample.singlecheck.SingleCheckArtistViewHolder; 20 | import com.thoughtbot.expandablerecyclerview.viewholders.ChildViewHolder; 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | import static android.view.LayoutInflater.from; 25 | 26 | public class MultiTypeCheckGenreAdapter 27 | extends MultiTypeExpandableRecyclerViewAdapter 28 | implements OnChildCheckChangedListener, OnChildrenCheckStateChangedListener { 29 | 30 | private static final String CHECKED_STATE_MAP = "child_check_controller_checked_state_map"; 31 | 32 | public static final int FAVORITE_VIEW_TYPE = 3; 33 | public static final int ARTIST_VIEW_TYPE = 4; 34 | 35 | private ChildCheckController childCheckController; 36 | private OnCheckChildClickListener childClickListener; 37 | 38 | public MultiTypeCheckGenreAdapter(List groups) { 39 | super(groups); 40 | childCheckController = new ChildCheckController(expandableList, this); 41 | } 42 | 43 | @Override 44 | public GenreViewHolder onCreateGroupViewHolder(ViewGroup parent, int viewType) { 45 | View view = from(parent.getContext()) 46 | .inflate(R.layout.list_item_genre, parent, false); 47 | return new GenreViewHolder(view); 48 | } 49 | 50 | @Override 51 | public ChildViewHolder onCreateChildViewHolder(ViewGroup parent, int viewType) { 52 | switch (viewType) { 53 | case ARTIST_VIEW_TYPE: 54 | View artist = from(parent.getContext()).inflate(R.layout.list_item_artist, parent, false); 55 | return new ArtistViewHolder(artist); 56 | case FAVORITE_VIEW_TYPE: 57 | View view = LayoutInflater.from(parent.getContext()) 58 | .inflate(R.layout.list_item_singlecheck_arist, parent, false); 59 | SingleCheckArtistViewHolder holder = new SingleCheckArtistViewHolder(view); 60 | holder.setOnChildCheckedListener(this); 61 | return holder; 62 | default: 63 | throw new IllegalArgumentException(viewType + " is an Invalid viewType"); 64 | } 65 | } 66 | 67 | @Override 68 | public void onBindChildViewHolder(ChildViewHolder holder, int flatPosition, ExpandableGroup group, 69 | int childIndex) { 70 | int viewType = getItemViewType(flatPosition); 71 | Artist artist = (Artist) group.getItems().get(childIndex); 72 | switch (viewType) { 73 | case ARTIST_VIEW_TYPE: 74 | ((ArtistViewHolder) holder).setArtistName(artist.getName()); 75 | break; 76 | case FAVORITE_VIEW_TYPE: 77 | ExpandableListPosition listPosition = expandableList.getUnflattenedPosition(flatPosition); 78 | ((SingleCheckArtistViewHolder) holder) 79 | .onBindViewHolder(flatPosition, childCheckController.isChildChecked(listPosition)); 80 | ((SingleCheckArtistViewHolder) holder).setArtistName(artist.getName()); 81 | } 82 | } 83 | 84 | @Override 85 | public void onBindGroupViewHolder(GenreViewHolder holder, int flatPosition, 86 | ExpandableGroup group) { 87 | holder.setGenreTitle(group); 88 | } 89 | 90 | @Override 91 | public void onChildCheckChanged(View view, boolean checked, int flatPos) { 92 | ExpandableListPosition listPos = expandableList.getUnflattenedPosition(flatPos); 93 | childCheckController.onChildCheckChanged(checked, listPos); 94 | if (childClickListener != null) { 95 | childClickListener.onCheckChildCLick(view, checked, 96 | (CheckedExpandableGroup) expandableList.getExpandableGroup(listPos), listPos.childPos); 97 | } 98 | } 99 | 100 | @Override 101 | public void updateChildrenCheckState(int firstChildFlattenedIndex, int numChildren) { 102 | notifyItemRangeChanged(firstChildFlattenedIndex, numChildren); 103 | } 104 | 105 | @Override 106 | public void onSaveInstanceState(Bundle outState) { 107 | outState.putParcelableArrayList(CHECKED_STATE_MAP, new ArrayList(expandableList.groups)); 108 | super.onSaveInstanceState(outState); 109 | } 110 | 111 | @Override 112 | public void onRestoreInstanceState(Bundle savedInstanceState) { 113 | if (savedInstanceState == null || !savedInstanceState.containsKey(CHECKED_STATE_MAP)) { 114 | return; 115 | } 116 | expandableList.groups = savedInstanceState.getParcelableArrayList(CHECKED_STATE_MAP); 117 | super.onRestoreInstanceState(savedInstanceState); 118 | } 119 | 120 | public void clearChoices() { 121 | childCheckController.clearCheckStates(); 122 | 123 | //only update the child views that are visible (i.e. their group is expanded) 124 | for (int i = 0; i < getGroups().size(); i++) { 125 | ExpandableGroup group = getGroups().get(i); 126 | if (isGroupExpanded(group)) { 127 | notifyItemRangeChanged(expandableList.getFlattenedFirstChildIndex(i), group.getItemCount()); 128 | } 129 | } 130 | } 131 | 132 | @Override 133 | public boolean isChild(int viewType) { 134 | return viewType == FAVORITE_VIEW_TYPE || viewType == ARTIST_VIEW_TYPE; 135 | } 136 | 137 | @Override 138 | public int getChildViewType(int position, ExpandableGroup group, int childIndex) { 139 | if (((Artist) (group).getItems().get(childIndex)).isFavorite()) { 140 | return FAVORITE_VIEW_TYPE; 141 | } else { 142 | return ARTIST_VIEW_TYPE; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/singlecheck/SingleCheckActivity.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.singlecheck; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.View; 9 | import android.view.View.OnClickListener; 10 | import android.widget.Button; 11 | import com.thoughtbot.expandablerecyclerview.sample.R; 12 | 13 | import static com.thoughtbot.expandablerecyclerview.sample.GenreDataFactory.makeSingleCheckGenres; 14 | 15 | public class SingleCheckActivity extends AppCompatActivity { 16 | 17 | private SingleCheckGenreAdapter adapter; 18 | 19 | @Override 20 | protected void onCreate(@Nullable Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_single_check); 23 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 24 | getSupportActionBar().setTitle(getClass().getSimpleName()); 25 | 26 | RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); 27 | LinearLayoutManager layoutManager = new LinearLayoutManager(this); 28 | 29 | adapter = new SingleCheckGenreAdapter(makeSingleCheckGenres()); 30 | recyclerView.setLayoutManager(layoutManager); 31 | recyclerView.setAdapter(adapter); 32 | 33 | Button clear = (Button) findViewById(R.id.clear_button); 34 | clear.setOnClickListener(new OnClickListener() { 35 | @Override 36 | public void onClick(View v) { 37 | adapter.clearChoices(); 38 | } 39 | }); 40 | } 41 | 42 | @Override 43 | protected void onSaveInstanceState(Bundle outState) { 44 | super.onSaveInstanceState(outState); 45 | adapter.onSaveInstanceState(outState); 46 | } 47 | 48 | @Override 49 | protected void onRestoreInstanceState(Bundle savedInstanceState) { 50 | super.onRestoreInstanceState(savedInstanceState); 51 | adapter.onRestoreInstanceState(savedInstanceState); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/singlecheck/SingleCheckArtistViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.singlecheck; 2 | 3 | import android.view.View; 4 | import android.widget.Checkable; 5 | import android.widget.CheckedTextView; 6 | import com.thoughtbot.expandablecheckrecyclerview.viewholders.CheckableChildViewHolder; 7 | import com.thoughtbot.expandablerecyclerview.sample.R; 8 | 9 | public class SingleCheckArtistViewHolder extends CheckableChildViewHolder { 10 | 11 | private CheckedTextView childCheckedTextView; 12 | 13 | public SingleCheckArtistViewHolder(View itemView) { 14 | super(itemView); 15 | childCheckedTextView = 16 | (CheckedTextView) itemView.findViewById(R.id.list_item_singlecheck_artist_name); 17 | } 18 | 19 | @Override 20 | public Checkable getCheckable() { 21 | return childCheckedTextView; 22 | } 23 | 24 | public void setArtistName(String artistName) { 25 | childCheckedTextView.setText(artistName); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/singlecheck/SingleCheckGenre.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.singlecheck; 2 | 3 | import android.os.Parcel; 4 | import com.thoughtbot.expandablecheckrecyclerview.models.SingleCheckExpandableGroup; 5 | import java.util.List; 6 | 7 | public class SingleCheckGenre extends SingleCheckExpandableGroup { 8 | 9 | private int iconResId; 10 | 11 | public SingleCheckGenre(String title, List items, int iconResId) { 12 | super(title, items); 13 | this.iconResId = iconResId; 14 | } 15 | 16 | protected SingleCheckGenre(Parcel in) { 17 | super(in); 18 | iconResId = in.readInt(); 19 | } 20 | 21 | public int getIconResId() { 22 | return iconResId; 23 | } 24 | 25 | @Override 26 | public void writeToParcel(Parcel dest, int flags) { 27 | super.writeToParcel(dest, flags); 28 | dest.writeInt(iconResId); 29 | } 30 | 31 | @Override 32 | public int describeContents() { 33 | return 0; 34 | } 35 | 36 | public static final Creator CREATOR = new Creator() { 37 | @Override 38 | public SingleCheckGenre createFromParcel(Parcel in) { 39 | return new SingleCheckGenre(in); 40 | } 41 | 42 | @Override 43 | public SingleCheckGenre[] newArray(int size) { 44 | return new SingleCheckGenre[size]; 45 | } 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /sample/src/main/java/com/thoughtbot/expandablerecyclerview/sample/singlecheck/SingleCheckGenreAdapter.java: -------------------------------------------------------------------------------- 1 | package com.thoughtbot.expandablerecyclerview.sample.singlecheck; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import com.thoughtbot.expandablecheckrecyclerview.CheckableChildRecyclerViewAdapter; 7 | import com.thoughtbot.expandablecheckrecyclerview.models.CheckedExpandableGroup; 8 | import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; 9 | import com.thoughtbot.expandablerecyclerview.sample.Artist; 10 | import com.thoughtbot.expandablerecyclerview.sample.R; 11 | import com.thoughtbot.expandablerecyclerview.sample.expand.GenreViewHolder; 12 | import java.util.List; 13 | 14 | public class SingleCheckGenreAdapter extends 15 | CheckableChildRecyclerViewAdapter { 16 | 17 | public SingleCheckGenreAdapter(List groups) { 18 | super(groups); 19 | } 20 | 21 | @Override 22 | public SingleCheckArtistViewHolder onCreateCheckChildViewHolder(ViewGroup parent, int viewType) { 23 | View view = LayoutInflater.from(parent.getContext()) 24 | .inflate(R.layout.list_item_singlecheck_arist, parent, false); 25 | return new SingleCheckArtistViewHolder(view); 26 | } 27 | 28 | @Override 29 | public void onBindCheckChildViewHolder(SingleCheckArtistViewHolder holder, int position, 30 | CheckedExpandableGroup group, int childIndex) { 31 | final Artist artist = (Artist) group.getItems().get(childIndex); 32 | holder.setArtistName(artist.getName()); 33 | } 34 | 35 | @Override 36 | public GenreViewHolder onCreateGroupViewHolder(ViewGroup parent, int viewType) { 37 | View view = LayoutInflater.from(parent.getContext()) 38 | .inflate(R.layout.list_item_genre, parent, false); 39 | return new GenreViewHolder(view); 40 | } 41 | 42 | @Override 43 | public void onBindGroupViewHolder(GenreViewHolder holder, int flatPosition, 44 | ExpandableGroup group) { 45 | holder.setGenreTitle(group); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_arrow_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 13 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_banjo.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 22 | 31 | 40 | 48 | 58 | 68 | 78 | 82 | 92 | 102 | 112 | 122 | 137 | 140 | 150 | 160 | 170 | 171 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_electric_guitar.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 23 | 36 | 40 | 44 | 51 | 58 | 67 | 74 | 77 | 84 | 91 | 98 | 99 | 109 | 119 | 129 | 133 | 150 | 151 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_maracas.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 32 | 48 | 63 | 77 | 93 | 115 | 118 | 127 | 138 | 139 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_saxaphone.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 10 | 16 | 48 | 49 | 62 | 71 | 80 | 89 | 98 | 102 | 106 | 110 | 114 | 118 | 122 | 126 | 130 | 134 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_star.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 14 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_violin.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 10 | 14 | 21 | 27 | 31 | 32 | 49 | 62 | 66 | 75 | 87 | 102 | 105 | 111 | 116 | 117 | 121 | 130 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_expand.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 |