diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index c4df0551..2e94a6e0 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,10 +1,11 @@ Please check off the following points BEFORE submitting a new issue / asking support. - [ ] Write a self explanatory Subject. +- [ ] Check if the issue has already been raised by someone else (see closed issue). 1) For Bugs / Support -- [ ] What you've tried (If necessary, indicate library Version / Android version / hardware) -- [ ] Expected behavior and actual behavior (problem encountered) -- [ ] Steps to reproduce the problem (also include code snippets if you think will help) +- [ ] What you've tried (If necessary, indicate library Version / Android version / hardware). +- [ ] Expected behavior and actual behavior (problem encountered). +- [ ] Steps to reproduce the problem (also include code snippets if you think will help). 2) For questions / new feature / improvement - [ ] Simply Erase all this text and ask your question; diff --git a/README.md b/README.md index 902d30dc..00cb0331 100644 --- a/README.md +++ b/README.md @@ -2,40 +2,43 @@ [![Download](https://api.bintray.com/packages/davideas/maven/flexible-adapter/images/download.svg) ](https://bintray.com/davideas/maven/flexible-adapter/_latestVersion) [![API](https://img.shields.io/badge/API-14%2B-green.svg?style=flat)](https://android-arsenal.com/api?level=14) [![Licence](https://img.shields.io/badge/Licence-Apache2-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) +[![Methods and Size](https://img.shields.io/badge/Methods%20and%20Size-core:%201162%20|%20deps:%2021349%20|%20159%20KB-e91e63.svg)](http://www.methodscount.com/?lib=eu.davidea%3Aflexible-adapter%3A5.0.0-rc1) # FlexibleAdapter -###### Fast and Versatile Adapter for your RecyclerView -- NEW! Beta version: [v5.0.0-b8](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b8) built on 2016.09.17 (144KB) - ####ANNOUNCEMENT: Important and Revolutionary changes are foreseen in v5.0.0. Please see [issues](https://github.com/davideas/FlexibleAdapter/issues) and [releases](https://github.com/davideas/FlexibleAdapter/releases). +###### Fast and versatile Adapter for your RecyclerView +- **NEW!** First release candidate: [v5.0.0-rc1](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-rc1) built on 2017.01.14 +- If you come from previous versions, update your code following the Wiki page [Migrations](https://github.com/davideas/FlexibleAdapter/wiki/Migrations). + > When initially Android team introduced the RecyclerView widget, we had to implement a custom Adapter in several applications, again and again to provide the items for our views.
We didn't know how to add selection and to combine all the use cases in the same Adapter. Since I created this library, it has become easy to configure how views will be displayed in a list, and now, nobody wants to use a ListView anymore. -The idea behind is to regroup many functionalities in a unique library, without the need to customize and import several third libraries not compatible among them. +The idea behind is to regroup multiple features in a unique library, without the need to customize and import several third libraries not compatible among them. The FlexibleAdapter helps developers to simplify this process without worrying too much about the Adapter anymore. It's easy to extend, it has predefined logic for different situations and prevents common mistakes.
-This library is configurable and it guides the developers to create a better user experience and now, even more with the new ViewHolders and new actions. +This library is configurable and it guides the developers to create a better user experience and now, even more with the new features. -#### Main functionalities +#### Main features * Simple item selection with ripple effect, Single & Multi selection mode. * Restore deleted items, **NEW** works with Expandable items too! * Customizable [FastScroller](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-FastScroller), **NEW** now in the library supporting all the 3 Layouts. * Customizable divider item decoration. * Add and Remove items with custom animations. -* [SearchFilter](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-Search-Filter) with Spannable text, **NEW** result is animated. Works with sub items too! -* **NEW!** [High performance](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-Search-Filter#performance-result-when-animations-are-active) on big list filtered with synchronization animations < 1'', slow phones < 2,5'' (available from beta8). -* **NEW!** Auto mapping ViewTypes with [Item interfaces](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-Item-Interfaces). -* **NEW!** Predefined [ViewHolders](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-ViewHolders). -* **NEW!** [Headers/Sections](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-Headers-and-Sections) with sticky behaviour fully clickable, collapsible, automatic linkage! -* **NEW!** Expandable items with Selection Coherence, multi-level expansion. -* **NEW!** [Drag&Drop and Swipe-To-Dismiss](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-Drag&Drop-and-Swipe#swiping-the-front-view) with Leave-Behind pattern, with Selection Coherence. -* **NEW!** Customizable Scrolling-Animations based on adapter position and beyond. -* **NEW!** Innovative [EndlessScroll](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-On-Load-More) (No OnScrollListener). +* Async [SearchFilter](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-Search-Filter) with Spannable text, **NEW** result list is animated. Works with sub items too! +* **NEW!** [High performance](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-Search-Filter#performance-result-when-animations-are-active) updates and filter on big list. +* **NEW!** Auto mapping multi view types with [Item interfaces](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-Item-Interfaces). +* **NEW!** Predefined [ViewHolders](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-ViewHolders) with callbacks. +* **NEW!** [Headers and Sections](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-Headers-and-Sections) with sticky behaviour fully clickable and collapsible, with elevation, transparency and automatic linkage! +* **NEW!** [Scrollable Headers and Footers](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-Scrollable-Headers-and-Footers) items that lay respectively at the top and at the bottom of the main items. +* **NEW!** Expandable items with _Selection Coherence_ and multi-level expansion. +* **NEW!** [Drag&Drop and Swipe-To-Dismiss](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-Drag&Drop-and-Swipe#swiping-the-front-view) with Leave-Behind pattern and with _Selection Coherence_. +* **NEW!** Customizable scrolling animations based on adapter position and beyond. +* **NEW!** Innovative [EndlessScroll](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-On-Load-More) (_No OnScrollListener_). * **NEW!** UndoHelper & [ActionModeHelper](https://github.com/davideas/FlexibleAdapter/wiki/5.x-%7C-ActionModeHelper). -* **NEW!** DrawableUtils for dynamic backgrounds with ripple (No XML). +* **NEW!** DrawableUtils for dynamic backgrounds with ripple (_No XML_). * **NEW!** Easy runtime position calculation for adding/moving items in sections. * **NEW!** [Wiki](https://github.com/davideas/FlexibleAdapter/wiki/) pages documentation. @@ -43,42 +46,43 @@ This library is configurable and it guides the developers to create a better use ``` repositories { jcenter() - maven {url = "http://dl.bintray.com/davideas/maven" } maven {url = "https://oss.sonatype.org/content/repositories/snapshots/" } //For Snapshots } ``` ``` dependencies { - //Using JCenter - compile 'eu.davidea:flexible-adapter:5.0.0-b8' + // Using JCenter + compile 'eu.davidea:flexible-adapter:5.0.0-rc1' - //Using MavenSnapshots repository for continuous updates from my development + // Using MavenSnapshots repository for continuous updates from my development compile 'eu.davidea:flexible-adapter:5.0.0-SNAPSHOT' } ``` # Wiki! I strongly recommend to read the **new [Wiki](https://github.com/davideas/FlexibleAdapter/wiki) pages**, where you can find a comprehensive Tutorial*.
-Wiki pages have been completely reviewed to support all the coming functionalities from 5.0.0. +Wiki pages have been completely reviewed to support all the coming features of version 5.0.0. \* = _Pages are under heavy revision, working in progress_ :-) #### Pull requests / Issues / Improvement requests Feel free to contribute and ask!
Active discussions: +- [The next steps of your development](https://github.com/davideas/FlexibleAdapter/issues/224). - [Snapshots and Pre-Releases for FlexibleAdapter v5.0.0](https://github.com/davideas/FlexibleAdapter/issues/39). - [Documentation](https://github.com/davideas/FlexibleAdapter/issues/120). #### Under the hood -Some simple functionalities have been implemented thanks to some Blogs (see at the bottom of the page), merged and methods have been improved for speed and scalability, for all Activities that use a RecyclerView. +Some simple features have been implemented, thanks to some Blogs (see at the bottom of the page), merged and methods have been improved for speed and scalability. -* At lower level there is `SelectableAdapter` class. It provides selection functionalities and it's able to _maintain the state_ after the rotation: you just need to call the onSave/onRestore methods from the Activity! +* At lower level there is `SelectableAdapter` class. It provides selection features and it's able to _maintain the state_ after the rotation: you just need to call the onSave/onRestore methods from the Activity! * At middle level, the `AnimatorAdapter` class has been added to give some animation at startup and when user scrolls. * At front level, the core class `FlexibleAdapter`. It holds and handles the main list, performs actions on all different types of item paying attention at the adding and removal of the items, as well as the new concept of "selection coherence". +* New useful extensions and helpers have been added during the time to simplify the development. * Item interfaces and predefined ViewHolders complete the whole library giving more actions to the items and configuration options to the developers and the end user. # Showcase of the demo App -You can download the latest demo App from the latest release page. +You can download the latest demo App from the latest release page OR run it with the emulator. ![Drag Grid & Overall](/screenshots/drag_grid_overall.png) ![Secondary Functionalities](/screenshots/secondary_functionalities.png) @@ -98,25 +102,26 @@ You can download the latest demo App from the latest release page. # Change Log ###### Latest release -[v5.0.0-b8](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b8) - 2016.09.17 +[v5.0.0-rc1](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-rc1) - 2017.01.14 ###### Old releases +[v5.0.0-b8](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b8) - 2016.09.17 | [v5.0.0-b7](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b7) - 2016.06.20 | -[v5.0.0-b6](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b6) - 2016.05.01 | -[v5.0.0-b5](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b5) - 2016.04.04 | -[v5.0.0-b4](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b4) - 2016.02.21
-[v5.0.0-b3](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b3) - 2016.02.08 | -[v5.0.0-b2](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b2) - 2016.01.31 | +[v5.0.0-b6](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b6) - 2016.05.01 | +[v5.0.0-b5](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b5) - 2016.04.04
+[v5.0.0-b4](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b4) - 2016.02.21 | +[v5.0.0-b3](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b3) - 2016.02.08 | +[v5.0.0-b2](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b2) - 2016.01.31 | [v5.0.0-b1](https://github.com/davideas/FlexibleAdapter/releases/tag/5.0.0-b1) - 2016.01.03
-[v4.2.0](https://github.com/davideas/FlexibleAdapter/releases/tag/4.2.0) - 2015.12.12 | -[v4.1.0](https://github.com/davideas/FlexibleAdapter/releases/tag/4.1.0) - 2015.11.29 | -[v4.0.1](https://github.com/davideas/FlexibleAdapter/releases/tag/4.0.1) - 2015.11.01 | +[v4.2.0](https://github.com/davideas/FlexibleAdapter/releases/tag/4.2.0) - 2015.12.12 | +[v4.1.0](https://github.com/davideas/FlexibleAdapter/releases/tag/4.1.0) - 2015.11.29 | +[v4.0.1](https://github.com/davideas/FlexibleAdapter/releases/tag/4.0.1) - 2015.11.01 | [v4.0.0](https://github.com/davideas/FlexibleAdapter/releases/tag/4.0.0) - 2015.10.18
-[v3.1](https://github.com/davideas/FlexibleAdapter/releases/tag/v3.1) - 2015.08.18 | -[v3.0](https://github.com/davideas/FlexibleAdapter/releases/tag/v3.0) - 2015.07.29 | -[v2.2](https://github.com/davideas/FlexibleAdapter/releases/tag/v2.2) - 2015.07.20 | -[v2.1](https://github.com/davideas/FlexibleAdapter/releases/tag/v2.1) - 2015.07.03 | -[v2.0](https://github.com/davideas/FlexibleAdapter/releases/tag/v2.0) - 2015.06.19 | +[v3.1](https://github.com/davideas/FlexibleAdapter/releases/tag/v3.1) - 2015.08.18 | +[v3.0](https://github.com/davideas/FlexibleAdapter/releases/tag/v3.0) - 2015.07.29 | +[v2.2](https://github.com/davideas/FlexibleAdapter/releases/tag/v2.2) - 2015.07.20 | +[v2.1](https://github.com/davideas/FlexibleAdapter/releases/tag/v2.1) - 2015.07.03 | +[v2.0](https://github.com/davideas/FlexibleAdapter/releases/tag/v2.0) - 2015.06.19 | [v1.0](https://github.com/davideas/FlexibleAdapter/releases/tag/v1.0) - 2015.05.03 # Limitations @@ -131,18 +136,20 @@ I've used these blogs as starting point: Special thanks goes to Martin Guillon ([Akylas](https://github.com/Akylas)) to have contributed at the development of the new technique for the Sticky Header. # Imported libraries -- For the moment only [LollipopContactsRecyclerViewFastScroller](https://github.com/AndroidDeveloperLB/LollipopContactsRecyclerViewFastScroller) has been imported, improved and adapted to work in conjunction with `AnimatorAdapter`. -- The library [sticky-headers-recyclerview](https://github.com/timehop/sticky-headers-recyclerview) was initially imported and super-optimized for _FlexibleAdapter_, then it was removed in favor of the new technique able to keep the _View_ and so to handle the click events. +- The library [LollipopContactsRecyclerViewFastScroller](https://github.com/AndroidDeveloperLB/LollipopContactsRecyclerViewFastScroller) has been imported, improved and adapted to work in conjunction with `AnimatorAdapter`. +- The library [sticky-headers-recyclerview](https://github.com/timehop/sticky-headers-recyclerview) was initially imported, then it was removed in favor of the new technique able to manage a real _View_ and so to handle the click events. # Apps that use this Adapter -It will be a pleasure to add your App here. -- [Socio - Shake and Connect!](https://play.google.com/store/apps/details?id=com.atsocio.socio)
-- [BNVR Client](https://play.google.com/store/apps/details?id=ru.beward.bnvr)
-- [Module.org](https://play.google.com/store/apps/details?id=org.module.app)
+It will be a pleasure to add your App here, once it is published. + +[Module.org](https://play.google.com/store/apps/details?id=org.module.app) | +[Socio - Shake and Connect!](https://play.google.com/store/apps/details?id=com.atsocio.socio) | +[Shibagram](https://play.google.com/store/apps/details?id=com.apripachkin.shibagram) | +[BNVR Client](https://play.google.com/store/apps/details?id=ru.beward.bnvr) # License - Copyright 2015-2016 Davide Steduto + Copyright 2015-2017 Davide Steduto Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/build.gradle b/build.gradle index 54337a48..3c4237bd 100644 --- a/build.gradle +++ b/build.gradle @@ -6,10 +6,10 @@ ext { developerEmail = "dave.dna@gmail.com" //Library - libraryCode = 18 - libraryVersion = "5.0.0-b8" + libraryCode = 19 + libraryVersion = "5.0.0-rc1" libraryDate = " built on " + getDate() - libraryDescription = "1 Adapter for SelectionMode, Undo, ViewHolders, Filter, FastScroller, Animations, Sticky Headers, Expandable, Draggable, Swipeable, EndlessScroll :-)" + libraryDescription = "1 Adapter for SelectionMode, ViewHolders, AsyncFilter, FastScroller, Animations, Undo, Sections, Sticky Headers, Scrollable Headers and Footers, EndlessScroll, Expandable, Draggable, Swipeable :-)" libraryName = "FlexibleAdapter" packageExt = "aar" @@ -23,9 +23,9 @@ ext { //Support and Build tools version minSdk = 14 - targetSdk = 24 - buildTools = "23.0.3" - supportLib = "24.2.1" + targetSdk = 25 + buildTools = "25.0.2" + supportLib = "25.1.0" //Support Libraries dependencies supportDependencies = [ @@ -49,7 +49,7 @@ def getDate() { return date.format('yyyy.MM.dd') } -//Avoid Javadoc Lint failures when using Java8 +//Avoid JavaDoc Lint failures when using Java8 if (JavaVersion.current().isJava8Compatible()) { allprojects { tasks.withType(Javadoc) { @@ -65,7 +65,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:2.2.0' + classpath 'com.android.tools.build:gradle:2.2.3' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' classpath "com.github.dcendents:android-maven-gradle-plugin:1.5" classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7" diff --git a/flexible-adapter-app/build.gradle b/flexible-adapter-app/build.gradle index d1f833d8..eb0539d4 100644 --- a/flexible-adapter-app/build.gradle +++ b/flexible-adapter-app/build.gradle @@ -41,9 +41,9 @@ android { dependencies { //Testing androidTestCompile supportDependencies.annotations - androidTestCompile 'com.android.support.test:runner:0.4.1' - androidTestCompile 'com.android.support.test:rules:0.4.1' - testCompile 'org.robolectric:robolectric:3.1.1' + androidTestCompile 'com.android.support.test:runner:0.5' + androidTestCompile 'com.android.support.test:rules:0.5' + testCompile 'org.robolectric:robolectric:3.1.2' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.0.71-beta' @@ -51,7 +51,7 @@ dependencies { compile project (":flexible-adapter") //FlipView - compile "eu.davidea:flipview:1.1.1" + compile "eu.davidea:flipview:1.1.2" //ButterKnife compile 'com.jakewharton:butterknife:8.0.1' diff --git a/flexible-adapter-app/src/main/AndroidManifest.xml b/flexible-adapter-app/src/main/AndroidManifest.xml index 93c46ccf..32cd7854 100644 --- a/flexible-adapter-app/src/main/AndroidManifest.xml +++ b/flexible-adapter-app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ - + @@ -20,7 +19,7 @@ - + @@ -28,6 +27,15 @@ + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/ExampleAdapter.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/ExampleAdapter.java index 9e1065a8..56db923a 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/ExampleAdapter.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/ExampleAdapter.java @@ -1,15 +1,23 @@ package eu.davidea.samples.flexibleadapter; -import android.support.annotation.NonNull; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.StaggeredGridLayoutManager; +import android.view.ViewGroup; import java.util.List; import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; -import eu.davidea.samples.flexibleadapter.models.LayoutItem; -import eu.davidea.samples.flexibleadapter.models.ULSItem; +import eu.davidea.flexibleadapter.utils.Utils; +import eu.davidea.samples.flexibleadapter.items.ScrollableExpandableItem; +import eu.davidea.samples.flexibleadapter.items.ScrollableFooterItem; +import eu.davidea.samples.flexibleadapter.items.ScrollableLayoutItem; +import eu.davidea.samples.flexibleadapter.items.ScrollableSubItem; +import eu.davidea.samples.flexibleadapter.items.ScrollableULSItem; import eu.davidea.samples.flexibleadapter.services.DatabaseConfiguration; /** @@ -26,36 +34,48 @@ public class ExampleAdapter extends FlexibleAdapter { private static final String TAG = ExampleAdapter.class.getSimpleName(); - private AbstractFlexibleItem mUseCaseItem; + private Context mContext; public ExampleAdapter(List items, Object listeners) { - //true = Items implement hashCode() and have stableIds! + //stableIds ? true = Items implement hashCode() so they can have stableIds! super(items, listeners, true); + + //In case you need a Handler, do this: + //- Overrides the internal Handler with a custom callback that extends the internal one + mHandler = new Handler(Looper.getMainLooper(), new MyHandlerCallback()); } @Override public void updateDataSet(List items, boolean animate) { - //NOTE: To have views/items not changed, set them into "items" before passing the final + // NOTE: To have views/items not changed, set them into "items" before passing the final // list to the Adapter. - //Overwrite the list and fully notify the change, pass false to not animate changes. - //Watch out! The original list must a copy + // Overwrite the list and fully notify the change, pass false to not animate changes. + // Watch out! The original list must a copy. super.updateDataSet(items, animate); - //Add example view - //showLayoutInfo(true); + // onPostUpdate() will automatically be called at the end of the Asynchronous update + // process. Manipulate the list inside that method only or you won't see the changes. + } + + /* + * You can override this method to define your own concept of "Empty". + * This method is never called internally. + */ + @Override + public boolean isEmpty() { + return super.isEmpty(); } /* * HEADER VIEW - * This method show how to add Header/Footer View as it was for ListView. - * The secret is the position! 0 for Header; itemCount for Footer ;-) + * This method shows how to add Header View as it was for ListView. + * Same Header item is enqueued for removal with a delay. * The view is represented by a custom Item type to better represent any dynamic content. */ public void showLayoutInfo(boolean scrollToPosition) { - if (!hasSearchText() && !isEmpty()) { - //Define Example View - final LayoutItem item = new LayoutItem("LAY-L"); + if (!hasSearchText()) { + final ScrollableLayoutItem item = new ScrollableLayoutItem("LAY-L"); if (mRecyclerView.getLayoutManager() instanceof StaggeredGridLayoutManager) { item.setId("LAY-S"); item.setTitle(mRecyclerView.getContext().getString(R.string.staggered_layout)); @@ -65,47 +85,81 @@ public void showLayoutInfo(boolean scrollToPosition) { } else { item.setTitle(mRecyclerView.getContext().getString(R.string.linear_layout)); } - item.setSubtitle(mRecyclerView.getContext().getString(R.string.columns, getSpanCount(mRecyclerView.getLayoutManager()))); - addItemWithDelay((getItem(0) instanceof ULSItem ? 1 : 0), item, 0L, - (!(getItem(0) instanceof ULSItem) && scrollToPosition)); - removeItemWithDelay(item, 4000L, true); + item.setSubtitle(mRecyclerView.getContext().getString( + R.string.columns, + String.valueOf(Utils.getSpanCount(mRecyclerView.getLayoutManager()))) + ); + // NOTE: If you have to change at runtime the LayoutManager AND add + // Scrollable Headers, consider to add them in post, using a delay >= 0 + // otherwise scroll animations on all items will not start correctly. + addScrollableHeaderWithDelay(item, 1200L, scrollToPosition); + removeScrollableHeaderWithDelay(item, 4000L); } } /* * ANOTHER HEADER VIEW - * This method show how to add Header/Footer View as it was for ListView. - * The secret is the position! 0 for Header; itemCount for Footer ;-) + * This method showcases how to add a Header View with a delay. * The view is represented by a custom Item type to better represent any dynamic content. */ public void addUserLearnedSelection(boolean scrollToPosition) { - if (!DatabaseConfiguration.userLearnedSelection && !hasSearchText() && !(getItem(0) instanceof ULSItem)) { - //Define Example View - final ULSItem item = new ULSItem("ULS"); + if (!DatabaseConfiguration.userLearnedSelection && !hasSearchText() && !(getItem(0) instanceof ScrollableULSItem)) { + final ScrollableULSItem item = new ScrollableULSItem("ULS"); item.setTitle(mRecyclerView.getContext().getString(R.string.uls_title)); item.setSubtitle(mRecyclerView.getContext().getString(R.string.uls_subtitle)); - addItemWithDelay(0, item, 1500L, scrollToPosition); + addScrollableHeaderWithDelay(item, 1000L, false); } } - @Override - public void filterItems(@NonNull List unfilteredItems) { - super.filterItems(unfilteredItems); - addUserLearnedSelection(false); - showLayoutInfo(false); + /* + * FOOTER VIEW + * This method showcases how to delay add a Footer View. + * The view is represented by a custom Item type to better represent any dynamic content. + */ + public void addScrollableFooter() { + final ScrollableFooterItem item = new ScrollableFooterItem("SFI"); + item.setTitle(mRecyclerView.getContext().getString(R.string.scrollable_footer_title)); + item.setSubtitle(mRecyclerView.getContext().getString(R.string.scrollable_footer_subtitle)); + addScrollableFooterWithDelay(item, 1000L, false); + } + + /* + * Showcase for EXPANDABLE HEADER VIEW + */ + public void addScrollableExpandableAsHeader() { + final ScrollableExpandableItem expandable = new ScrollableExpandableItem("SEHI"); + expandable.setTitle(mRecyclerView.getContext().getString(R.string.scrollable_expandable_header_title)); + expandable.setSubtitle(mRecyclerView.getContext().getString(R.string.scrollable_expandable_header_subtitle)); + expandable.addSubItem(new ScrollableSubItem("SEHI_1")); + expandable.addSubItem(new ScrollableSubItem("SEHI_2")); + addScrollableHeaderWithDelay(expandable, 1500L, false); + } + + /* + * Showcase for EXPANDABLE FOOTER VIEW + */ + public void addScrollableExpandableAsFooter() { + final ScrollableExpandableItem expandable = new ScrollableExpandableItem("SEFI"); + expandable.setTitle(mRecyclerView.getContext().getString(R.string.scrollable_expandable_footer_title)); + expandable.setSubtitle(mRecyclerView.getContext().getString(R.string.scrollable_expandable_footer_subtitle)); + expandable.addSubItem(new ScrollableSubItem("SEFI_1")); + expandable.addSubItem(new ScrollableSubItem("SEFI_2")); + addScrollableFooterWithDelay(expandable, 1500L, false); } /** * This is a customization of the Layout that hosts the header when sticky. * The code works, but it is commented because not used (default is used). + *

Note: You now can set a custom container by calling + * {@link #setStickyHeaderContainer(ViewGroup)}

*/ // @Override -// public ViewGroup getStickySectionHeadersHolder() { +// public ViewGroup getStickyHeaderContainer() { // FrameLayout frameLayout = new FrameLayout(mRecyclerView.getContext()); // frameLayout.setLayoutParams(new ViewGroup.LayoutParams( -// ViewGroup.LayoutParams.WRAP_CONTENT,//or MATCH_PARENT +// ViewGroup.LayoutParams.WRAP_CONTENT, //or MATCH_PARENT // ViewGroup.LayoutParams.WRAP_CONTENT)); -// ((ViewGroup) mRecyclerView.getParent()).addView(frameLayout);//This is important otherwise the Header disappears! +// ((ViewGroup) mRecyclerView.getParent()).addView(frameLayout); //This is important otherwise the Header disappears! // return (ViewGroup) mInflater.inflate(R.layout.sticky_header_layout, frameLayout); // } @@ -124,7 +178,7 @@ public void filterItems(@NonNull List unfilteredItems) { */ // @Override // public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { -// //Not implemented: METHOD A is used +// // Not implemented: METHOD A is used // } /** @@ -133,16 +187,51 @@ public void filterItems(@NonNull List unfilteredItems) { */ // @Override // public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { -// //Not implemented: METHOD A is used +// // Not implemented: METHOD A is used // } - @Override public String onCreateBubbleText(int position) { - if (!DatabaseConfiguration.userLearnedSelection && position == 0) {//This 'if' is for my example only - //TODO FOR YOU: This is the normal line you should use: Usually it's the first letter - return Integer.toString(position); + if (position < getScrollableHeaders().size()) { + return "Top"; + } else if (position >= getItemCount() - getScrollableFooters().size()){ + return "Bottom"; + } else { + position -= getScrollableHeaders().size() + 1; } + // TODO FOR YOU: The basic value, usually, is the first letter + // For me is the position return super.onCreateBubbleText(position); } + /** + * IMPORTANT: In order to preserve the internal calls, this custom Callback + * must extends {@link FlexibleAdapter.HandlerCallback} + * which implements {@link android.os.Handler.Callback}, + * therefore you must call {@code super().handleMessage(message)}. + *

This handler can launch asynchronous tasks.

+ * If you catch the reserved "what", keep in mind that this code should be executed + * before that task has been completed. + *

Note: numbers 0-9 are reserved for the Adapter, use others for new values.

+ */ + private class MyHandlerCallback extends HandlerCallback { + @Override + public boolean handleMessage(Message message) { + boolean done = super.handleMessage(message); + switch (message.what) { + // Currently reserved (you don't need to check these numbers!) + case 0: //async updateDataSet + case 1: //async filterItems + case 2: //confirm delete + case 8: //onLoadMore remove progress item + return done; + + // Free to use, example: + case 10: + case 11: + return true; + } + return false; + } + } + } \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/MainActivity.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/MainActivity.java index 3ffcdbe5..4b569dc5 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/MainActivity.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/MainActivity.java @@ -2,6 +2,8 @@ import android.app.SearchManager; import android.content.Context; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -11,6 +13,8 @@ import android.support.design.widget.FloatingActionButton; import android.support.design.widget.NavigationView; import android.support.design.widget.Snackbar; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.app.FragmentManager; import android.support.v4.view.GravityCompat; import android.support.v4.view.MenuItemCompat; @@ -47,7 +51,6 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; import eu.davidea.flexibleadapter.items.IExpandable; import eu.davidea.flexibleadapter.items.IFlexible; -import eu.davidea.flexibleadapter.items.IHeader; import eu.davidea.samples.flexibleadapter.dialogs.EditItemDialog; import eu.davidea.samples.flexibleadapter.dialogs.MessageDialog; import eu.davidea.samples.flexibleadapter.fragments.AbstractFragment; @@ -57,21 +60,23 @@ import eu.davidea.samples.flexibleadapter.fragments.FragmentExpandableMultiLevel; import eu.davidea.samples.flexibleadapter.fragments.FragmentExpandableSections; import eu.davidea.samples.flexibleadapter.fragments.FragmentHeadersSections; +import eu.davidea.samples.flexibleadapter.fragments.FragmentHolderSections; import eu.davidea.samples.flexibleadapter.fragments.FragmentInstagramHeaders; import eu.davidea.samples.flexibleadapter.fragments.FragmentOverall; import eu.davidea.samples.flexibleadapter.fragments.FragmentSelectionModes; import eu.davidea.samples.flexibleadapter.fragments.FragmentStaggeredLayout; import eu.davidea.samples.flexibleadapter.fragments.OnFragmentInteractionListener; -import eu.davidea.samples.flexibleadapter.models.AbstractModelItem; -import eu.davidea.samples.flexibleadapter.models.ExpandableItem; -import eu.davidea.samples.flexibleadapter.models.HeaderItem; -import eu.davidea.samples.flexibleadapter.models.OverallItem; -import eu.davidea.samples.flexibleadapter.models.SimpleItem; -import eu.davidea.samples.flexibleadapter.models.StaggeredItem; -import eu.davidea.samples.flexibleadapter.models.SubItem; +import eu.davidea.samples.flexibleadapter.items.AbstractItem; +import eu.davidea.samples.flexibleadapter.items.ExpandableItem; +import eu.davidea.samples.flexibleadapter.items.HeaderItem; +import eu.davidea.samples.flexibleadapter.items.OverallItem; +import eu.davidea.samples.flexibleadapter.items.SimpleItem; +import eu.davidea.samples.flexibleadapter.items.StaggeredItem; +import eu.davidea.samples.flexibleadapter.items.SubItem; import eu.davidea.samples.flexibleadapter.services.DatabaseConfiguration; import eu.davidea.samples.flexibleadapter.services.DatabaseService; import eu.davidea.samples.flexibleadapter.services.DatabaseType; +import eu.davidea.samples.flexibleadapter.views.HeaderView; import eu.davidea.utils.ScrollAwareFABBehavior; import eu.davidea.utils.Utils; @@ -79,9 +84,9 @@ * The Demo application is organized in Fragments with 1 Activity {@code MainActivity} * implementing most of the methods. Each Fragment shows a different example and can assemble * more functionalities at once. - * + *

*

The Activity implementation is organized in this order:

- * + *

*

    *
  • Activity management *
  • Initialization methods @@ -94,10 +99,10 @@ *
  • ActionMode implementation *
  • Extras *
- * + *

* The Fragments may use Activity implementations or may override specific behaviors * themselves. Fragments have {@code AbstractFragment} in common to have some methods reusable. - * + *

*

...more on * Demo app Wiki page.

*/ @@ -136,18 +141,17 @@ public class MainActivity extends AppCompatActivity implements private NavigationView mNavigationView; private AbstractFragment mFragment; private SearchView mSearchView; + private final Handler mRefreshHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { public boolean handleMessage(Message message) { switch (message.what) { - case 0: //Stop + case 0: // Stop mSwipeRefreshLayout.setRefreshing(false); - mSwipeRefreshLayout.setEnabled(true); return true; - case 1: //Start + case 1: // Start mSwipeRefreshLayout.setRefreshing(true); - mSwipeRefreshLayout.setEnabled(false); return true; - case 2: //Show empty view + case 2: // Show empty view ViewCompat.animate(findViewById(R.id.empty_view)).alpha(1); return true; default: @@ -156,7 +160,9 @@ public boolean handleMessage(Message message) { } }); - /* ACTIVITY MANAGEMENT */ + /* =================== + * ACTIVITY MANAGEMENT + * =================== */ @Override protected void onCreate(Bundle savedInstanceState) { @@ -171,15 +177,15 @@ protected void onCreate(Bundle savedInstanceState) { Log.d(TAG, "onCreate"); FlexibleAdapter.enableLogs(true); - //Initialize Toolbar, Drawer & FAB + // Initialize Toolbar, Drawer & FAB initializeToolbar(); initializeDrawer(); initializeFab(); - //Initialize Fragment containing Adapter & RecyclerView + // Initialize Fragment containing Adapter & RecyclerView initializeFragment(savedInstanceState); - //With FlexibleAdapter v5.0.0 we don't need to call this function anymore - //It is automatically called if Activity implements FlexibleAdapter.OnUpdateListener + // With FlexibleAdapter v5.0.0 we don't need to call this function anymore + // It is automatically called if Activity implements FlexibleAdapter.OnUpdateListener //updateEmptyView(); } @@ -194,9 +200,9 @@ public void onSaveInstanceState(Bundle outState) { @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); - //Restore previous state + // Restore previous state if (savedInstanceState != null && mAdapter != null) { - //Selection + // Selection mAdapter.onRestoreInstanceState(savedInstanceState); mActionModeHelper.restoreSelection(this); } @@ -211,7 +217,9 @@ public void onFragmentChange(SwipeRefreshLayout swipeRefreshLayout, RecyclerView initializeActionModeHelper(mode); } - /* INITIALIZATION METHODS */ + /* ====================== + * INITIALIZATION METHODS + * ====================== */ private void initializeActionModeHelper(int mode) { mActionModeHelper = new ActionModeHelper(mAdapter, mFragment.getContextMenuResId(), this) { @@ -239,20 +247,21 @@ private void initializeFragment(Bundle savedInstanceState) { } private void initializeSwipeToRefresh() { - //Swipe down to force synchronize + // Swipe down to force synchronize //mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout); mSwipeRefreshLayout.setDistanceToTriggerSync(390); - //mSwipeRefreshLayout.setEnabled(true); Controlled by fragments! + //mSwipeRefreshLayout.setEnabled(true); //Controlled by fragments! mSwipeRefreshLayout.setColorSchemeResources( android.R.color.holo_purple, android.R.color.holo_blue_light, android.R.color.holo_green_light, android.R.color.holo_orange_light); mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { - //Passing true as parameter we always animate the changes between the old and the new data set + // Passing true as parameter we always animate the changes between the old and the new data set + DatabaseService.getInstance().updateNewItems(); mAdapter.updateDataSet(DatabaseService.getInstance().getDatabaseList(), DatabaseConfiguration.animateOnUpdate); - mSwipeRefreshLayout.setEnabled(false); - mRefreshHandler.sendEmptyMessageDelayed(0, 100L);//Simulate network time + mSwipeRefreshLayout.setRefreshing(true); + mRefreshHandler.sendEmptyMessageDelayed(0, 1500L); //Simulate network time mActionModeHelper.destroyActionModeIfCan(); } }); @@ -264,7 +273,7 @@ private void initializeToolbar() { mHeaderView = (HeaderView) findViewById(R.id.toolbar_header_view); mHeaderView.bindTo(getString(R.string.app_name), getString(R.string.overall)); //mToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.toolbar_layout); - //Toolbar will now take on default Action Bar characteristics + // Toolbar will now take on default Action Bar characteristics setSupportActionBar(mToolbar); } @@ -278,7 +287,7 @@ private void initializeDrawer() { mNavigationView = (NavigationView) findViewById(R.id.nav_view); mNavigationView.setNavigationItemSelectedListener(this); - //Version + // Version TextView appVersion = (TextView) mNavigationView.getHeaderView(0).findViewById(R.id.app_version); appVersion.setText(getString(R.string.about_version, Utils.getVersionName(this), @@ -294,7 +303,7 @@ public void onClick(View v) { mFragment.performFabAction(); } }); - //No Fab on 1st fragment + // No Fab on 1st fragment hideFabSilently(); } @@ -307,7 +316,9 @@ public void onFastScrollerStateChange(boolean scrolling) { } } - /* NAVIGATION DRAWER & FRAGMENT MANAGEMENT */ + /* ======================================= + * NAVIGATION DRAWER & FRAGMENT MANAGEMENT + * ======================================= */ /** * IMPORTANT!! READ THE COMMENT FOR THE FRAGMENT REPLACE @@ -320,7 +331,7 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) { ScrollAwareFABBehavior fabBehavior = ((ScrollAwareFABBehavior) layoutParams.getBehavior()); fabBehavior.setEnabled(false); - //Handle navigation view item clicks + // Handle navigation view item clicks int id = item.getItemId(); if (id == R.id.nav_overall) { mFragment = FragmentOverall.newInstance(2); @@ -343,6 +354,20 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) { mFragment = FragmentExpandableSections.newInstance(3); } else if (id == R.id.nav_staggered) { mFragment = FragmentStaggeredLayout.newInstance(2); + } else if (id == R.id.nav_model_holders) { + mFragment = FragmentHolderSections.newInstance(); + } else if (id == R.id.nav_viewpager) { + Intent intent = new Intent(this, ViewPagerActivity.class); + ActivityOptionsCompat activityOptionsCompat = ActivityOptionsCompat.makeBasic(); + ActivityCompat.startActivity(this, intent, activityOptionsCompat.toBundle()); + // Close drawer + mRecyclerView.post(new Runnable() { + @Override + public void run() { + mDrawer.closeDrawer(GravityCompat.START); + } + }); + return true; } else if (id == R.id.nav_about) { MessageDialog.newInstance( R.drawable.ic_info_grey600_24dp, @@ -353,22 +378,25 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) { .show(getFragmentManager(), MessageDialog.TAG); return true; } else if (id == R.id.nav_github) { - + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("https://github.com/davideas/FlexibleAdapter")); + startActivity(Intent.createChooser(intent, getString(R.string.intent_chooser))); + return true; } // Insert the fragment by replacing any existing fragment if (mFragment != null) { - //Highlight the selected item has been done by NavigationView + // Highlight the selected item has been done by NavigationView item.setChecked(true); - //THIS IS VERY IMPORTANT. Because you are going to inflate a new RecyclerView, its - //Adapter will be null, therefore the following method cannot be called automatically! - //If your StickyHeaderContainer is in the main view, you must call this method to clean - //the previous sticky view. Alternatively you can move the of StickyHeaderLayout - //in the Fragment view. + // THIS IS VERY IMPORTANT. Because you are going to inflate a new RecyclerView, its + // Adapter will be null, therefore the following method cannot be called automatically! + // If your StickyHeaderContainer is in the main view, you must call this method to clean + // the previous sticky view. Alternatively you can move the of StickyHeaderLayout + // in the Fragment view. mAdapter.onDetachedFromRecyclerView(mRecyclerView); - //Inflate the new Fragment with the new RecyclerView and a new Adapter + // Inflate the new Fragment with the new RecyclerView and a new Adapter FragmentManager fragmentManager = getSupportFragmentManager(); fragmentManager.beginTransaction().replace(R.id.recycler_view_container, mFragment).commit(); - //Close drawer + // Close drawer mRecyclerView.post(new Runnable() { @Override public void run() { @@ -384,7 +412,9 @@ public void run() { return false; } - /* FLOATING ACTION BUTTON */ + /* ====================== + * FLOATING ACTION BUTTON + * ====================== */ private void hideFabSilently() { mFab.setAlpha(0f); @@ -409,11 +439,13 @@ private void showFab() { } } - /* SEARCH VIEW */ + /* =========== + * SEARCH VIEW + * =========== */ @Override public void initSearchView(final Menu menu) { - //Associate searchable configuration with the SearchView + // Associate searchable configuration with the SearchView Log.d(TAG, "onCreateOptionsMenu setup SearchView!"); SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); MenuItem searchItem = menu.findItem(R.id.action_search); @@ -452,11 +484,11 @@ public boolean onQueryTextChange(String newText) { if (mAdapter.hasNewSearchText(newText)) { Log.d(TAG, "onQueryTextChange newText: " + newText); mAdapter.setSearchText(newText); - //Fill and Filter mItems with your custom list and automatically animate the changes - //Watch out! The original list must be a copy + // Fill and Filter mItems with your custom list and automatically animate the changes + // Watch out! The original list must be a copy mAdapter.filterItems(DatabaseService.getInstance().getDatabaseList(), DatabaseConfiguration.delay); } - //Disable SwipeRefresh if search is active!! + // Disable SwipeRefresh if search is active!! mSwipeRefreshLayout.setEnabled(!mAdapter.hasSearchText()); return true; } @@ -467,7 +499,9 @@ public boolean onQueryTextSubmit(String query) { return onQueryTextChange(query); } - /* OPTION MENU PREPARATION & MANAGEMENT */ + /* ==================================== + * OPTION MENU PREPARATION & MANAGEMENT + * ==================================== */ @Override public boolean onPrepareOptionsMenu(Menu menu) { @@ -487,37 +521,42 @@ public boolean onPrepareOptionsMenu(Menu menu) { //mSearchView.setIconified(false);//This is not necessary } } - //Fast Scroller + // Fast Scroller MenuItem fastScrollerItem = menu.findItem(R.id.action_fast_scroller); if (fastScrollerItem != null) { fastScrollerItem.setChecked(mAdapter.isFastScrollerEnabled()); } - //Animate on update? + // Animate on update? MenuItem animateUpdateMenuItem = menu.findItem(R.id.action_animate_on_update); if (animateUpdateMenuItem != null) { animateUpdateMenuItem.setChecked(DatabaseConfiguration.animateOnUpdate); } - //Headers are shown? + // Headers are shown? MenuItem headersMenuItem = menu.findItem(R.id.action_show_hide_headers); if (headersMenuItem != null) { headersMenuItem.setTitle(mAdapter.areHeadersShown() ? R.string.hide_headers : R.string.show_headers); } - //Sticky Header item? + // Sticky Header item? MenuItem stickyItem = menu.findItem(R.id.action_sticky_headers); if (stickyItem != null) { stickyItem.setEnabled(mAdapter.areHeadersShown()); stickyItem.setChecked(mAdapter.areHeadersSticky()); } - //Scrolling Animations? + // Scrolling Animations? MenuItem animationMenuItem = menu.findItem(R.id.action_animation); if (animationMenuItem != null) { animationMenuItem.setChecked(DatabaseConfiguration.animateOnScrolling); } - //Reverse scrolling animation? + // Reverse scrolling animation? MenuItem reverseMenuItem = menu.findItem(R.id.action_reverse); if (reverseMenuItem != null) { reverseMenuItem.setEnabled(mAdapter.isAnimationOnScrollingEnabled()); - reverseMenuItem.setChecked(mAdapter.isAnimationOnReverseScrolling()); + reverseMenuItem.setChecked(mAdapter.isAnimationOnReverseScrollingEnabled()); + } + // DiffUtil? + MenuItem diffUtilItem = menu.findItem(R.id.action_diff_util); + if (diffUtilItem != null) { + diffUtilItem.setChecked(DatabaseConfiguration.animateWithDiffUtil); } return super.onPrepareOptionsMenu(menu); } @@ -535,15 +574,15 @@ public boolean onOptionsItemSelected(MenuItem item) { DatabaseConfiguration.animateOnScrolling = false; mAdapter.setAnimationOnScrolling(false); item.setChecked(false); - Snackbar.make(findViewById(R.id.main_view), "Disabled scrolling animation, now reopen the page\n(P = persistent)", Snackbar.LENGTH_SHORT).show(); + Snackbar.make(findViewById(R.id.main_view), "Disabled scrolling animation, now reopen the page\n(* = persistent)", Snackbar.LENGTH_SHORT).show(); } else { DatabaseConfiguration.animateOnScrolling = true; mAdapter.setAnimationOnScrolling(true); item.setChecked(true); - Snackbar.make(findViewById(R.id.main_view), "Enabled scrolling animation, now reopen the page\n(P = persistent)", Snackbar.LENGTH_SHORT).show(); + Snackbar.make(findViewById(R.id.main_view), "Enabled scrolling animation, now reopen the page\n(* = persistent)", Snackbar.LENGTH_SHORT).show(); } } else if (id == R.id.action_reverse) { - if (mAdapter.isAnimationOnReverseScrolling()) { + if (mAdapter.isAnimationOnReverseScrollingEnabled()) { mAdapter.setAnimationOnReverseScrolling(false); item.setChecked(false); Snackbar.make(findViewById(R.id.main_view), "Disabled reverse scrolling animation", Snackbar.LENGTH_SHORT).show(); @@ -552,6 +591,18 @@ public boolean onOptionsItemSelected(MenuItem item) { item.setChecked(true); Snackbar.make(findViewById(R.id.main_view), "Enabled reverse scrolling animation", Snackbar.LENGTH_SHORT).show(); } + } else if (id == R.id.action_diff_util) { + if (mAdapter.isAnimateChangesWithDiffUtil()) { + DatabaseConfiguration.animateWithDiffUtil = false; + mAdapter.setAnimateChangesWithDiffUtil(false); + item.setChecked(false); + Snackbar.make(findViewById(R.id.main_view), "Default calculation is used to animate changes\n(* = persistent)", Snackbar.LENGTH_SHORT).show(); + } else { + DatabaseConfiguration.animateWithDiffUtil = true; + mAdapter.setAnimateChangesWithDiffUtil(true); + item.setChecked(true); + Snackbar.make(findViewById(R.id.main_view), "DiffUtil is used to animate changes\n(* = persistent)", Snackbar.LENGTH_SHORT).show(); + } } else if (id == R.id.action_auto_collapse) { if (mAdapter.isAutoCollapseOnExpand()) { mAdapter.setAutoCollapseOnExpand(false); @@ -581,15 +632,10 @@ public boolean onOptionsItemSelected(MenuItem item) { item.setTitle(R.string.hide_headers); } } else if (id == R.id.action_sticky_headers) { - if (mAdapter.areHeadersSticky()) { - mAdapter.disableStickyHeaders(); - item.setChecked(false); - Snackbar.make(findViewById(R.id.main_view), "Sticky headers disabled", Snackbar.LENGTH_SHORT).show(); - } else { - mAdapter.enableStickyHeaders(); - item.setChecked(true); - Snackbar.make(findViewById(R.id.main_view), "Sticky headers enabled", Snackbar.LENGTH_SHORT).show(); - } + mAdapter.setStickyHeaders(!mAdapter.areHeadersSticky()); + item.setChecked(!mAdapter.areHeadersSticky()); + Snackbar.make(findViewById(R.id.main_view), "Sticky headers " + + (mAdapter.areHeadersSticky() ? "disabled" : "enabled"), Snackbar.LENGTH_SHORT).show(); } else if (id == R.id.action_selection_mode) { if (mAdapter.getMode() == SelectableAdapter.MODE_IDLE) { mAdapter.setMode(SelectableAdapter.MODE_SINGLE); @@ -614,15 +660,16 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } - /* DIALOG LISTENER IMPLEMENTATION (For the example of onItemClick) */ - + /* =============================================================== + * DIALOG LISTENER IMPLEMENTATION (For the example of onItemClick) + * =============================================================== */ @Override public void onTitleModified(int position, String newTitle) { AbstractFlexibleItem abstractItem = mAdapter.getItem(position); assert abstractItem != null; - if (abstractItem instanceof AbstractModelItem) { - AbstractModelItem exampleItem = (AbstractModelItem) abstractItem; + if (abstractItem instanceof AbstractItem) { + AbstractItem exampleItem = (AbstractItem) abstractItem; exampleItem.setTitle(newTitle); } else if (abstractItem instanceof HeaderItem) { HeaderItem headerItem = (HeaderItem) abstractItem; @@ -631,7 +678,11 @@ public void onTitleModified(int position, String newTitle) { mAdapter.updateItem(position, abstractItem, null); } - /* FLEXIBLE ADAPTER LISTENERS IMPLEMENTATION */ + /* ======================================================================== + * FLEXIBLE ADAPTER LISTENERS IMPLEMENTATION + * Listeners implementation are in MainActivity to easily reuse the common + * components like SwipeToRefresh, ActionMode, NavigationView, etc... + * ======================================================================== */ @Override public boolean onItemClick(int position) { @@ -643,13 +694,14 @@ public boolean onItemClick(int position) { return false; } - //Action on elements are allowed if Mode is IDLE, otherwise selection has priority + // Action on elements are allowed if Mode is IDLE, otherwise selection has priority if (mAdapter.getMode() != SelectableAdapter.MODE_IDLE && mActionModeHelper != null) { - return mActionModeHelper.onClick(position); + boolean activate = mActionModeHelper.onClick(position); + Log.d(TAG, "Last activated position " + mActionModeHelper.getActivatedPosition()); + return activate; } else { - //Notify the active callbacks or implement a custom action onClick - if (!(flexibleItem instanceof ExpandableItem) && flexibleItem instanceof SimpleItem - || flexibleItem instanceof SubItem) { + // Notify the active callbacks or implement a custom action onClick + if (flexibleItem instanceof SimpleItem || flexibleItem instanceof SubItem) { //TODO FOR YOU: call your custom Action on item click String title = extractTitleFrom(flexibleItem); EditItemDialog.newInstance(title, position).show(getFragmentManager(), EditItemDialog.TAG); @@ -687,54 +739,65 @@ public void onItemSwipe(final int position, int direction) { Log.i(TAG, "onItemSwipe position=" + position + " direction=" + (direction == ItemTouchHelper.LEFT ? "LEFT" : "RIGHT")); - //Option 1 FULL_SWIPE: Direct action no Undo Action - //Do something based on direction when item has been swiped: + // Option 1 FULL_SWIPE: Direct action no Undo Action + // Do something based on direction when item has been swiped: // A) update item, set "read" if an email etc. // B) remove the item from the adapter; - //Option 2 FULL_SWIPE: Delayed action with Undo Action - //Show action button and start a new Handler: + // Option 2 FULL_SWIPE: Delayed action with Undo Action + // Show action button and start a new Handler: // A) on time out do something based on direction (open dialog with options); - //Create list for single position (only in onItemSwipe) + // Create list for single position (only in onItemSwipe) List positions = new ArrayList<>(1); positions.add(position); - //Build the message + // Build the message IFlexible abstractItem = mAdapter.getItem(position); StringBuilder message = new StringBuilder(); message.append(extractTitleFrom(abstractItem)).append(" "); - //Experimenting NEW feature + // Experimenting NEW feature if (abstractItem.isSelectable()) mAdapter.setRestoreSelectionOnUndo(false); - //Perform different actions - //Here, option 2A) is implemented + // Perform different actions + // Here, option 2A) is implemented if (direction == ItemTouchHelper.LEFT) { message.append(getString(R.string.action_archived)); + + // Example of UNDO color + int actionTextColor; + if (Utils.hasMarshmallow()) { + actionTextColor = getColor(R.color.material_color_orange_500); + } else { + //noinspection deprecation + actionTextColor = getResources().getColor(R.color.material_color_orange_500); + } + new UndoHelper(mAdapter, this) - .withPayload(null)//You can pass any custom object (in this case Boolean is enough) + .withPayload(null) //You can pass any custom object (in this case Boolean is enough) .withAction(UndoHelper.ACTION_UPDATE, new UndoHelper.SimpleActionListener() { @Override public boolean onPreAction() { - //Return true to avoid default immediate deletion. - //Ask to the user what to do, open a custom dialog. On option chosen, - //remove the item from Adapter list as usual. + // Return true to avoid default immediate deletion. + // Ask to the user what to do, open a custom dialog. On option chosen, + // remove the item from Adapter list as usual. return true; } }) + .withActionTextColor(actionTextColor) .remove(positions, findViewById(R.id.main_view), message, getString(R.string.undo), UndoHelper.UNDO_TIMEOUT); //Here, option 1B) is implemented } else if (direction == ItemTouchHelper.RIGHT) { message.append(getString(R.string.action_deleted)); + mSwipeRefreshLayout.setRefreshing(true); new UndoHelper(mAdapter, this) - .withPayload(null)//You can pass any custom object (in this case Boolean is enough) + .withPayload(null) //You can pass any custom object (in this case Boolean is enough) .withAction(UndoHelper.ACTION_REMOVE, new UndoHelper.SimpleActionListener() { @Override public void onPostAction() { - logOrphanHeaders(); - //Handle ActionMode title + // Handle ActionMode title if (mAdapter.getSelectedItemCount() == 0) mActionModeHelper.destroyActionModeIfCan(); else @@ -757,7 +820,8 @@ public void onUpdateEmptyView(int size) { FastScroller fastScroller = (FastScroller) findViewById(R.id.fast_scroller); View emptyView = findViewById(R.id.empty_view); TextView emptyText = (TextView) findViewById(R.id.empty_text); - emptyText.setText(getString(R.string.no_items)); + if (emptyText != null) + emptyText.setText(getString(R.string.no_items)); if (size > 0) { fastScroller.setVisibility(View.VISIBLE); mRefreshHandler.removeMessages(2); @@ -767,15 +831,17 @@ public void onUpdateEmptyView(int size) { mRefreshHandler.sendEmptyMessage(2); fastScroller.setVisibility(View.GONE); } - if (mAdapter != null && mAdapter.hasSearchText()) { - Snackbar.make(findViewById(R.id.main_view), "Filtered " + size + " items", Snackbar.LENGTH_SHORT).show(); + if (mAdapter != null) { + String message = (mAdapter.hasSearchText() ? "Filtered " : "Refreshed "); + message += size + " items in " + mAdapter.getTime() + "ms"; + Snackbar.make(findViewById(R.id.main_view), message, Snackbar.LENGTH_SHORT).show(); } } @Override public void onUndoConfirmed(int action) { if (action == UndoHelper.ACTION_UPDATE) { - //FIXME: Adjust click animation on swiped item + //TODO: Complete click animation on swiped item // final RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForLayoutPosition(mSwipedPosition); // if (holder instanceof ItemTouchHelperCallback.ViewHolderCallback) { // final View view = ((ItemTouchHelperCallback.ViewHolderCallback) holder).getFrontView(); @@ -789,11 +855,11 @@ public void onUndoConfirmed(int action) { // animator.start(); // } } else if (action == UndoHelper.ACTION_REMOVE) { - //Custom action is restore deleted items + // Custom action is restore deleted items mAdapter.restoreDeletedItems(); - //Enable SwipeRefresh - mRefreshHandler.sendEmptyMessage(0); - //Check also selection restoration + // Disable Refreshing + mSwipeRefreshLayout.setRefreshing(false); + // Check also selection restoration if (mAdapter.isRestoreWithSelection()) { mActionModeHelper.restoreSelection(this); } @@ -802,12 +868,12 @@ public void onUndoConfirmed(int action) { @Override public void onDeleteConfirmed(int action) { - //Enable SwipeRefresh - mRefreshHandler.sendEmptyMessage(0); - //Removing items from Database. Example: + // Disable Refreshing + mSwipeRefreshLayout.setRefreshing(false); + // Removing items from Database. Example: for (AbstractFlexibleItem adapterItem : mAdapter.getDeletedItems()) { try { - //NEW! You can take advantage of AutoMap and differentiate logic by viewType using "switch" statement + // NEW! You can take advantage of AutoMap and differentiate logic by viewType using "switch" statement switch (adapterItem.getLayoutRes()) { case R.layout.recycler_sub_item: SubItem subItem = (SubItem) adapterItem; @@ -821,15 +887,14 @@ public void onDeleteConfirmed(int action) { } } catch (IllegalStateException e) { - //AutoMap is disabled, fallback to if-else with "instanceof" statement + // AutoMap is disabled, fallback to if-else with "instanceof" statement if (adapterItem instanceof SubItem) { - //SubItem + // SubItem SubItem subItem = (SubItem) adapterItem; IExpandable expandable = mAdapter.getExpandableOf(subItem); DatabaseService.getInstance().removeSubItem(expandable, subItem); Log.d(TAG, "Confirm removed " + subItem.getTitle()); - } else if (adapterItem instanceof SimpleItem) { - //SimpleItem or ExpandableItem(extends SimpleItem) + } else if (adapterItem instanceof SimpleItem || adapterItem instanceof ExpandableItem) { DatabaseService.getInstance().removeItem(adapterItem); Log.d(TAG, "Confirm removed " + adapterItem); } @@ -837,7 +902,9 @@ public void onDeleteConfirmed(int action) { } } - /* ACTION MODE IMPLEMENTATION */ + /* ========================== + * ACTION MODE IMPLEMENTATION + * ========================== */ @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { @@ -861,11 +928,11 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { case R.id.action_select_all: mAdapter.selectAll(); mActionModeHelper.updateContextTitle(mAdapter.getSelectedItemCount()); - //We consume the event + // We consume the event return true; case R.id.action_delete: - //Build message before delete, for the SnackBar + // Build message before delete, for the SnackBar StringBuilder message = new StringBuilder(); message.append(getString(R.string.action_deleted)).append(" "); for (Integer pos : mAdapter.getSelectedPositions()) { @@ -874,57 +941,56 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { message.append(", "); } - //Experimenting NEW feature + // Experimenting NEW feature mAdapter.setRestoreSelectionOnUndo(true); - //New Undo Helper + // New Undo Helper new UndoHelper(mAdapter, this) .withPayload(Payload.CHANGE) .withAction(UndoHelper.ACTION_REMOVE, new UndoHelper.OnActionListener() { @Override public boolean onPreAction() { - //Don't consume the event - //OR use UndoHelper.SimpleActionListener and Override only onPostAction() + // Don't consume the event + // OR use UndoHelper.SimpleActionListener and Override only onPostAction() return false; } @Override public void onPostAction() { - //Disable SwipeRefresh + // Enable Refreshing mRefreshHandler.sendEmptyMessage(1); - mRefreshHandler.sendEmptyMessageDelayed(0, 20000); - //Finish the action mode + mRefreshHandler.sendEmptyMessageDelayed(0, 7000); + // Finish the action mode mActionModeHelper.destroyActionModeIfCan(); - logOrphanHeaders(); } }) .remove(mAdapter.getSelectedPositions(), findViewById(R.id.main_view), message, - getString(R.string.undo), 20000); + getString(R.string.undo), 7000); - //We consume the event + // We consume the event return true; case R.id.action_merge: if (mAdapter.getSelectedItemCount() > 1) { - //Selected positions are sorted by default, we take the first item of the set + // Selected positions are sorted by default, we take the first item of the set int mainPosition = mAdapter.getSelectedPositions().get(0); mAdapter.removeSelection(mainPosition); StaggeredItem mainItem = (StaggeredItem) mAdapter.getItem(mainPosition); for (Integer position : mAdapter.getSelectedPositions()) { - //Merge item - Save the modification in the memory for next refresh + // Merge item - Save the modification in the memory for next refresh DatabaseService.getInstance().mergeItem(mainItem, (StaggeredItem) mAdapter.getItem(position)); } - //Remove merged item from the list + // Remove merged item from the list mAdapter.removeAllSelectedItems(); - //Keep selection on mainItem & Skip default notification by calling addSelection + // Keep selection on mainItem & Skip default notification by calling addSelection mAdapter.addSelection(mainPosition); - //Custom notification to bind again (ripple only) + // Custom notification to bind again (ripple only) mAdapter.notifyItemChanged(mainPosition, "blink"); - //New title for context + // New title for context mActionModeHelper.updateContextTitle(mAdapter.getSelectedItemCount()); } - //We consume always the event, never finish the ActionMode + // We consume always the event, never finish the ActionMode return true; case R.id.action_split: @@ -933,25 +999,25 @@ public void onPostAction() { if (mainItem.getMergedItems() != null) { List itemsToSplit = new ArrayList<>(mainItem.getMergedItems()); for (StaggeredItem itemToSplit : itemsToSplit) { - //Split item - Save the modification in the memory for next refresh + // Split item - Save the modification in the memory for next refresh DatabaseService.getInstance().splitItem(mainItem, itemToSplit); - //We know the section object, so we can insert directly the item at the right position - //The calculated position is then returned + // We know the section object, so we can insert directly the item at the right position + // The calculated position is then returned int position = mAdapter.addItemToSection(itemToSplit, mainItem.getHeader(), new DatabaseService.ItemComparatorById()); - mAdapter.toggleSelection(position);//Execute default notification + mAdapter.toggleSelection(position); //Execute default notification mAdapter.notifyItemChanged(position, "blink"); } - //Custom notification to bind again (ripple only) + // Custom notification to bind again (ripple only) mAdapter.notifyItemChanged(mAdapter.getGlobalPositionOf(mainItem), "blink"); - //New title for context + // New title for context mActionModeHelper.updateContextTitle(mAdapter.getSelectedItemCount()); } } - //We consume always the event, never finish the ActionMode + // We consume always the event, never finish the ActionMode return true; default: - //If an item is not implemented we don't consume the event, so we finish the ActionMode + // If an item is not implemented we don't consume the event, so we finish the ActionMode return false; } } @@ -966,44 +1032,38 @@ public void onDestroyActionMode(ActionMode mode) { } } - /* EXTRAS */ + /* ====== + * EXTRAS + * ====== */ @Override public void onBackPressed() { - //If Drawer is open, back key closes it + // If Drawer is open, back key closes it if (mDrawer.isDrawerOpen(GravityCompat.START)) { mDrawer.closeDrawer(GravityCompat.START); return; } - //If ActionMode is active, back key closes it + // If ActionMode is active, back key closes it if (mActionModeHelper.destroyActionModeIfCan()) return; - //If SearchView is visible, back key cancels search and iconify it + // If SearchView is visible, back key cancels search and iconify it if (mSearchView != null && !mSearchView.isIconified()) { mSearchView.setIconified(true); return; } - //Return to Overall View + // Return to Overall View if (DatabaseService.getInstance().getDatabaseType() != DatabaseType.OVERALL) { MenuItem menuItem = mNavigationView.getMenu().findItem(R.id.nav_overall); onNavigationItemSelected(menuItem); return; } - //Close the App + // Close the App DatabaseService.onDestroy(); super.onBackPressed(); } - private void logOrphanHeaders() { - //If removeOrphanHeader is set false, once hidden the Orphan Headers are not shown - // anymore, but you can recover them using getOrphanHeaders() - for (IHeader header : mAdapter.getOrphanHeaders()) { - Log.w(TAG, "Logging orphan header " + header); - } - } - private String extractTitleFrom(IFlexible flexibleItem) { - if (flexibleItem instanceof AbstractModelItem) { - AbstractModelItem exampleItem = (AbstractModelItem) flexibleItem; + if (flexibleItem instanceof AbstractItem) { + AbstractItem exampleItem = (AbstractItem) flexibleItem; String title = exampleItem.getTitle(); if (exampleItem instanceof ExpandableItem) { ExpandableItem expandableItem = (ExpandableItem) flexibleItem; @@ -1016,7 +1076,7 @@ private String extractTitleFrom(IFlexible flexibleItem) { HeaderItem headerItem = (HeaderItem) flexibleItem; return headerItem.getTitle(); } - //We already covered all situations with instanceof + // We already covered all situations with instanceof return ""; } diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/OverallAdapter.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/OverallAdapter.java index 1661dce8..89aa28a4 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/OverallAdapter.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/OverallAdapter.java @@ -1,6 +1,7 @@ package eu.davidea.samples.flexibleadapter; import android.app.Activity; +import android.content.Context; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; @@ -13,8 +14,10 @@ import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; import eu.davidea.flexibleadapter.items.IFlexible; -import eu.davidea.samples.flexibleadapter.models.LayoutItem; -import eu.davidea.samples.flexibleadapter.models.OverallItem; +import eu.davidea.flexibleadapter.utils.DrawableUtils; +import eu.davidea.samples.flexibleadapter.items.ScrollableLayoutItem; +import eu.davidea.samples.flexibleadapter.items.OverallItem; +import eu.davidea.samples.flexibleadapter.items.ScrollableUseCaseItem; import eu.davidea.samples.flexibleadapter.services.DatabaseService; import eu.davidea.utils.Utils; @@ -47,7 +50,7 @@ public OverallAdapter(Activity activity) { public void showLayoutInfo(boolean scrollToPosition) { if (!hasSearchText()) { //Define Example View - final LayoutItem item = new LayoutItem("LAY-L"); + final ScrollableLayoutItem item = new ScrollableLayoutItem("LAY-L"); if (mRecyclerView.getLayoutManager() instanceof StaggeredGridLayoutManager) { item.setId("LAY-S"); item.setTitle(mRecyclerView.getContext().getString(R.string.staggered_layout)); @@ -57,9 +60,12 @@ public void showLayoutInfo(boolean scrollToPosition) { } else { item.setTitle(mRecyclerView.getContext().getString(R.string.linear_layout)); } - item.setSubtitle(mRecyclerView.getContext().getString(R.string.columns, getSpanCount(mRecyclerView.getLayoutManager()))); - addItemWithDelay(0, item, 500L, scrollToPosition); - removeItemWithDelay(item, 2000L, true); + item.setSubtitle(mRecyclerView.getContext().getString( + R.string.columns, + String.valueOf(eu.davidea.flexibleadapter.utils.Utils.getSpanCount(mRecyclerView.getLayoutManager()))) + ); + addScrollableHeaderWithDelay(item, 500L, scrollToPosition); + removeScrollableHeaderWithDelay(item, 3000L); } } @@ -70,8 +76,9 @@ public void showLayoutInfo(boolean scrollToPosition) { @Override public int getItemViewType(int position) { IFlexible item = getItem(position); - if (item instanceof LayoutItem) return R.layout.recycler_layout_item; - else return R.layout.recycler_label_item; + if (item instanceof ScrollableUseCaseItem) return R.layout.recycler_scrollable_usecase_item; + else if (item instanceof ScrollableLayoutItem) return R.layout.recycler_scrollable_layout_item; + else return R.layout.recycler_overall_item; } /** @@ -82,8 +89,12 @@ public int getItemViewType(int position) { public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { mInflater = LayoutInflater.from(parent.getContext()); switch (viewType) { - case R.layout.recycler_layout_item: - return new LayoutItem.ExampleViewHolder( + case R.layout.recycler_scrollable_usecase_item: + return new ScrollableUseCaseItem.UCViewHolder( + mInflater.inflate(viewType, parent, false), this); + + case R.layout.recycler_scrollable_layout_item: + return new ScrollableLayoutItem.LayoutViewHolder( mInflater.inflate(viewType, parent, false), this); default: return new OverallItem.LabelViewHolder( @@ -94,13 +105,36 @@ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType /** * METHOD A - NEW! Via Model objects. In this case you don't need to implement this method! * METHOD B - You override and implement this method as you prefer (don't call super). + * + * Using Method B, some methods need to be called by the user, see bottom of this method! */ @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payload) { int viewType = getItemViewType(position); - if (viewType == R.layout.recycler_layout_item) { - LayoutItem item = (LayoutItem) getItem(position); - LayoutItem.ExampleViewHolder vHolder = (LayoutItem.ExampleViewHolder) holder; + Context context = holder.itemView.getContext(); + + + if (viewType == R.layout.recycler_scrollable_usecase_item) { + ScrollableUseCaseItem item = (ScrollableUseCaseItem) getItem(position); + ScrollableUseCaseItem.UCViewHolder vHolder = (ScrollableUseCaseItem.UCViewHolder) holder; + assert item != null; + + DrawableUtils.setBackgroundCompat(holder.itemView, DrawableUtils.getRippleDrawable( + DrawableUtils.getColorDrawable(context.getResources().getColor(R.color.material_color_blue_grey_50)), + DrawableUtils.getColorControlHighlight(context)) + ); + vHolder.mTitle.setText(Utils.fromHtmlCompat(item.getTitle())); + vHolder.mSubtitle.setText(Utils.fromHtmlCompat(item.getSubtitle())); + + //Support for StaggeredGridLayoutManager + if (holder.itemView.getLayoutParams() instanceof StaggeredGridLayoutManager.LayoutParams) { + ((StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams()).setFullSpan(true); + Log.d("LayoutItem", "LayoutItem configured fullSpan for StaggeredGridLayout"); + } + + } else if (viewType == R.layout.recycler_scrollable_layout_item) { + ScrollableLayoutItem item = (ScrollableLayoutItem) getItem(position); + ScrollableLayoutItem.LayoutViewHolder vHolder = (ScrollableLayoutItem.LayoutViewHolder) holder; assert item != null; vHolder.mTitle.setSelected(true);//For marquee @@ -113,7 +147,7 @@ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List Log.d("LayoutItem", "LayoutItem configured fullSpan for StaggeredGridLayout"); } - } else if (viewType == R.layout.recycler_label_item) { + } else if (viewType == R.layout.recycler_overall_item) { OverallItem item = (OverallItem) getItem(position); OverallItem.LabelViewHolder vHolder = (OverallItem.LabelViewHolder) holder; assert item != null; @@ -130,6 +164,14 @@ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List vHolder.mIcon.setImageDrawable(item.getIcon()); } } + + // IMPORTANT!!! + // With method B, animateView() needs to be called by the user! + // With method A, the call is handled by the Adapter + animateView(holder, position); + // Same concept for EndlessScrolling and View activation: + // - onLoadMore(position); + // - holder.itemView.setActivated(isSelected(position)); } } \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/ViewPagerActivity.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/ViewPagerActivity.java new file mode 100644 index 00000000..955a7975 --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/ViewPagerActivity.java @@ -0,0 +1,143 @@ +package eu.davidea.samples.flexibleadapter; + +import android.os.Bundle; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.view.OnApplyWindowInsetsListener; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewPager; +import android.support.v4.view.WindowInsetsCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import eu.davidea.samples.flexibleadapter.fragments.FragmentViewPager; +import eu.davidea.samples.flexibleadapter.views.HeaderView; + +public class ViewPagerActivity extends AppCompatActivity { + + /** + * The {@link android.support.v4.view.PagerAdapter} that will provide + * fragments for each of the sections. We use a + * {@link FragmentPagerAdapter} derivative, which will keep every + * loaded fragment in memory. If this becomes too memory intensive, it + * may be best to switch to a + * {@link android.support.v4.app.FragmentStatePagerAdapter}. + */ + private SectionsPagerAdapter mSectionsPagerAdapter; + + /** + * The {@link ViewPager} that will host the section contents. + */ + private ViewPager mViewPager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_pager); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + HeaderView headerView = (HeaderView) findViewById(R.id.toolbar_header_view); + headerView.bindTo(getString(R.string.app_name), getString(R.string.viewpager)); + + // Create the adapter that will return a fragment for each of the three + // primary sections of the activity. + mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); + + // Set up the ViewPager with the sections adapter. + mViewPager = (ViewPager) findViewById(R.id.view_pager); + mViewPager.setAdapter(mSectionsPagerAdapter); + + TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); + tabLayout.setupWithViewPager(mViewPager); + + //Coordinatorlayout Status Bar Padding Disappears From Viewpager 2nd-page + //http://stackoverflow.com/questions/31368781/coordinatorlayout-status-bar-padding-disappears-from-viewpager-2nd-page + ViewCompat.setOnApplyWindowInsetsListener(mViewPager, new OnApplyWindowInsetsListener() { + @Override + public WindowInsetsCompat onApplyWindowInsets(View v, + WindowInsetsCompat insets) { + insets = ViewCompat.onApplyWindowInsets(v, insets); + if (insets.isConsumed()) { + return insets; + } + + boolean consumed = false; + for (int i = 0, count = mViewPager.getChildCount(); i < count; i++) { + ViewCompat.dispatchApplyWindowInsets(mViewPager.getChildAt(i), insets); + if (insets.isConsumed()) { + consumed = true; + } + } + return consumed ? insets.consumeSystemWindowInsets() : insets; + } + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_view_pager, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + if (id == android.R.id.home) { + super.onBackPressed(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + /** + * A {@link FragmentPagerAdapter} that returns a fragment corresponding to + * one of the sections/tabs/pages. + */ + public class SectionsPagerAdapter extends FragmentPagerAdapter { + + public SectionsPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + // getItem is called to instantiate the fragment for the given page. + // Return a FragmentViewPager (defined as a static inner class below). + return FragmentViewPager.newInstance(position + 1); + } + + @Override + public int getCount() { + // Show 3 total pages. + return 3; + } + + @Override + public CharSequence getPageTitle(int position) { + switch (position) { + case 0: + return "SECTION 1"; + case 1: + return "SECTION 2"; + case 2: + return "SECTION 3"; + } + return null; + } + } + +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentAnimators.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentAnimators.java index 7c91a01f..39aa0b79 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentAnimators.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentAnimators.java @@ -37,6 +37,7 @@ import eu.davidea.samples.flexibleadapter.animators.SlideInLeftAnimator; import eu.davidea.samples.flexibleadapter.animators.SlideInRightAnimator; import eu.davidea.samples.flexibleadapter.animators.SlideInUpAnimator; +import eu.davidea.samples.flexibleadapter.items.ScrollableUseCaseItem; import eu.davidea.samples.flexibleadapter.services.DatabaseConfiguration; import eu.davidea.samples.flexibleadapter.services.DatabaseService; @@ -67,29 +68,29 @@ public FragmentAnimators() { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - //Settings for FlipView + // Settings for FlipView FlipView.resetLayoutAnimationDelay(true, 1000L); - //Create New Database and Initialize RecyclerView - DatabaseService.getInstance().createAnimatorsDatabase(20);//N. of sections + // Create New Database and Initialize RecyclerView + DatabaseService.getInstance().createAnimatorsDatabase(20); //N. of sections initializeRecyclerView(savedInstanceState); - //Restore FAB button and icon + // Restore FAB button and icon initializeFab(); - //Settings for FlipView + // Settings for FlipView FlipView.stopLayoutAnimation(); } @SuppressWarnings({"ConstantConditions", "NullableProblems"}) private void initializeRecyclerView(Bundle savedInstanceState) { mAdapter = new ExampleAdapter(DatabaseService.getInstance().getDatabaseList(), getActivity()); - //Experimenting NEW features (v5.0.0) + // Experimenting NEW features (v5.0.0) mAdapter.expandItemsAtStartUp() .setAutoCollapseOnExpand(false) .setAutoScrollOnExpand(true) .setOnlyEntryAnimation(false) - .setAnimationEntryStep(true)//In Overall, watch the effect at initial loading when Grid Layout is set + .setAnimationEntryStep(true) //In Overall, watch the effect at initial loading when Grid Layout is set .setAnimationOnScrolling(DatabaseConfiguration.animateOnScrolling) .setAnimationOnReverseScrolling(true) .setAnimationInterpolator(new DecelerateInterpolator()) @@ -99,24 +100,26 @@ private void initializeRecyclerView(Bundle savedInstanceState) { mRecyclerView.setAdapter(mAdapter); mRecyclerView.setHasFixedSize(true); //Size of RV will not change - //NOTE: Custom item animators inherit 'canReuseUpdatedViewHolder()' from Default Item + // NOTE: Custom item animators inherit 'canReuseUpdatedViewHolder()' from Default Item // Animator. It will return true if a Payload is provided. FlexibleAdapter is actually // sending Payloads onItemChange notifications. mRecyclerView.setItemAnimator(new FlexibleItemAnimator()); initializeSpinnerItemAnimators(); initializeSpinnerScrollAnimators(); - //Experimenting NEW features (v5.0.0) + // Experimenting NEW features (v5.0.0) mAdapter.setSwipeEnabled(true) .getItemTouchHelperCallback() - .setSwipeFlags(ItemTouchHelper.RIGHT);//Enable swipe + .setSwipeFlags(ItemTouchHelper.RIGHT); //Enable swipe SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) getView().findViewById(R.id.swipeRefreshLayout); swipeRefreshLayout.setEnabled(false); mListener.onFragmentChange(swipeRefreshLayout, mRecyclerView, SelectableAdapter.MODE_IDLE); - //Add sample HeaderView items on the top (not belongs to the library) - mAdapter.showLayoutInfo(savedInstanceState == null); + // Add 1 Scrollable Header + mAdapter.addScrollableHeader(new ScrollableUseCaseItem( + getString(R.string.animator_use_case_title), + getString(R.string.animator_use_case_description))); } @Override @@ -130,7 +133,7 @@ public void performFabAction() { @Override public void showNewLayoutInfo(MenuItem item) { super.showNewLayoutInfo(item); - mAdapter.showLayoutInfo(true); + mAdapter.showLayoutInfo(false); } @Override @@ -238,7 +241,7 @@ public enum AnimatorType { FadeInRight(new FadeInRightAnimator(new OvershootInterpolator(1f))), Landing(new LandingAnimator(new OvershootInterpolator(1f))), ScaleIn(new ScaleInAnimator(new OvershootInterpolator(1f))), - FlipInTopX(new FlipInTopXAnimator(new DecelerateInterpolator(1f))),//Makes use of index inside + FlipInTopX(new FlipInTopXAnimator(new DecelerateInterpolator(1f))), //Makes use of index inside FlipInBottomX(new FlipInBottomXAnimator(new OvershootInterpolator(1f))), SlideInLeft(new SlideInLeftAnimator(new OvershootInterpolator(1f))), SlideInRight(new SlideInRightAnimator(new OvershootInterpolator(1f))), diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentAsyncFilter.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentAsyncFilter.java index 8638c8a0..ef90cccc 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentAsyncFilter.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentAsyncFilter.java @@ -101,9 +101,13 @@ private void initializeRecyclerView() { FlipView.resetLayoutAnimationDelay(true, 1000L); //Experimenting NEW features (v5.0.0) - mAdapter.setAnimateToLimit(DatabaseConfiguration.animateToLimit)//Size limit = MAX_VALUE will always animate the changes + mAdapter.setAnimateChangesWithDiffUtil(DatabaseConfiguration.animateWithDiffUtil) + .setAnimateToLimit(DatabaseConfiguration.animateToLimit)//Size limit = MAX_VALUE will always animate the changes .setNotifyMoveOfFilteredItems(DatabaseConfiguration.notifyMove)//When true, filtering on big list is very slow! .setNotifyChangeOfUnfilteredItems(DatabaseConfiguration.notifyChange)//We have highlighted text while filtering, so let's enable this feature to be consistent with the active filter + .setAnimationInitialDelay(100L) + .setAnimationOnScrolling(true) + .setAnimationOnReverseScrolling(true) .setOnlyEntryAnimation(true); if (mRecyclerView == null) { mRecyclerView = (RecyclerView) getView().findViewById(R.id.recycler_view); @@ -128,7 +132,7 @@ private void initializeRecyclerView() { } else { mFab.setImageResource(R.drawable.ic_settings_white_24dp); mRecyclerView.removeItemDecoration(mDivider); - mAdapter.setFastScroller((FastScroller) getActivity().findViewById(R.id.fast_scroller), + mAdapter.setFastScroller((FastScroller) getView().findViewById(R.id.fast_scroller), Utils.getColorAccent(getActivity()), (MainActivity) getActivity()); } diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentEndlessScrolling.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentEndlessScrolling.java index c028d45d..a82393cd 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentEndlessScrolling.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentEndlessScrolling.java @@ -3,7 +3,6 @@ import android.os.Bundle; import android.os.Handler; import android.support.v4.widget.SwipeRefreshLayout; -import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; @@ -18,6 +17,7 @@ import eu.davidea.fastscroller.FastScroller; import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.Payload; import eu.davidea.flexibleadapter.SelectableAdapter; import eu.davidea.flexibleadapter.common.SmoothScrollGridLayoutManager; import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; @@ -25,7 +25,8 @@ import eu.davidea.samples.flexibleadapter.ExampleAdapter; import eu.davidea.samples.flexibleadapter.MainActivity; import eu.davidea.samples.flexibleadapter.R; -import eu.davidea.samples.flexibleadapter.models.ProgressItem; +import eu.davidea.samples.flexibleadapter.animators.FadeInDownAnimator; +import eu.davidea.samples.flexibleadapter.items.ProgressItem; import eu.davidea.samples.flexibleadapter.services.DatabaseConfiguration; import eu.davidea.samples.flexibleadapter.services.DatabaseService; import eu.davidea.utils.Utils; @@ -42,6 +43,7 @@ public class FragmentEndlessScrolling extends AbstractFragment public static final String TAG = FragmentEndlessScrolling.class.getSimpleName(); private ExampleAdapter mAdapter; + private ProgressItem mProgressItem = new ProgressItem(); public static FragmentEndlessScrolling newInstance(int columnCount) { FragmentEndlessScrolling fragment = new FragmentEndlessScrolling(); @@ -61,120 +63,152 @@ public FragmentEndlessScrolling() { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - //Settings for FlipView + // Settings for FlipView FlipView.resetLayoutAnimationDelay(true, 1000L); - //Create New Database and Initialize RecyclerView - DatabaseService.getInstance().createEndlessDatabase(100);//N. of items + // Create New Database and Initialize RecyclerView + if (savedInstanceState == null) { + DatabaseService.getInstance().createEndlessDatabase(0); //N. of items + } initializeRecyclerView(savedInstanceState); - //Settings for FlipView + // Settings for FlipView FlipView.stopLayoutAnimation(); } @SuppressWarnings({"ConstantConditions", "NullableProblems"}) private void initializeRecyclerView(Bundle savedInstanceState) { - //Initialize Adapter and RecyclerView - //ExampleAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' + // Initialize Adapter and RecyclerView + // ExampleAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' mAdapter = new ExampleAdapter(DatabaseService.getInstance().getDatabaseList(), getActivity()); - //Experimenting NEW features (v5.0.0) + // Experimenting NEW features (v5.0.0) mAdapter.setAutoScrollOnExpand(true) - .setHandleDragEnabled(true) - //.setAnimateToLimit(Integer.MAX_VALUE)//Use the default value - .setNotifyMoveOfFilteredItems(true)//When true, filtering on big list is very slow, not in this case! - .setNotifyChangeOfUnfilteredItems(true)//We have highlighted text while filtering, so let's enable this feature to be consistent with the active filter + //.setAnimateToLimit(Integer.MAX_VALUE) //Use the default value + .setNotifyMoveOfFilteredItems(true) //When true, filtering on big list is very slow, not in this case! + .setNotifyChangeOfUnfilteredItems(true) //We have highlighted text while filtering, so let's enable this feature to be consistent with the active filter .setAnimationOnScrolling(DatabaseConfiguration.animateOnScrolling) .setAnimationOnReverseScrolling(true); mRecyclerView = (RecyclerView) getView().findViewById(R.id.recycler_view); mRecyclerView.setLayoutManager(createNewLinearLayoutManager()); mRecyclerView.setAdapter(mAdapter); mRecyclerView.setHasFixedSize(true); //Size of RV will not change - //NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if - // a Payload is provided. FlexibleAdapter is actually sending Payloads onItemChange. - mRecyclerView.setItemAnimator(new DefaultItemAnimator()); + // NOTE: Use the custom FadeInDownAnimator for ALL notifications for ALL items, + // but ScrollableFooterItem implements AnimatedViewHolder with a unique animation: SlideInUp! + mRecyclerView.setItemAnimator(new FadeInDownAnimator()); - //Add FastScroll to the RecyclerView, after the Adapter has been attached the RecyclerView!!! - mAdapter.setFastScroller((FastScroller) getActivity().findViewById(R.id.fast_scroller), + // Add FastScroll to the RecyclerView, after the Adapter has been attached the RecyclerView!!! + mAdapter.setFastScroller((FastScroller) getView().findViewById(R.id.fast_scroller), Utils.getColorAccent(getActivity()), (MainActivity) getActivity()); - //Experimenting NEW features (v5.0.0) - mAdapter.setLongPressDragEnabled(true);//Enable long press to drag items - mAdapter.setSwipeEnabled(true);//Enable swipe items - mAdapter.setDisplayHeadersAtStartUp(true);//Show Headers at startUp! + // Experimenting NEW features (v5.0.0) + mAdapter.setLongPressDragEnabled(true) //Enable long press to drag items + .setHandleDragEnabled(true) //Enable drag using handle view + .setSwipeEnabled(true); //Enable swipe items SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) getView().findViewById(R.id.swipeRefreshLayout); swipeRefreshLayout.setEnabled(true); mListener.onFragmentChange(swipeRefreshLayout, mRecyclerView, SelectableAdapter.MODE_IDLE); - //EndlessScrollListener - OnLoadMore (v5.0.0) - mAdapter.setEndlessScrollListener(this, new ProgressItem()); - mAdapter.setEndlessScrollThreshold(1);//Default=1 + // EndlessScrollListener - OnLoadMore (v5.0.0) + mAdapter.setEndlessScrollListener(this, mProgressItem) + //.setEndlessPageSize(3) //Endless is automatically disabled if newItems < 3 + .setEndlessTargetCount(15); //Endless is automatically disabled if totalItems >= 15 + //.setEndlessScrollThreshold(1); //Default=1 - //Add sample HeaderView items on the top (not belongs to the library) - mAdapter.addUserLearnedSelection(savedInstanceState == null); + // Add 1 Scrollable Header and 1 Footer items mAdapter.showLayoutInfo(savedInstanceState == null); + mAdapter.addScrollableFooter(); } @Override public void showNewLayoutInfo(MenuItem item) { super.showNewLayoutInfo(item); - mAdapter.showLayoutInfo(true); + mAdapter.showLayoutInfo(false); + } + + /** + * No more data to load. + *

This method is called if any limit is reached (targetCount or pageSize + * must be set) AND if new data is temporary unavailable (ex. no connection or no + * new updates remotely). If no new data, a {@link FlexibleAdapter#notifyItemChanged(int, Object)} + * with a payload {@link Payload#NO_MORE_LOAD} is triggered on the progressItem.

+ * + * @param newItemsSize the last size of the new items loaded + * @see FlexibleAdapter#setEndlessTargetCount(int) + * @see FlexibleAdapter#setEndlessPageSize(int) + * @since 5.0.0-rc1 + */ + @Override + public void noMoreLoad(int newItemsSize) { + Log.d(TAG, "newItemsSize=" + newItemsSize); + Log.d(TAG, "Total pages loaded=" + mAdapter.getEndlessCurrentPage()); + Log.d(TAG, "Total items loaded=" + mAdapter.getMainItemCount()); } /** * Loads more data. + *

Use {@code lastPosition} and {@code currentPage} to know what to load next.

+ * {@code lastPosition} is the count of the main items without Scrollable Headers. + * + * @param lastPosition the position of the last main item in the adapter + * @param currentPage the current page + * @since 5.0.0-b6 + *
5.0.0-rc1 added {@code lastPosition} and {@code currentPage} as parameters */ @Override - public void onLoadMore() { - //We don't want load more items when searching into the current Collection! - //Alternatively, for a special filter, if we want load more items when filter is active, the + public void onLoadMore(int lastPosition, int currentPage) { + // We don't want load more items when searching into the current Collection! + // Alternatively, for a special filter, if we want load more items when filter is active, the // new items that arrive from remote, should be already filtered, before adding them to the Adapter! if (mAdapter.hasSearchText()) { mAdapter.onLoadMoreComplete(null); return; } - Log.i(TAG, "onLoadMore invoked!"); - //Simulating asynchronous call + // Simulating asynchronous call new Handler().postDelayed(new Runnable() { @Override public void run() { final List newItems = new ArrayList<>(); - //Simulating success/failure - int count = new Random().nextInt(5); - int totalItemsOfType = mAdapter.getItemCountOfTypes(R.layout.recycler_expandable_item); + // 1. Simulating success/failure with Random + int count = new Random().nextInt(7); + int totalItemsOfType = mAdapter.getItemCountOfTypes(R.layout.recycler_simple_item); for (int i = 1; i <= count; i++) { - if (i % 2 != 0) { - newItems.add(DatabaseService.newSimpleItem(totalItemsOfType + i, null)); - } else { - newItems.add(DatabaseService.newExpandableItem(totalItemsOfType + i, null)); - } + newItems.add(DatabaseService.newSimpleItem(totalItemsOfType + i, null)); } - //Callback the Adapter to notify the change: - //- New items will be added to the end of the list - //- When list is null or empty, ProgressItem will be hidden - mAdapter.onLoadMoreComplete(newItems); + // 2. Callback the Adapter to notify the change: + // - New items will be added to the end of the main list + // - When list is null or empty and limits are reached, Endless scroll will be disabled. + // To enable again, you must call setEndlessProgressItem(@Nullable T progressItem). + mAdapter.onLoadMoreComplete(newItems, 5000L); DatabaseService.getInstance().addAll(newItems); + // - Retrieve the new page number after adding new items! + Log.d(TAG, "EndlessCurrentPage=" + mAdapter.getEndlessCurrentPage()); + Log.d(TAG, "EndlessPageSize=" + mAdapter.getEndlessPageSize()); + Log.d(TAG, "EndlessTargetCount=" + mAdapter.getEndlessTargetCount()); - //Expand all Expandable items: Not Expandable items are automatically skipped/ignored! + // 3. If you have new Expandable and you want expand them, do as following: + // Note: normal items are automatically skipped/ignored because they do not + // implement IExpandable interface! So don't care about them. for (AbstractFlexibleItem item : newItems) { - //Simple expansion is performed: - // - Automatic scroll is performed - //mAdapter.expand(item); - - //Initialization is performed: - // - Expanded status is ignored(WARNING: possible subItem duplication) + // Option A. (Best use case) Initialization is performed: + // - Expanded status is ignored. WARNING: possible subItems duplication! // - Automatic scroll is skipped mAdapter.expand(item, true); + + // Option B. Simple expansion is performed: + // - WARNING: Automatic scroll is performed! + //mAdapter.expand(item); } - //Notify user - String message = (newItems.size() > 0 ? - "Simulated: " + newItems.size() + " new items arrived :-)" : - "Simulated: No more items to load :-("); - Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + // 4. Notify user + if (getActivity() != null && newItems.size() > 0) { + Toast.makeText(getActivity(), + "Simulated: " + newItems.size() + " new items arrived :-)", + Toast.LENGTH_SHORT).show(); + } } - }, 2500); + }, 4000L); } @Override @@ -212,11 +246,14 @@ protected GridLayoutManager createNewGridLayoutManager() { gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { - //NOTE: If you use simple integer to identify the ViewType, - //here, you should use them and not Layout integers + // NOTE: If you use simple integers to identify the ViewType, + // here, you should use them and not Layout integers switch (mAdapter.getItemViewType(position)) { - case R.layout.recycler_layout_item: - case R.layout.recycler_uls_item: + case R.layout.recycler_scrollable_expandable_item: + case R.layout.recycler_scrollable_header_item: + case R.layout.recycler_scrollable_footer_item: + case R.layout.recycler_scrollable_layout_item: + case R.layout.recycler_scrollable_uls_item: case R.layout.progress_item: return mColumnCount; default: diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentExpandableMultiLevel.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentExpandableMultiLevel.java index 6f7f8967..d9fad955 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentExpandableMultiLevel.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentExpandableMultiLevel.java @@ -49,50 +49,49 @@ public FragmentExpandableMultiLevel() { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - //Settings for FlipView + // Settings for FlipView FlipView.resetLayoutAnimationDelay(true, 1000L); - //Create New Database and Initialize RecyclerView + // Create New Database and Initialize RecyclerView DatabaseService.getInstance().createExpandableMultiLevelDatabase(50); initializeRecyclerView(savedInstanceState); - //Settings for FlipView + // Settings for FlipView FlipView.stopLayoutAnimation(); } @SuppressWarnings({"ConstantConditions", "NullableProblems"}) private void initializeRecyclerView(Bundle savedInstanceState) { - //Initialize Adapter and RecyclerView - //ExampleAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' + // Initialize Adapter and RecyclerView + // ExampleAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' mAdapter = new ExampleAdapter(DatabaseService.getInstance().getDatabaseList(), getActivity()); - //Experimenting NEW features (v5.0.0) + // Experimenting NEW features (v5.0.0) mAdapter.expandItemsAtStartUp() .setAutoCollapseOnExpand(false) - .setMinCollapsibleLevel(1)//Auto-collapse only items with level >= 1 (avoid to collapse also sections!) - .setAutoScrollOnExpand(true) - .setRemoveOrphanHeaders(false); + .setMinCollapsibleLevel(1) //Auto-collapse only items with level >= 1 (avoid to collapse also sections!) + .setAutoScrollOnExpand(true); mRecyclerView = (RecyclerView) getView().findViewById(R.id.recycler_view); mRecyclerView.setLayoutManager(createNewLinearLayoutManager()); mRecyclerView.setAdapter(mAdapter); mRecyclerView.setHasFixedSize(true); //Size of RV will not change - //NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if + // NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if // a Payload is provided. FlexibleAdapter is actually sending Payloads onItemChange. mRecyclerView.setItemAnimator(new DefaultItemAnimator()); - //Add FastScroll to the RecyclerView, after the Adapter has been attached the RecyclerView!!! - mAdapter.setFastScroller((FastScroller) getActivity().findViewById(R.id.fast_scroller), + // Add FastScroll to the RecyclerView, after the Adapter has been attached the RecyclerView!!! + mAdapter.setFastScroller((FastScroller) getView().findViewById(R.id.fast_scroller), Utils.getColorAccent(getActivity()), (MainActivity) getActivity()); - //Experimenting NEW features (v5.0.0) - mAdapter.setLongPressDragEnabled(true)//Enable long press to drag items - .setHandleDragEnabled(true)//Enable handle drag - .setSwipeEnabled(true)//Enable swipe items - .setDisplayHeadersAtStartUp(true);//Show Headers at startUp! + // Experimenting NEW features (v5.0.0) + mAdapter.setLongPressDragEnabled(true) //Enable long press to drag items + .setHandleDragEnabled(true) //Enable handle drag + .setSwipeEnabled(true); //Enable swipe items + //.setDisplayHeadersAtStartUp(true); //Show Headers at startUp: (not necessary if Headers are also Expandable) SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) getView().findViewById(R.id.swipeRefreshLayout); swipeRefreshLayout.setEnabled(true); mListener.onFragmentChange(swipeRefreshLayout, mRecyclerView, SelectableAdapter.MODE_IDLE); - //Add sample HeaderView items on the top (not belongs to the library) + // Add 2 Scrollable Headers mAdapter.addUserLearnedSelection(savedInstanceState == null); mAdapter.showLayoutInfo(savedInstanceState == null); } @@ -100,7 +99,7 @@ private void initializeRecyclerView(Bundle savedInstanceState) { @Override public void showNewLayoutInfo(MenuItem item) { super.showNewLayoutInfo(item); - mAdapter.showLayoutInfo(true); + mAdapter.showLayoutInfo(false); } @Override @@ -109,11 +108,11 @@ protected GridLayoutManager createNewGridLayoutManager() { gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { - //NOTE: If you use simple integer to identify the ViewType, - //here, you should use them and not Layout integers + // NOTE: If you use simple integers to identify the ViewType, + // here, you should use them and not Layout integers switch (mAdapter.getItemViewType(position)) { - case R.layout.recycler_layout_item: - case R.layout.recycler_uls_item: + case R.layout.recycler_scrollable_layout_item: + case R.layout.recycler_scrollable_uls_item: case R.layout.recycler_header_item: case R.layout.recycler_expandable_header_item: case R.layout.recycler_expandable_item: diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentExpandableSections.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentExpandableSections.java index 8b410f67..599d4eb8 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentExpandableSections.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentExpandableSections.java @@ -51,63 +51,62 @@ public FragmentExpandableSections() { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - //Settings for FlipView + // Settings for FlipView FlipView.resetLayoutAnimationDelay(true, 1000L); - //Create New Database and Initialize RecyclerView - DatabaseService.getInstance().createExpandableSectionsDatabase(100);//N. of sections + // Create New Database and Initialize RecyclerView + DatabaseService.getInstance().createExpandableSectionsDatabase(100); //N. of sections initializeRecyclerView(savedInstanceState); - //Settings for FlipView + // Settings for FlipView FlipView.stopLayoutAnimation(); } @SuppressWarnings({"ConstantConditions", "NullableProblems"}) private void initializeRecyclerView(Bundle savedInstanceState) { - //Initialize Adapter and RecyclerView - //ExampleAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' + // Initialize Adapter and RecyclerView + // ExampleAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' mAdapter = new ExampleAdapter(DatabaseService.getInstance().getDatabaseList(), getActivity()); - //Experimenting NEW features (v5.0.0) + // Experimenting NEW features (v5.0.0) mAdapter.expandItemsAtStartUp() .setAutoCollapseOnExpand(false) .setAutoScrollOnExpand(true) - .setAnimateToLimit(Integer.MAX_VALUE)//Size limit = MAX_VALUE will always animate the changes - .setNotifyMoveOfFilteredItems(false)//When true, filtering on big list is very slow! - .setNotifyChangeOfUnfilteredItems(true)//We have highlighted text while filtering, so let's enable this feature to be consistent with the active filter - .setRemoveOrphanHeaders(false) + .setAnimateToLimit(Integer.MAX_VALUE) //Size limit = MAX_VALUE will always animate the changes + .setNotifyMoveOfFilteredItems(false) //When true, filtering on big list is very slow! + .setNotifyChangeOfUnfilteredItems(true) //We have highlighted text while filtering, so let's enable this feature to be consistent with the active filter .setAnimationOnScrolling(DatabaseConfiguration.animateOnScrolling) .setAnimationOnReverseScrolling(true); mRecyclerView = (RecyclerView) getView().findViewById(R.id.recycler_view); mRecyclerView.setLayoutManager(createNewLinearLayoutManager()); mRecyclerView.setAdapter(mAdapter); mRecyclerView.setHasFixedSize(true); //Size of RV will not change - //NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if + // NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if // a Payload is provided. FlexibleAdapter is actually sending Payloads onItemChange. mRecyclerView.setItemAnimator(new DefaultItemAnimator()); - //Custom divider item decorator + // Custom divider item decorator mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), - R.drawable.divider, 0));//Increase to add gap between sections (Works only with LinearLayout!) + R.drawable.divider, 0)); //Increase to add gap between sections (Works only with LinearLayout!) - //Add FastScroll to the RecyclerView, after the Adapter has been attached the RecyclerView!!! - mAdapter.setFastScroller((FastScroller) getActivity().findViewById(R.id.fast_scroller), + // Add FastScroll to the RecyclerView, after the Adapter has been attached the RecyclerView!!! + mAdapter.setFastScroller((FastScroller) getView().findViewById(R.id.fast_scroller), Utils.getColorAccent(getActivity()), (MainActivity) getActivity()); - //Experimenting NEW features (v5.0.0) - mAdapter.setLongPressDragEnabled(true)//Enable long press to drag items - .setHandleDragEnabled(true);//Enable handle drag - //.setDisplayHeadersAtStartUp(true);//Show Headers at startUp! (not necessary if Headers are also Expandable) + // Experimenting NEW features (v5.0.0) + mAdapter.setLongPressDragEnabled(true) //Enable long press to drag items + .setHandleDragEnabled(true); //Enable handle drag + //.setDisplayHeadersAtStartUp(true); //Show Headers at startUp: (not necessary if Headers are also Expandable) SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) getView().findViewById(R.id.swipeRefreshLayout); swipeRefreshLayout.setEnabled(true); mListener.onFragmentChange(swipeRefreshLayout, mRecyclerView, SelectableAdapter.MODE_IDLE); - //Add sample HeaderView items on the top (not belongs to the library) + // Add 1 Scrollable Header mAdapter.showLayoutInfo(savedInstanceState == null); } @Override public void showNewLayoutInfo(MenuItem item) { super.showNewLayoutInfo(item); - mAdapter.showLayoutInfo(true); + mAdapter.showLayoutInfo(false); } @Override @@ -116,11 +115,11 @@ protected GridLayoutManager createNewGridLayoutManager() { gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { - //NOTE: If you use simple integer to identify the ViewType, - //here, you should use them and not Layout integers + // NOTE: If you use simple integers to identify the ViewType, + // here, you should use them and not Layout integers switch (mAdapter.getItemViewType(position)) { - case R.layout.recycler_layout_item: - case R.layout.recycler_uls_item: + case R.layout.recycler_scrollable_layout_item: + case R.layout.recycler_scrollable_uls_item: case R.layout.recycler_header_item: case R.layout.recycler_expandable_header_item: case R.layout.recycler_expandable_item: diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentHeadersSections.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentHeadersSections.java index d875de35..6f5e119d 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentHeadersSections.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentHeadersSections.java @@ -23,7 +23,8 @@ import eu.davidea.samples.flexibleadapter.R; import eu.davidea.samples.flexibleadapter.dialogs.BottomSheetDialog; import eu.davidea.samples.flexibleadapter.dialogs.OnParameterSelectedListener; -import eu.davidea.samples.flexibleadapter.models.ExpandableHeaderItem; +import eu.davidea.samples.flexibleadapter.items.ExpandableHeaderItem; +import eu.davidea.samples.flexibleadapter.items.ScrollableUseCaseItem; import eu.davidea.samples.flexibleadapter.services.DatabaseConfiguration; import eu.davidea.samples.flexibleadapter.services.DatabaseService; import eu.davidea.utils.Utils; @@ -62,59 +63,62 @@ public FragmentHeadersSections() { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - //Settings for FlipView + // Settings for FlipView FlipView.resetLayoutAnimationDelay(true, 1000L); - //Create New Database and Initialize RecyclerView + // Create New Database and Initialize RecyclerView DatabaseService.getInstance().createHeadersSectionsDatabase(400, 100); initializeRecyclerView(savedInstanceState); - //Restore FAB button and icon + // Restore FAB button and icon initializeFab(); - //Settings for FlipView + // Settings for FlipView FlipView.stopLayoutAnimation(); } @SuppressWarnings({"ConstantConditions", "NullableProblems"}) private void initializeRecyclerView(Bundle savedInstanceState) { - //Initialize Adapter and RecyclerView - //ExampleAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' + // Initialize Adapter and RecyclerView + // ExampleAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' mAdapter = new ExampleAdapter(DatabaseService.getInstance().getDatabaseList(), getActivity()); - //Experimenting NEW features (v5.0.0) - mAdapter.setRemoveOrphanHeaders(false) - .setNotifyChangeOfUnfilteredItems(true)//We have highlighted text while filtering, so let's enable this feature to be consistent with the active filter + // Experimenting NEW features (v5.0.0) + mAdapter.setNotifyChangeOfUnfilteredItems(true) //We have highlighted text while filtering, so let's enable this feature to be consistent with the active filter .setAnimationOnScrolling(DatabaseConfiguration.animateOnScrolling); mRecyclerView = (RecyclerView) getView().findViewById(R.id.recycler_view); mRecyclerView.setLayoutManager(createNewLinearLayoutManager()); mRecyclerView.setAdapter(mAdapter); mRecyclerView.setHasFixedSize(true); //Size of RV will not change - //NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if + // NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if // a Payload is provided. FlexibleAdapter is actually sending Payloads onItemChange. mRecyclerView.setItemAnimator(new DefaultItemAnimator()); - //Add FastScroll to the RecyclerView, after the Adapter has been attached the RecyclerView!!! - mAdapter.setFastScroller((FastScroller) getActivity().findViewById(R.id.fast_scroller), + // Add FastScroll to the RecyclerView, after the Adapter has been attached the RecyclerView!!! + mAdapter.setFastScroller((FastScroller) getView().findViewById(R.id.fast_scroller), Utils.getColorAccent(getActivity()), (MainActivity) getActivity()); mAdapter.setLongPressDragEnabled(true) .setHandleDragEnabled(true) .setSwipeEnabled(true) .setUnlinkAllItemsOnRemoveHeaders(true) - //Show Headers at startUp, 1st call, correctly executed, no warning log message! + // Show Headers at startUp, 1st call, correctly executed, no warning log message! .setDisplayHeadersAtStartUp(true) - .enableStickyHeaders() - //Simulate developer 2nd call mistake, now it's safe, not executed, no warning log message! + .setStickyHeaders(true) + // Simulate developer 2nd call mistake, now it's safe, not executed, no warning log message! .setDisplayHeadersAtStartUp(true) - //Simulate developer 3rd call mistake, still safe, not executed, warning log message displayed! + // Simulate developer 3rd call mistake, still safe, not executed, warning log message displayed! .showAllHeaders(); SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) getView().findViewById(R.id.swipeRefreshLayout); swipeRefreshLayout.setEnabled(true); mListener.onFragmentChange(swipeRefreshLayout, mRecyclerView, SelectableAdapter.MODE_IDLE); - //Add sample HeaderView items on the top (not belongs to the library) - //mAdapter.addUserLearnedSelection(savedInstanceState == null); + // Add 3 Scrollable Headers and 1 Footer + mAdapter.addUserLearnedSelection(savedInstanceState == null); + mAdapter.addScrollableHeaderWithDelay(new ScrollableUseCaseItem( + getString(R.string.headers_sections_use_case_title), + getString(R.string.headers_sections_use_case_description)), 900L, false); mAdapter.showLayoutInfo(savedInstanceState == null); + mAdapter.addScrollableFooter(); } @Override @@ -126,7 +130,7 @@ public void performFabAction() { @Override public void showNewLayoutInfo(MenuItem item) { super.showNewLayoutInfo(item); - mAdapter.showLayoutInfo(true); + mAdapter.showLayoutInfo(false); } @Override @@ -164,7 +168,7 @@ public void onParameterSelected(int itemType, int referencePosition, int childPo scrollTo = mAdapter.getGlobalPositionOf(referenceHeader); } - //With Sticky Headers enabled, this seems necessary to give + // With Sticky Headers enabled, this seems necessary to give // time at the RV to be in correct state before scrolling final int scrollToFinal = scrollTo; mRecyclerView.post(new Runnable() { @@ -186,11 +190,14 @@ protected GridLayoutManager createNewGridLayoutManager() { gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { - //NOTE: If you use simple integer to identify the ViewType, - //here, you should use them and not Layout integers + // NOTE: If you use simple integers to identify the ViewType, + // here, you should use them and not Layout integers switch (mAdapter.getItemViewType(position)) { - case R.layout.recycler_layout_item: - case R.layout.recycler_uls_item: + case R.layout.recycler_scrollable_usecase_item: + case R.layout.recycler_scrollable_header_item: + case R.layout.recycler_scrollable_footer_item: + case R.layout.recycler_scrollable_layout_item: + case R.layout.recycler_scrollable_uls_item: case R.layout.recycler_header_item: case R.layout.recycler_expandable_header_item: return mColumnCount; diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentHolderSections.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentHolderSections.java new file mode 100644 index 00000000..c4a63e2f --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentHolderSections.java @@ -0,0 +1,98 @@ +package eu.davidea.samples.flexibleadapter.fragments; + +import android.os.Bundle; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.DefaultItemAnimator; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; + +import eu.davidea.fastscroller.FastScroller; +import eu.davidea.flexibleadapter.SelectableAdapter; +import eu.davidea.samples.flexibleadapter.ExampleAdapter; +import eu.davidea.samples.flexibleadapter.MainActivity; +import eu.davidea.samples.flexibleadapter.R; +import eu.davidea.samples.flexibleadapter.items.ScrollableUseCaseItem; +import eu.davidea.samples.flexibleadapter.services.DatabaseService; +import eu.davidea.utils.Utils; + +/** + * A fragment representing a list of Holder Items. + * Activities containing this fragment MUST implement the {@link OnFragmentInteractionListener} + * interface. + */ +public class FragmentHolderSections extends AbstractFragment { + + public static final String TAG = FragmentHolderSections.class.getSimpleName(); + + /** + * Custom implementation of FlexibleAdapter + */ + private ExampleAdapter mAdapter; + + + public static FragmentHolderSections newInstance() { + return new FragmentHolderSections(); + } + + /** + * Mandatory empty constructor for the fragment manager to instantiate the + * fragment (e.g. upon screen orientation changes). + */ + public FragmentHolderSections() { + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Create New Database and Initialize RecyclerView + DatabaseService.getInstance().createHolderSectionsDatabase(50, 10); + initializeRecyclerView(savedInstanceState); + } + + @SuppressWarnings({"ConstantConditions", "NullableProblems"}) + private void initializeRecyclerView(Bundle savedInstanceState) { + // Initialize Adapter and RecyclerView + // ExampleAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' + mAdapter = new ExampleAdapter(DatabaseService.getInstance().getDatabaseList(), getActivity()); + + mRecyclerView = (RecyclerView) getView().findViewById(R.id.recycler_view); + mRecyclerView.setLayoutManager(createNewLinearLayoutManager()); + mRecyclerView.setAdapter(mAdapter); + mRecyclerView.setHasFixedSize(true); //Size of RV will not change + // NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if + // a Payload is provided. FlexibleAdapter is actually sending Payloads onItemChange. + mRecyclerView.setItemAnimator(new DefaultItemAnimator()); + + // Add FastScroll to the RecyclerView, after the Adapter has been attached the RecyclerView!!! + mAdapter.setFastScroller((FastScroller) getView().findViewById(R.id.fast_scroller), + Utils.getColorAccent(getActivity()), (MainActivity) getActivity()); + mAdapter.setDisplayHeadersAtStartUp(true) + .setStickyHeaders(true) + .setOnlyEntryAnimation(true); + + SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) getView().findViewById(R.id.swipeRefreshLayout); + swipeRefreshLayout.setEnabled(true); + mListener.onFragmentChange(swipeRefreshLayout, mRecyclerView, SelectableAdapter.MODE_IDLE); + + // Add 1 Scrollable Header + mAdapter.addScrollableHeader(new ScrollableUseCaseItem( + getString(R.string.model_holders_use_case_title), + getString(R.string.model_holders_use_case_description))); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + Log.v(TAG, "onCreateOptionsMenu called!"); + inflater.inflate(R.menu.menu_holders, menu); + mListener.initSearchView(menu); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + } + +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentInstagramHeaders.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentInstagramHeaders.java index d6c37fa3..ca3076de 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentInstagramHeaders.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentInstagramHeaders.java @@ -14,12 +14,13 @@ import java.util.List; import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.Payload; import eu.davidea.flexibleadapter.SelectableAdapter; import eu.davidea.flexibleadapter.common.DividerItemDecoration; import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; import eu.davidea.flipview.FlipView; import eu.davidea.samples.flexibleadapter.R; -import eu.davidea.samples.flexibleadapter.models.ProgressItem; +import eu.davidea.samples.flexibleadapter.items.ProgressItem; import eu.davidea.samples.flexibleadapter.services.DatabaseService; /** @@ -52,75 +53,107 @@ public FragmentInstagramHeaders() { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - //Settings for FlipView + // Settings for FlipView FlipView.resetLayoutAnimationDelay(true, 1000L); - //Create New Database and Initialize RecyclerView + // Create New Database and Initialize RecyclerView DatabaseService.getInstance().createInstagramHeadersDatabase(15); - initializeRecyclerView(savedInstanceState); + initializeRecyclerView(); - //Settings for FlipView + // Settings for FlipView FlipView.stopLayoutAnimation(); } @SuppressWarnings({"unchecked", "ConstantConditions"}) - private void initializeRecyclerView(Bundle savedInstanceState) { - //Initialize Adapter and RecyclerView - //true = it makes use of stableIds, I strongly suggest to implement 'item.hashCode()' + private void initializeRecyclerView() { + // Initialize Adapter and RecyclerView + // true = it makes use of stableIds, I strongly suggest to implement 'item.hashCode()' mAdapter = new FlexibleAdapter<>(DatabaseService.getInstance().getDatabaseList(), getActivity(), true); - mAdapter.initializeListeners(getActivity()) - //Experimenting NEW features (v5.0.0) + mAdapter.addListener(getActivity()) + // Experimenting NEW features (v5.0.0) .setAnimationOnScrolling(true) .setAnimationOnReverseScrolling(true); mRecyclerView = (RecyclerView) getView().findViewById(R.id.recycler_view); mRecyclerView.setLayoutManager(createNewLinearLayoutManager()); mRecyclerView.setAdapter(mAdapter); mRecyclerView.setHasFixedSize(true); //Size of RV will not change - //NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if + // NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if // a Payload is provided. FlexibleAdapter is actually sending Payloads onItemChange. mRecyclerView.setItemAnimator(new DefaultItemAnimator()); - //Custom divider item decorator with 24dpi as empty space between sections + // Custom divider item decorator with 24dpi as empty space between sections mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), 0, 24)); - mAdapter.setDisplayHeadersAtStartUp(true)//Show Headers at startUp! - .enableStickyHeaders()//Make headers sticky - //Endless scroll with 1 item threshold + mAdapter.setDisplayHeadersAtStartUp(true) //Show Headers at startUp! + .setStickyHeaders(true) //Make headers sticky + // Endless scroll with 1 item threshold .setEndlessScrollListener(this, new ProgressItem()) - .setEndlessScrollThreshold(1);//Default=1 + .setEndlessScrollThreshold(1); //Default=1 SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) getView().findViewById(R.id.swipeRefreshLayout); swipeRefreshLayout.setEnabled(true); mListener.onFragmentChange(swipeRefreshLayout, mRecyclerView, SelectableAdapter.MODE_IDLE); } + /** + * No more data to load. + *

This method is called if any limit is reached (targetCount or pageSize + * must be set) AND if new data is temporary unavailable (ex. no connection or no + * new updates remotely). If no new data, a {@link FlexibleAdapter#notifyItemChanged(int, Object)} + * with a payload {@link Payload#NO_MORE_LOAD} is triggered on the progressItem.

+ * + * @param newItemsSize the last size of the new items loaded + * @see FlexibleAdapter#setEndlessTargetCount(int) + * @see FlexibleAdapter#setEndlessPageSize(int) + * @since 5.0.0-rc1 + */ + @Override + public void noMoreLoad(int newItemsSize) { + // This method will never be called if No limits are set and loading more will always + // produce new items (as this example does) + } + /** * Loads more data. + *

Use {@code lastPosition} and {@code currentPage} to know what to load next.

+ * {@code lastPosition} is the count of the main items without Scrollable Headers. + * + * @param lastPosition the position of the last main item in the adapter + * @param currentPage the current page + * @since 5.0.0-b6 + *
5.0.0-rc1 added {@code lastPosition} and {@code currentPage} as parameters */ @Override - public void onLoadMore() { + public void onLoadMore(int lastPosition, int currentPage) { Log.i(TAG, "onLoadMore invoked!"); - //Simulating asynchronous call + // Simulating asynchronous call new Handler().postDelayed(new Runnable() { @SuppressWarnings("unchecked") @Override public void run() { final List newItems = new ArrayList<>(3); - //Simulating success/failure + // Simulating success/failure int totalItemsOfType = mAdapter.getItemCountOfTypes(R.layout.recycler_instagram_item); for (int i = 1; i <= 3; i++) { newItems.add(DatabaseService.newInstagramItem(totalItemsOfType + i)); } - //Callback the Adapter to notify the change - //Items will be added to the end of the list + // Callback the Adapter to notify the change + // Items will be added to the end of the main list mAdapter.onLoadMoreComplete(newItems); - - //Notify user - String message = "Fetched " + newItems.size() + " new items"; - Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + Log.d(TAG, "newItemsSize=" + newItems.size()); + Log.d(TAG, "EndlessCurrentPage=" + mAdapter.getEndlessCurrentPage()); + Log.d(TAG, "EndlessPageSize=" + mAdapter.getEndlessPageSize()); + Log.d(TAG, "EndlessTargetCount=" + mAdapter.getEndlessTargetCount()); + + // Notify user + if (getActivity() != null && newItems.size() > 0) { + Toast.makeText(getActivity(), + "Fetched " + newItems.size() + " new items", + Toast.LENGTH_SHORT).show(); + } } - }, 2000); + }, 3000); } @Override diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentOverall.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentOverall.java index f67bfc12..7d099aa8 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentOverall.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentOverall.java @@ -16,6 +16,7 @@ import eu.davidea.flexibleadapter.common.SmoothScrollGridLayoutManager; import eu.davidea.samples.flexibleadapter.OverallAdapter; import eu.davidea.samples.flexibleadapter.R; +import eu.davidea.samples.flexibleadapter.items.ScrollableUseCaseItem; import eu.davidea.samples.flexibleadapter.services.DatabaseService; /** @@ -51,43 +52,54 @@ public FragmentOverall() { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - //Create overall items and Initialize RecyclerView + // Create overall items and Initialize RecyclerView DatabaseService.getInstance().createOverallDatabase(getActivity().getResources()); initializeRecyclerView(savedInstanceState); } @SuppressWarnings({"ConstantConditions", "NullableProblems"}) private void initializeRecyclerView(Bundle savedInstanceState) { - //Initialize Adapter and RecyclerView - //OverallAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' + // Initialize Adapter and RecyclerView + // OverallAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' + + // In this example the Adapter make uses of METHOD B and extends FlexibleAdapter: items + // don't implement the AutoMap and don't implement create and binding methods: The Adapter + // remains responsible to handling all view types. mAdapter = new OverallAdapter(getActivity()); - //Experimenting NEW features (v5.0.0) - mAdapter.setAnimationOnScrolling(true) - .setAnimationOnReverseScrolling(true) + + // Experimenting NEW features (v5.0.0) + mAdapter.setOnlyEntryAnimation(true) .setAnimationInterpolator(new DecelerateInterpolator()) .setAnimationInitialDelay(500L) - .setAnimationDelay(150L); + .setAnimationDelay(70L); + + // Prepare the RecyclerView and attach the Adapter to it mRecyclerView = (RecyclerView) getView().findViewById(R.id.recycler_view); - mRecyclerView.setItemViewCacheSize(0);//Setting ViewCache to 0 (default=2) will animate items better while scrolling down+up with LinearLayout + mRecyclerView.setItemViewCacheSize(0); //Setting ViewCache to 0 (default=2) will animate items better while scrolling down+up with LinearLayout mRecyclerView.setLayoutManager(createNewStaggeredGridLayoutManager()); mRecyclerView.setAdapter(mAdapter); - mRecyclerView.setHasFixedSize(true);//Size of RV will not change + mRecyclerView.setHasFixedSize(true); //Size of RV will not change + + // After Adapter is attached to RecyclerView mAdapter.setLongPressDragEnabled(true); mRecyclerView.postDelayed(new Runnable() { @Override public void run() { - if (getView() != null) {//Fix NPE when closing app before the execution of Runnable + if (getView() != null) { //Fix NPE when closing app before the execution of Runnable Snackbar.make(getView(), "Long press drag is enabled", Snackbar.LENGTH_SHORT).show(); } } - }, 1500L); + }, 4000L); SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) getView().findViewById(R.id.swipeRefreshLayout); swipeRefreshLayout.setEnabled(true); mListener.onFragmentChange(swipeRefreshLayout, mRecyclerView, SelectableAdapter.MODE_IDLE); - //Add sample HeaderView items on the top (not belongs to the library) + // Add 2 Scrollable Headers mAdapter.showLayoutInfo(savedInstanceState == null); + mAdapter.addScrollableHeader(new ScrollableUseCaseItem( + getString(R.string.overall_use_case_title), + getString(R.string.overall_use_case_description))); } @Override @@ -109,10 +121,11 @@ protected GridLayoutManager createNewGridLayoutManager() { gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { - //NOTE: If you use simple integer to identify the ViewType, - //here, you should use them and not Layout integers + // NOTE: If you use simple integers to identify the ViewType, + // here, you should use them and not Layout integers switch (mAdapter.getItemViewType(position)) { - case R.layout.recycler_layout_item: + case R.layout.recycler_scrollable_usecase_item: + case R.layout.recycler_scrollable_layout_item: return mColumnCount; default: return 1; diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentSelectionModes.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentSelectionModes.java index 610ed1b3..99c853cf 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentSelectionModes.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentSelectionModes.java @@ -5,20 +5,21 @@ import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.RecyclerView; -import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.widget.AdapterView; + +import java.util.List; import eu.davidea.fastscroller.FastScroller; -import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.SelectableAdapter; import eu.davidea.flexibleadapter.common.DividerItemDecoration; +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; import eu.davidea.flipview.FlipView; import eu.davidea.samples.flexibleadapter.ExampleAdapter; import eu.davidea.samples.flexibleadapter.MainActivity; import eu.davidea.samples.flexibleadapter.R; +import eu.davidea.samples.flexibleadapter.items.ScrollableUseCaseItem; import eu.davidea.samples.flexibleadapter.services.DatabaseService; import eu.davidea.utils.Utils; @@ -27,22 +28,10 @@ * Activities containing this fragment MUST implement the {@link OnFragmentInteractionListener} * interface. */ -public class FragmentSelectionModes extends AbstractFragment - implements FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener { +public class FragmentSelectionModes extends AbstractFragment { public static final String TAG = FragmentSelectionModes.class.getSimpleName(); - /** - * The serialization (saved instance state) Bundle key representing the - * activated item position. Only used on tablets. - */ - private static final String STATE_ACTIVATED_POSITION = "activated_position"; - - /** - * The current activated item position. - */ - private int mActivatedPosition = RecyclerView.NO_POSITION; - /** * Custom implementation of FlexibleAdapter */ @@ -65,47 +54,40 @@ public static FragmentSelectionModes newInstance(int columnCount) { public FragmentSelectionModes() { } - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (savedInstanceState != null) { - //Previously serialized activated item position - if (savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) - setSelection(savedInstanceState.getInt(STATE_ACTIVATED_POSITION)); - } - } - @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - //Settings for FlipView + // Settings for FlipView FlipView.resetLayoutAnimationDelay(true, 1000L); - //Create New Database and Initialize RecyclerView + // Create New Database and Initialize RecyclerView DatabaseService.getInstance().createEndlessDatabase(200); initializeRecyclerView(savedInstanceState); - //Settings for FlipView + // Settings for FlipView FlipView.stopLayoutAnimation(); } @SuppressWarnings({"ConstantConditions", "NullableProblems"}) private void initializeRecyclerView(Bundle savedInstanceState) { - //Initialize Adapter and RecyclerView - //ExampleAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' - mAdapter = new ExampleAdapter(DatabaseService.getInstance().getDatabaseList(), getActivity()); - mAdapter.setMode(SelectableAdapter.MODE_SINGLE); + //Get copy of the Database list + List items = DatabaseService.getInstance().getDatabaseList(); + + // Initialize Adapter and RecyclerView + // ExampleAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' + mAdapter = new ExampleAdapter(items, getActivity()); + mAdapter.setNotifyChangeOfUnfilteredItems(true) //This will rebind new item when refreshed + .setMode(SelectableAdapter.MODE_SINGLE); - //Experimenting NEW features (v5.0.0) + // Experimenting NEW features (v5.0.0) mRecyclerView = (RecyclerView) getView().findViewById(R.id.recycler_view); mRecyclerView.setLayoutManager(createNewLinearLayoutManager()); mRecyclerView.setAdapter(mAdapter); mRecyclerView.setHasFixedSize(true); //Size of RV will not change - //NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if + // NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if // a Payload is provided. FlexibleAdapter is actually sending Payloads onItemChange. mRecyclerView.setItemAnimator(new DefaultItemAnimator()); - //Divider item decorator with DrawOver enabled + // Divider item decorator with DrawOver enabled mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), R.drawable.divider) .withDrawOver(true)); mRecyclerView.postDelayed(new Runnable() { @@ -115,75 +97,26 @@ public void run() { } }, 1500L); - //Add FastScroll to the RecyclerView, after the Adapter has been attached the RecyclerView!!! - mAdapter.setFastScroller((FastScroller) getActivity().findViewById(R.id.fast_scroller), + // Add FastScroll to the RecyclerView, after the Adapter has been attached the RecyclerView!!! + mAdapter.setFastScroller((FastScroller) getView().findViewById(R.id.fast_scroller), Utils.getColorAccent(getActivity()), (MainActivity) getActivity()); SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) getView().findViewById(R.id.swipeRefreshLayout); swipeRefreshLayout.setEnabled(true); mListener.onFragmentChange(swipeRefreshLayout, mRecyclerView, SelectableAdapter.MODE_SINGLE); - //Add sample HeaderView items on the top (not belongs to the library) + // Add 2 Scrollable Headers mAdapter.addUserLearnedSelection(savedInstanceState == null); - mAdapter.showLayoutInfo(savedInstanceState == null); + mAdapter.addScrollableHeaderWithDelay(new ScrollableUseCaseItem( + getString(R.string.selection_modes_use_case_title), + getString(R.string.selection_modes_use_case_description)), 1100L, true + ); } @Override public void showNewLayoutInfo(MenuItem item) { super.showNewLayoutInfo(item); - mAdapter.showLayoutInfo(true); - } - - //TODO: Should include setActivatedPosition in the library? - public void setSelection(final int position) { - if (mAdapter.getMode() == FlexibleAdapter.MODE_SINGLE) { - Log.v(TAG, "setSelection called!"); - setActivatedPosition(position); - mRecyclerView.postDelayed(new Runnable() { - @Override - public void run() { - mRecyclerView.smoothScrollToPosition(position); - } - }, 1000L); - } - } - - private void setActivatedPosition(int position) { - Log.d(TAG, "ItemList New mActivatedPosition=" + position); - mActivatedPosition = position; - } - - /** - * Called when single tap occurs. - * - * @param position the adapter position of the item clicked - * @return true if the click should activate the ItemView, false for no change. - */ - @Override - public boolean onItemClick(int position) { - if (position != mActivatedPosition) - setActivatedPosition(position); - return true; - } - - /** - * Called when long tap occurs. - * - * @param position the adapter position of the item clicked - */ - @Override - public void onItemLongClick(int position) { - //TODO: Handling ActionMode - } - - @Override - public void onSaveInstanceState(Bundle outState) { - if (mActivatedPosition != AdapterView.INVALID_POSITION) { - //Serialize and persist the activated item position. - outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition); - Log.d(TAG, STATE_ACTIVATED_POSITION + "=" + mActivatedPosition); - } - super.onSaveInstanceState(outState); + mAdapter.showLayoutInfo(false); } @Override diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentStaggeredLayout.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentStaggeredLayout.java index 8e56a2cb..526b0468 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentStaggeredLayout.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentStaggeredLayout.java @@ -12,16 +12,18 @@ import java.util.Random; +import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.Payload; import eu.davidea.flexibleadapter.SelectableAdapter; import eu.davidea.flexibleadapter.common.TopSnappedSmoothScroller; +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; import eu.davidea.flexibleadapter.items.IHeader; import eu.davidea.flexibleadapter.utils.Utils; -import eu.davidea.samples.flexibleadapter.ExampleAdapter; import eu.davidea.samples.flexibleadapter.R; -import eu.davidea.samples.flexibleadapter.models.StaggeredHeaderItem; -import eu.davidea.samples.flexibleadapter.models.StaggeredItem; -import eu.davidea.samples.flexibleadapter.models.StaggeredItemStatus; +import eu.davidea.samples.flexibleadapter.items.ScrollableUseCaseItem; +import eu.davidea.samples.flexibleadapter.items.StaggeredHeaderItem; +import eu.davidea.samples.flexibleadapter.items.StaggeredItem; +import eu.davidea.samples.flexibleadapter.items.StaggeredItemStatus; import eu.davidea.samples.flexibleadapter.services.DatabaseService; /** @@ -34,7 +36,7 @@ public class FragmentStaggeredLayout extends AbstractFragment { public static final String TAG = FragmentStaggeredLayout.class.getSimpleName(); - private ExampleAdapter mAdapter; + private FlexibleAdapter mAdapter; public static FragmentStaggeredLayout newInstance(int columnCount) { FragmentStaggeredLayout fragment = new FragmentStaggeredLayout(); @@ -55,48 +57,50 @@ public FragmentStaggeredLayout() { public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - //Create New Database and Initialize RecyclerView + // Create New Database and Initialize RecyclerView DatabaseService.getInstance().createStaggeredDatabase(getActivity()); initializeRecyclerView(savedInstanceState); - //Restore FAB button and icon + // Restore FAB button and icon initializeFab(); } @SuppressWarnings({"ConstantConditions", "NullableProblems"}) private void initializeRecyclerView(Bundle savedInstanceState) { - //Initialize Adapter and RecyclerView - //ExampleAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' - mAdapter = new ExampleAdapter(DatabaseService.getInstance().getDatabaseList(), getActivity()); - mAdapter.setNotifyMoveOfFilteredItems(true); + // Initialize Adapter and RecyclerView + // ExampleAdapter makes use of stableIds, I strongly suggest to implement 'item.hashCode()' + mAdapter = new FlexibleAdapter<>(DatabaseService.getInstance().getDatabaseList(), getActivity()); mRecyclerView = (RecyclerView) getView().findViewById(R.id.recycler_view); - - //Customize the speed of the smooth scroll. - //NOTE: Every time you change this value you MUST recreate the LayoutManager instance + // Customize the speed of the smooth scroll. + // NOTE: Every time you change this value you MUST recreate the LayoutManager instance // and to assign it again to the RecyclerView! TopSnappedSmoothScroller.MILLISECONDS_PER_INCH = 33f; mRecyclerView.setLayoutManager(createNewStaggeredGridLayoutManager()); - //This value is restored to 100f (default) right here, because it is used in the constructor - // by Android. If we don't change it now, others LayoutManager will be impacted too by the - // above modification! + // This value is restored to 100f (default) right here, because it is used in the + // constructor by Android. If we don't change it now, others LayoutManager will be + // impacted too by the above modification! TopSnappedSmoothScroller.MILLISECONDS_PER_INCH = 100f; mRecyclerView.setAdapter(mAdapter); mRecyclerView.setHasFixedSize(true); //Size of RV will not change - //NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if + // NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if // a Payload is provided. FlexibleAdapter is actually sending Payloads onItemChange. mRecyclerView.setItemAnimator(new DefaultItemAnimator()); - //Experimenting NEW features (v5.0.0) - mAdapter.setDisplayHeadersAtStartUp(true)//Show Headers at startUp! - .setPermanentDelete(true); + // Experimenting NEW features (v5.0.0) + mAdapter.setDisplayHeadersAtStartUp(true) //Show Headers at startUp! + .setNotifyMoveOfFilteredItems(true) + .setPermanentDelete(true) //Default=true + .setOnlyEntryAnimation(true); SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) getView().findViewById(R.id.swipeRefreshLayout); swipeRefreshLayout.setEnabled(true); mListener.onFragmentChange(swipeRefreshLayout, mRecyclerView, SelectableAdapter.MODE_IDLE); - //Add sample HeaderView items on the top (not belongs to the library) - mAdapter.showLayoutInfo(savedInstanceState == null); + // Add 1 Scrollable Header + mAdapter.addScrollableHeader(new ScrollableUseCaseItem( + getString(R.string.staggered_use_case_title), + getString(R.string.staggered_use_case_description))); } @Override @@ -104,12 +108,6 @@ public int getContextMenuResId() { return R.menu.menu_staggered_context; } - @Override - public void showNewLayoutInfo(MenuItem item) { - super.showNewLayoutInfo(item); - mAdapter.showLayoutInfo(true); - } - @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); @@ -127,9 +125,9 @@ public boolean onOptionsItemSelected(MenuItem item) { } else if (id == R.id.action_delete) { DatabaseService.getInstance().removeAll(); mAdapter.updateDataSet(null, true); - //This is necessary if we call updateDataSet() and not removeItems + // This is necessary if we call updateDataSet() and not removeItems DatabaseService.getInstance().resetHeaders(); - //Change fab action (ADD NEW ITEM UNTIL 15) + // Change fab action (ADD NEW ITEM UNTIL 15) FloatingActionButton fab = (FloatingActionButton) getActivity().findViewById(R.id.fab); fab.setImageResource(R.drawable.fab_add); } @@ -143,34 +141,34 @@ public void performFabAction() { StaggeredHeaderItem headerItem = DatabaseService.getInstance().getHeaderByStatus(status); int scrollTo; - //CALCULATE POSITION FOR - //- Useful in ALL situations of moving/adding an item. - //- Position is calculated based on the custom Comparator implementation. - //- Comparator object should sort the Section (in the eventuality the header is hidden) - // and the Item into the Section (see the Class ItemComparatorByGroup for an - // example of implementation). - //- It respects the custom sort, also for a non-displayed section! - //- When moving/adding, the relative header item will be automatically displayed too - // if not yet visible. + // CALCULATE POSITION FOR + // - Useful in ALL situations of moving/adding an item. + // - Position is calculated based on the custom Comparator implementation. + // - Comparator object should sort the Section (in the eventuality the header is hidden) + // and the Item into the Section (see the Class ItemComparatorByGroup for an + // example of implementation). + // - It respects the custom sort, also for a non-displayed section! + // - When moving/adding, the relative header item will be automatically displayed too + // if not yet visible. if (mAdapter.getItemCountOfTypes(R.layout.recycler_staggered_item) >= 15) { //FAB Action: Move Item scrollTo = moveItem(status, headerItem); } - //ADD ITEM TO SECTION - //- Useful only to add new items of every type - //- Comparator object should sort the Section (in the eventuality the header is hidden) - // and the Item into the Section (see the Class ItemComparatorByGroup for an - // example of implementation). - //- The relative header will be automatically displayed too if not yet visible. - //- if you already know the relative index of the new item, then call the correct - // method without the Comparator object. + // ADD ITEM TO SECTION + // - Useful only to add new items of every type + // - Comparator object should sort the Section (in the eventuality the header is hidden) + // and the Item into the Section (see the Class ItemComparatorByGroup for an + // example of implementation). + // - The relative header will be automatically displayed too if not yet visible. + // - if you already know the relative index of the new item, then call the correct + // method without the Comparator object. else { - //FAB Action: Add Item + // FAB Action: Add Item scrollTo = addItem(status, headerItem); } - //Show to the user the result of the addition/changes + // Show to the user the result of the addition/changes smoothScrollTo(scrollTo, headerItem); refreshItem(scrollTo); clearEmptySections(); @@ -181,19 +179,19 @@ private int addItem(StaggeredItemStatus status, StaggeredHeaderItem headerItem) DatabaseService.getInstance().getMaxStaggeredId(), headerItem); staggeredItem.setStatus(status);//!!! - //The section object is known + // The section object is known mAdapter.addItemToSection(staggeredItem, staggeredItem.getHeader(), new DatabaseService.ItemComparatorByGroup()); - //Add Item to the Database as well for next refresh + // Add Item to the Database as well for next refresh DatabaseService.getInstance().addItem(staggeredItem, new DatabaseService.ItemComparatorById()); - //Change fab action (MOVE ITEM) + // Change fab action (MOVE ITEM) if (mAdapter.getItemCountOfTypes(R.layout.recycler_staggered_item) >= 15) { FloatingActionButton fab = (FloatingActionButton) getActivity().findViewById(R.id.fab); fab.setImageResource(R.drawable.ic_sort_white_24dp); } - //Retrieve the final position due to a possible hidden header became now visible! + // Retrieve the final position due to a possible hidden header became now visible! int scrollTo = mAdapter.getGlobalPositionOf(staggeredItem); Log.d(TAG, "Creating New Item " + staggeredItem + " at position " + scrollTo); return scrollTo; @@ -202,22 +200,22 @@ private int addItem(StaggeredItemStatus status, StaggeredHeaderItem headerItem) private int moveItem(StaggeredItemStatus status, StaggeredHeaderItem headerItem) { StaggeredItem staggeredItem = DatabaseService.getInstance().getRandomStaggeredItem(); if (!staggeredItem.getHeader().equals(headerItem)) { - //Before calculate the position, change header/section + // Before calculate the position, change header/section staggeredItem.setStatus(status);//!!! staggeredItem.setHeader(headerItem); int toPosition = mAdapter.calculatePositionFor(staggeredItem, new DatabaseService.ItemComparatorByGroup()); - //Move item to just calculated position under the correct section + // Move item to just calculated position under the correct section mAdapter.moveItem(mAdapter.getGlobalPositionOf(staggeredItem), toPosition, Payload.MOVE); } - //Retrieve the final position due to a possible hidden header became now visible! + // Retrieve the final position due to a possible hidden header became now visible! int scrollTo = mAdapter.getGlobalPositionOf(staggeredItem); Log.d(TAG, "Moving Item to position" + scrollTo); return scrollTo; } private void smoothScrollTo(final int scrollTo, final StaggeredHeaderItem headerItem) { - //Smooth scrolling should be delayed because the just added item could not be yet + // Smooth scrolling should be delayed because the just added item could not be yet // animated/rendered by the LayoutManager mRecyclerView.postDelayed(new Runnable() { @Override @@ -237,7 +235,7 @@ public void run() { } private void refreshItem(final int position) { - //We notify the item that it is changed, bind it again (change color) + // We notify the item that it is changed, bind it again (change color) mRecyclerView.postDelayed(new Runnable() { @Override public void run() { @@ -247,7 +245,7 @@ public void run() { } private void clearEmptySections() { - //We remove the header item that is without items (empty sections) + // We remove the header item that is without items (empty sections) for (final IHeader header : mAdapter.getHeaderItems()) { Log.d(TAG, "Header=" + header.toString() + " Items=" + mAdapter.getSectionItems(header).size()); if (mAdapter.getSectionItems(header).size() == 0) { diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentViewPager.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentViewPager.java new file mode 100644 index 00000000..b81f47c0 --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/fragments/FragmentViewPager.java @@ -0,0 +1,161 @@ +package eu.davidea.samples.flexibleadapter.fragments; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.widget.DefaultItemAnimator; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +import eu.davidea.fastscroller.FastScroller; +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager; +import eu.davidea.flexibleadapter.items.IFlexible; +import eu.davidea.flipview.FlipView; +import eu.davidea.samples.flexibleadapter.R; +import eu.davidea.samples.flexibleadapter.items.HeaderItem; +import eu.davidea.samples.flexibleadapter.services.DatabaseConfiguration; +import eu.davidea.samples.flexibleadapter.services.DatabaseService; +import eu.davidea.utils.Utils; + +/** + * A placeholder fragment containing a simple view. + */ +public class FragmentViewPager extends Fragment { + /** + * The fragment argument representing the section number for this + * fragment. + */ + private static final String ARG_SECTION_NUMBER = "section_number"; + private static final String TAG = FragmentViewPager.class.getSimpleName(); + + private int mSection; + private FlexibleAdapter mAdapter; + + public FragmentViewPager() { + } + + /** + * Returns a new instance of this fragment for the given section + * number. + */ + public static FragmentViewPager newInstance(int sectionNumber) { + FragmentViewPager fragment = new FragmentViewPager(); + Bundle args = new Bundle(); + args.putInt(ARG_SECTION_NUMBER, sectionNumber); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getArguments() != null) { + mSection = getArguments().getInt(ARG_SECTION_NUMBER); + Log.d(TAG, "Creating new Fragment for Section " + mSection); + } + + // Contribution for specific action buttons in the Toolbar + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_view_pager, container, false); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Settings for FlipView + FlipView.resetLayoutAnimationDelay(true, 1000L); + + // Initialize RecyclerView + initializeRecyclerView(); + + // Settings for FlipView + FlipView.stopLayoutAnimation(); + } + + private void initializeRecyclerView() { + // Initialize Adapter and RecyclerView + // Use of stableIds, I strongly suggest to implement 'item.hashCode()' + mAdapter = new FlexibleAdapter<>(createList(50, 5), getActivity(), true); + // Experimenting NEW features (v5.0.0) + mAdapter.setAnimationOnScrolling(DatabaseConfiguration.animateOnScrolling); + + RecyclerView mRecyclerView = (RecyclerView) getView().findViewById(R.id.recycler_view); + mRecyclerView.setLayoutManager(new SmoothScrollLinearLayoutManager(getActivity())); + mRecyclerView.setAdapter(mAdapter); + mRecyclerView.setHasFixedSize(true); //Size of RV will not change + // NOTE: Use default item animator 'canReuseUpdatedViewHolder()' will return true if + // a Payload is provided. FlexibleAdapter is actually sending Payloads onItemChange. + mRecyclerView.setItemAnimator(new DefaultItemAnimator()); + + // Add FastScroll to the RecyclerView, after the Adapter has been attached the RecyclerView!!! + FastScroller fastScroller = (FastScroller) getView().findViewById(R.id.fast_scroller); + mAdapter.setFastScroller(fastScroller, Utils.getColorAccent(getActivity())); + mAdapter.toggleFastScroller(); + + // Sticky Headers + mAdapter.setDisplayHeadersAtStartUp(true) + .setStickyHeaders(true); + } + + private List createList(int size, int headers) { + HeaderItem header = null; + List items = new ArrayList<>(); + int lastHeaderId = 0; + for (int i = 0; i < size; i++) { + header = i % Math.round(size / headers) == 0 ? + DatabaseService.newHeader(++lastHeaderId) : header; + items.add(DatabaseService.newSimpleItem(i + 1, header)); + } + return items; + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + // Headers are shown? + MenuItem headersMenuItem = menu.findItem(R.id.action_show_hide_headers); + if (headersMenuItem != null) { + headersMenuItem.setTitle(mAdapter.areHeadersShown() ? R.string.hide_headers : R.string.show_headers); + } + // Sticky Header item? + MenuItem stickyItem = menu.findItem(R.id.action_sticky_headers); + if (stickyItem != null) { + stickyItem.setEnabled(mAdapter.areHeadersShown()); + stickyItem.setChecked(mAdapter.areHeadersSticky()); + } + super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == R.id.action_sticky_headers) { + item.setChecked(!mAdapter.areHeadersSticky()); + mAdapter.setStickyHeaders(!mAdapter.areHeadersSticky()); + } else if (id == R.id.action_show_hide_headers) { + if (mAdapter.areHeadersShown()) { + mAdapter.hideAllHeaders(); + item.setTitle(R.string.show_headers); + } else { + mAdapter.showAllHeaders(); + item.setTitle(R.string.hide_headers); + } + } + return super.onOptionsItemSelected(item); + } + +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/holders/HeaderHolder.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/holders/HeaderHolder.java new file mode 100644 index 00000000..61eee3ab --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/holders/HeaderHolder.java @@ -0,0 +1,100 @@ +package eu.davidea.samples.flexibleadapter.holders; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.AbstractHeaderItem; +import eu.davidea.flexibleadapter.items.IFilterable; +import eu.davidea.flexibleadapter.items.IHolder; +import eu.davidea.samples.flexibleadapter.R; +import eu.davidea.samples.flexibleadapter.models.HeaderModel; +import eu.davidea.viewholders.FlexibleViewHolder; + +/** + * The holder item is just a wrapper for the Model item. + * + * @author Davide Steduto + * @since 19/10/2016 + */ +public class HeaderHolder extends AbstractHeaderItem + implements IFilterable, IHolder { + + private HeaderModel model; + + public HeaderHolder(HeaderModel model) { + this.model = model; + } + + @Override + public boolean equals(Object o) { + if (o instanceof HeaderHolder) { + HeaderHolder inItem = (HeaderHolder) o; + return model.equals(inItem.getModel()); + } + return false; + } + + @Override + public int hashCode() { + return model.hashCode(); + } + + /** + * @return the model object + */ + @Override + public HeaderModel getModel() { + return model; + } + + /** + * Filter is applied to the model fields. + */ + @Override + public boolean filter(String constraint) { + return model.getTitle() != null && model.getTitle().equals(constraint); + } + + @Override + public int getLayoutRes() { + return R.layout.recycler_holder_header; + } + + @Override + public HeaderViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { + return new HeaderViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); + } + + @Override + public void bindViewHolder(final FlexibleAdapter adapter, HeaderViewHolder holder, int position, List payloads) { + holder.mTitle.setText(model.getTitle()); + List sectionableList = adapter.getSectionItems(this); + String subTitle = (sectionableList.isEmpty() ? "Empty section" : + sectionableList.size() + " section items"); + holder.mSubtitle.setText(subTitle); + } + + static class HeaderViewHolder extends FlexibleViewHolder { + + @BindView(R.id.title) + public TextView mTitle; + @BindView(R.id.subtitle) + public TextView mSubtitle; + + /** + * Default constructor. + */ + public HeaderViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter, true);//true only for header items when will be sticky + ButterKnife.bind(this, view); + } + } + +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/holders/ItemHolder.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/holders/ItemHolder.java new file mode 100644 index 00000000..8af3ff68 --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/holders/ItemHolder.java @@ -0,0 +1,109 @@ +package eu.davidea.samples.flexibleadapter.holders; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.AbstractSectionableItem; +import eu.davidea.flexibleadapter.items.IFilterable; +import eu.davidea.flexibleadapter.items.IHolder; +import eu.davidea.samples.flexibleadapter.R; +import eu.davidea.samples.flexibleadapter.models.ItemModel; +import eu.davidea.viewholders.FlexibleViewHolder; + +/** + * The holder item is just a wrapper for the Model item. + * + *

Holder item can be used to display the same modelData in multiple RecyclerViews managed by + * different Adapters, you can implement a derived IFlexible item to HOLD your data model object!

+ * + * In this way you can separate the memory zones of the flags (enabled, expanded, hidden, + * selectable) used by a specific Adapter, to be independent by another Adapter. For instance, + * an item can be Shown and Expanded in a RV, while in the other RV can be Hidden or Not Expanded! + * + * @author Davide Steduto + * @since 19/10/2016 + */ +public class ItemHolder extends AbstractSectionableItem + implements IFilterable, IHolder { + + private ItemModel model; + + /** + * The header item must in its bounds, it must implement IHeader, therefore: HeaderHolder! + */ + public ItemHolder(ItemModel model, HeaderHolder header) { + super(header); + this.model = model; + } + + @Override + public boolean equals(Object o) { + if (o instanceof ItemHolder) { + ItemHolder inItem = (ItemHolder) o; + return model.equals(inItem.getModel()); + } + return false; + } + + @Override + public int hashCode() { + return model.hashCode(); + } + + /** + * @return the model object + */ + @Override + public ItemModel getModel() { + return model; + } + + /** + * Filter is applied to the model fields. + */ + @Override + public boolean filter(String constraint) { + return model.getTitle() != null && model.getTitle().toLowerCase().trim().contains(constraint) || + model.getSubtitle() != null && model.getSubtitle().toLowerCase().trim().contains(constraint); + } + + @Override + public int getLayoutRes() { + return R.layout.recycler_holder_item; + } + + @Override + public ItemViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { + return new ItemViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); + } + + @Override + public void bindViewHolder(final FlexibleAdapter adapter, ItemViewHolder holder, int position, List payloads) { + holder.mTitle.setText(model.getTitle()); + holder.mSubtitle.setText(model.getSubtitle()); + } + + static class ItemViewHolder extends FlexibleViewHolder { + + @BindView(R.id.title) + public TextView mTitle; + @BindView(R.id.subtitle) + public TextView mSubtitle; + + /** + * Default constructor. + */ + public ItemViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter); + ButterKnife.bind(this, view); + } + } + +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/AbstractModelItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/AbstractItem.java similarity index 65% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/AbstractModelItem.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/AbstractItem.java index 2478ad29..0d65a1f0 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/AbstractModelItem.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/AbstractItem.java @@ -1,6 +1,4 @@ -package eu.davidea.samples.flexibleadapter.models; - -import java.io.Serializable; +package eu.davidea.samples.flexibleadapter.items; import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; import eu.davidea.viewholders.FlexibleViewHolder; @@ -9,26 +7,25 @@ * This class will benefit of the already implemented methods (getter and setters) in * {@link eu.davidea.flexibleadapter.items.AbstractFlexibleItem}. * - * It is used as Base item for all example models. + * It is used as base item for all example models. */ -public abstract class AbstractModelItem - extends AbstractFlexibleItem - implements Serializable { - - private static final long serialVersionUID = -6882745111884490060L; +public abstract class AbstractItem + extends AbstractFlexibleItem { - private String id; - private String title; - private String subtitle; + protected String id; + protected String title; + protected String subtitle = ""; + /* number of times this item has been refreshed */ + protected int updates; - public AbstractModelItem(String id) { + public AbstractItem(String id) { this.id = id; } @Override public boolean equals(Object inObject) { - if (inObject instanceof AbstractModelItem) { - AbstractModelItem inItem = (AbstractModelItem) inObject; + if (inObject instanceof AbstractItem) { + AbstractItem inItem = (AbstractItem) inObject; return this.id.equals(inItem.id); } return false; @@ -67,6 +64,14 @@ public void setSubtitle(String subtitle) { this.subtitle = subtitle; } + public int getUpdates() { + return updates; + } + + public void increaseUpdates() { + this.updates++; + } + @Override public String toString() { return "id=" + id + diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/AnimatorExpandableItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/AnimatorExpandableItem.java similarity index 96% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/AnimatorExpandableItem.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/AnimatorExpandableItem.java index 7a33265e..5c0ebe80 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/AnimatorExpandableItem.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/AnimatorExpandableItem.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.animation.Animator; import android.support.annotation.NonNull; @@ -15,7 +15,7 @@ import eu.davidea.flexibleadapter.helpers.AnimatorHelper; import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem; import eu.davidea.samples.flexibleadapter.R; -import eu.davidea.samples.flexibleadapter.models.AnimatorExpandableItem.AnimatorExpandableViewHolder; +import eu.davidea.samples.flexibleadapter.items.AnimatorExpandableItem.AnimatorExpandableViewHolder; import eu.davidea.samples.flexibleadapter.services.DatabaseConfiguration; import eu.davidea.viewholders.ExpandableViewHolder; @@ -89,7 +89,7 @@ public AnimatorExpandableViewHolder createViewHolder(FlexibleAdapter adapter, La @Override public void bindViewHolder(FlexibleAdapter adapter, AnimatorExpandableViewHolder holder, int position, List payloads) { if (payloads.size() > 0) { - Log.i(this.getClass().getSimpleName(), "AnimatorExpandableItem Payload " + payloads); + Log.d(this.getClass().getSimpleName(), "AnimatorExpandableItem Payload " + payloads); } else { holder.mTitle.setText(getTitle()); } diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/AnimatorSubItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/AnimatorSubItem.java similarity index 97% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/AnimatorSubItem.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/AnimatorSubItem.java index 24983ac5..4024ebd7 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/AnimatorSubItem.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/AnimatorSubItem.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.animation.Animator; import android.support.annotation.NonNull; @@ -92,7 +92,7 @@ static final class ChildViewHolder extends FlexibleViewHolder implements Animate public TextView mTitle; - public ChildViewHolder(View view, FlexibleAdapter adapter) { + ChildViewHolder(View view, FlexibleAdapter adapter) { super(view, adapter); this.mTitle = (TextView) view.findViewById(R.id.title); } diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ConfigurationItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ConfigurationItem.java similarity index 99% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ConfigurationItem.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ConfigurationItem.java index 0b3b5383..2fbc9fda 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ConfigurationItem.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ConfigurationItem.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.animation.Animator; import android.support.annotation.IntDef; diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ExpandableHeaderItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ExpandableHeaderItem.java similarity index 56% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ExpandableHeaderItem.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ExpandableHeaderItem.java index 434b424e..1683e4ba 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ExpandableHeaderItem.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ExpandableHeaderItem.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.support.v7.widget.StaggeredGridLayoutManager; import android.util.Log; @@ -12,10 +12,11 @@ import java.util.List; import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.Payload; import eu.davidea.flexibleadapter.items.IExpandable; import eu.davidea.flexibleadapter.items.IHeader; import eu.davidea.samples.flexibleadapter.R; -import eu.davidea.samples.flexibleadapter.models.ExpandableHeaderItem.ExpandableHeaderViewHolder; +import eu.davidea.samples.flexibleadapter.items.ExpandableHeaderItem.ExpandableHeaderViewHolder; import eu.davidea.viewholders.ExpandableViewHolder; /** @@ -24,12 +25,10 @@ * It's important to note that, the ViewHolder must be specified in all <diamond> signature. */ public class ExpandableHeaderItem - extends AbstractModelItem + extends AbstractItem implements IExpandable, IHeader { - private static final long serialVersionUID = -1882711111814491060L; - /* Flags for FlexibleAdapter */ private boolean mExpanded = false; @@ -109,7 +108,7 @@ public ExpandableHeaderViewHolder createViewHolder(FlexibleAdapter adapter, Layo @Override public void bindViewHolder(FlexibleAdapter adapter, ExpandableHeaderViewHolder holder, int position, List payloads) { if (payloads.size() > 0) { - Log.i(this.getClass().getSimpleName(), "ExpandableHeaderItem Payload " + payloads); + Log.d(this.getClass().getSimpleName(), "ExpandableHeaderItem Payload " + payloads); } else { holder.mTitle.setText(getTitle()); } @@ -125,11 +124,11 @@ public void bindViewHolder(FlexibleAdapter adapter, ExpandableHeaderViewHolder h */ static class ExpandableHeaderViewHolder extends ExpandableViewHolder { - public TextView mTitle; - public TextView mSubtitle; - public ImageView mHandleView; + TextView mTitle; + TextView mSubtitle; + ImageView mHandleView; - public ExpandableHeaderViewHolder(View view, FlexibleAdapter adapter) { + ExpandableHeaderViewHolder(View view, FlexibleAdapter adapter) { super(view, adapter, true);//True for sticky mTitle = (TextView) view.findViewById(R.id.title); mSubtitle = (TextView) view.findViewById(R.id.subtitle); @@ -147,23 +146,93 @@ public ExpandableHeaderViewHolder(View view, FlexibleAdapter adapter) { } } + /** + * Allows to expand or collapse child views of this itemView when {@link View.OnClickListener} + * event occurs on the entire view. + *

This method returns always true; Extend with "return false" to Not expand or collapse + * this ItemView onClick events.

+ * + * @return always true, if not overridden + * @since 5.0.0-b1 + */ @Override protected boolean isViewExpandableOnClick() { - return true;//true by default + return true;//default=true + } + + /** + * Allows to collapse child views of this ItemView when {@link View.OnLongClickListener} + * event occurs on the entire view. + *

This method returns always true; Extend with "return false" to Not collapse this + * ItemView onLongClick events.

+ * + * @return always true, if not overridden + * @since 5.0.0-b1 + */ + protected boolean isViewCollapsibleOnLongClick() { + return true;//default=true + } + + /** + * Allows to notify change and rebound this itemView on expanding and collapsing events, + * in order to update the content (so, user can decide to display the current expanding status). + *

This method returns always false; Override with {@code "return true"} to trigger the + * notification.

+ * + * @return true to rebound the content of this itemView on expanding and collapsing events, + * false to ignore the events + * @see #expandView(int) + * @see #collapseView(int) + * @since 5.0.0-rc1 + */ + @Override + protected boolean shouldNotifyParentOnClick() { + return true;//default=false + } + + /** + * Expands or Collapses based on the current state. + * + * @see #shouldNotifyParentOnClick() + * @see #expandView(int) + * @see #collapseView(int) + * @since 5.0.0-b1 + */ + @Override + protected void toggleExpansion() { + super.toggleExpansion(); //If overridden, you must call the super method } + /** + * Triggers expansion of this itemView. + *

If {@link #shouldNotifyParentOnClick()} returns {@code true}, this view is rebound + * with payload {@link Payload#EXPANDED}.

+ * + * @see #shouldNotifyParentOnClick() + * @since 5.0.0-b1 + */ @Override protected void expandView(int position) { - super.expandView(position); - //Let's notify the item has been expanded - if (mAdapter.isExpanded(position)) mAdapter.notifyItemChanged(position, true); + super.expandView(position); //If overridden, you must call the super method + // Let's notify the item has been expanded. Note: from 5.0.0-rc1 the next line becomes + // obsolete, override the new method shouldNotifyParentOnClick() as showcased here + //if (mAdapter.isExpanded(position)) mAdapter.notifyItemChanged(position, true); } + /** + * Triggers collapse of this itemView. + *

If {@link #shouldNotifyParentOnClick()} returns {@code true}, this view is rebound + * with payload {@link Payload#COLLAPSED}.

+ * + * @see #shouldNotifyParentOnClick() + * @since 5.0.0-b1 + */ @Override protected void collapseView(int position) { - super.collapseView(position); - //Let's notify the item has been collapsed - if (!mAdapter.isExpanded(position)) mAdapter.notifyItemChanged(position, true); + super.collapseView(position); //If overridden, you must call the super method + // Let's notify the item has been collapsed. Note: from 5.0.0-rc1 the next line becomes + // obsolete, override the new method shouldNotifyParentOnClick() as showcased here + //if (!mAdapter.isExpanded(position)) mAdapter.notifyItemChanged(position, true); } } diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ExpandableItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ExpandableItem.java new file mode 100644 index 00000000..e45ec988 --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ExpandableItem.java @@ -0,0 +1,299 @@ +package eu.davidea.samples.flexibleadapter.items; + +import android.animation.Animator; +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.helpers.AnimatorHelper; +import eu.davidea.flexibleadapter.items.IExpandable; +import eu.davidea.flexibleadapter.items.IFilterable; +import eu.davidea.flexibleadapter.items.ISectionable; +import eu.davidea.flexibleadapter.utils.DrawableUtils; +import eu.davidea.flexibleadapter.utils.Utils; +import eu.davidea.flipview.FlipView; +import eu.davidea.samples.flexibleadapter.R; +import eu.davidea.viewholders.ExpandableViewHolder; + +/** + * If you don't have fields in common better to extend directly from + * {@link eu.davidea.flexibleadapter.items.AbstractExpandableItem} to benefit of the already + * implemented methods around subItems list. + */ +public class ExpandableItem extends AbstractItem + implements ISectionable, IFilterable, + IExpandable { + + /* The header of this item */ + HeaderItem header; + + /* subItems list */ + private List mSubItems; + + /* Flags for FlexibleAdapter */ + private boolean mExpanded = false; + + public ExpandableItem(String id, HeaderItem header) { + super(id); + this.header = header; + setDraggable(true); + setSwipeable(true); + } + + @Override + public HeaderItem getHeader() { + return header; + } + + @Override + public void setHeader(HeaderItem header) { + this.header = header; + } + + @Override + public boolean isExpanded() { + return mExpanded; + } + + @Override + public void setExpanded(boolean expanded) { + mExpanded = expanded; + } + + @Override + public int getExpansionLevel() { + return 0; + } + + @Override + public List getSubItems() { + return mSubItems; + } + + public final boolean hasSubItems() { + return mSubItems!= null && mSubItems.size() > 0; + } + + public boolean removeSubItem(SubItem item) { + return item != null && mSubItems.remove(item); + } + + public boolean removeSubItem(int position) { + if (mSubItems != null && position >= 0 && position < mSubItems.size()) { + mSubItems.remove(position); + return true; + } + return false; + } + + public void addSubItem(SubItem subItem) { + if (mSubItems == null) + mSubItems = new ArrayList<>(); + mSubItems.add(subItem); + } + + public void addSubItem(int position, SubItem subItem) { + if (mSubItems != null && position >= 0 && position < mSubItems.size()) { + mSubItems.add(position, subItem); + } else + addSubItem(subItem); + } + + @Override + public int getLayoutRes() { + return R.layout.recycler_expandable_item; + } + + @Override + public ParentViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { + return new ParentViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); + } + + @Override + @SuppressWarnings({"unchecked"}) + public void bindViewHolder(final FlexibleAdapter adapter, ParentViewHolder holder, int position, List payloads) { + Context context = holder.itemView.getContext(); + + // Subtitle + setSubtitle(adapter.getCurrentChildren(this).size() + " subItems" + + (getHeader() != null ? " - " + getHeader().getId() : "") + + (getUpdates() > 0 ? " - u" + getUpdates() : "")); + + // Background, when bound the first time + if (payloads.size() == 0) { + Drawable drawable = DrawableUtils.getSelectableBackgroundCompat( + Color.WHITE, Color.parseColor("#dddddd"), // Same color of divider + DrawableUtils.getColorControlHighlight(context)); + DrawableUtils.setBackgroundCompat(holder.itemView, drawable); + DrawableUtils.setBackgroundCompat(holder.frontView, drawable); + } + + if (payloads.size() > 0) { + Log.d(this.getClass().getSimpleName(), "ExpandableItem Payload " + payloads); + if (adapter.hasSearchText()) { + Utils.highlightText(holder.mSubtitle, getSubtitle(), adapter.getSearchText()); + } else { + holder.mSubtitle.setText(getSubtitle()); + } + // We stop the process here, we only want to update the subtitle + + } else { + // DemoApp: INNER ANIMATION EXAMPLE! ImageView - Handle Flip Animation on + // Select ALL and Deselect ALL + if (adapter.isSelectAll() || adapter.isLastItemInActionMode()) { + // Consume the Animation + holder.mFlipView.flip(adapter.isSelected(position), 200L); + } else { + // Display the current flip status + holder.mFlipView.flipSilently(adapter.isSelected(position)); + } + + // In case of searchText matches with Title or with a field this will be highlighted + if (adapter.hasSearchText()) { + Utils.highlightText(holder.mTitle, getTitle(), adapter.getSearchText()); + Utils.highlightText(holder.mSubtitle, getSubtitle(), adapter.getSearchText()); + } else { + holder.mTitle.setText(getTitle()); + holder.mSubtitle.setText(getSubtitle()); + } + } + } + + @Override + public boolean filter(String constraint) { + return getTitle() != null && getTitle().toLowerCase().trim().contains(constraint) || + getSubtitle() != null && getSubtitle().toLowerCase().trim().contains(constraint); + } + + /** + * This ViewHolder is expandable and collapsible. + */ + static final class ParentViewHolder extends ExpandableViewHolder { + + FlipView mFlipView; + TextView mTitle; + TextView mSubtitle; + ImageView mHandleView; + Context mContext; + View frontView; + View rearLeftView; + View rearRightView; + + public boolean swiped = false; + + ParentViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter); + this.mContext = view.getContext(); + this.mTitle = (TextView) view.findViewById(R.id.title); + this.mSubtitle = (TextView) view.findViewById(R.id.subtitle); + this.mFlipView = (FlipView) view.findViewById(R.id.image); + this.mFlipView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mAdapter.mItemLongClickListener != null) { + mAdapter.mItemLongClickListener.onItemLongClick(getAdapterPosition()); + Toast.makeText(mContext, "ImageClick on " + mTitle.getText() + " position " + getAdapterPosition(), Toast.LENGTH_SHORT).show(); + toggleActivation(); + } + } + }); + this.mHandleView = (ImageView) view.findViewById(R.id.row_handle); + setDragHandleView(mHandleView); + + this.frontView = view.findViewById(R.id.front_view); + this.rearLeftView = view.findViewById(R.id.rear_left_view); + this.rearRightView = view.findViewById(R.id.rear_right_view); + } + + @Override + protected void setDragHandleView(@NonNull View view) { + if (mAdapter.isHandleDragEnabled()) { + view.setVisibility(View.VISIBLE); + super.setDragHandleView(view); + } else { + view.setVisibility(View.GONE); + } + } + + @Override + public void toggleActivation() { + super.toggleActivation(); + // Here we use a custom Animation inside the ItemView + mFlipView.flip(mAdapter.isSelected(getAdapterPosition())); + } + + @Override + public float getActivationElevation() { + return eu.davidea.utils.Utils.dpToPx(itemView.getContext(), 4f); + } + + @Override + protected boolean shouldActivateViewWhileSwiping() { + return false;//default=false + } + + @Override + protected boolean shouldAddSelectionInActionMode() { + return false;//default=false + } + + @Override + public View getFrontView() { + return frontView; + } + + @Override + public View getRearLeftView() { + return rearLeftView; + } + + @Override + public View getRearRightView() { + return rearRightView; + } + + @Override + public void scrollAnimators(@NonNull List animators, int position, boolean isForward) { + if (mAdapter.getRecyclerView().getLayoutManager() instanceof GridLayoutManager || + mAdapter.getRecyclerView().getLayoutManager() instanceof StaggeredGridLayoutManager) { + if (position % 2 != 0) + AnimatorHelper.slideInFromRightAnimator(animators, itemView, mAdapter.getRecyclerView(), 0.5f); + else + AnimatorHelper.slideInFromLeftAnimator(animators, itemView, mAdapter.getRecyclerView(), 0.5f); + } else { + // Linear layout + if (mAdapter.isSelected(position)) + AnimatorHelper.slideInFromRightAnimator(animators, itemView, mAdapter.getRecyclerView(), 0.5f); + else + AnimatorHelper.slideInFromLeftAnimator(animators, itemView, mAdapter.getRecyclerView(), 0.5f); + } + } + + @Override + public void onItemReleased(int position) { + swiped = (mActionState == ItemTouchHelper.ACTION_STATE_SWIPE); + super.onItemReleased(position); + } + } + + @Override + public String toString() { + return "ExpandableItem[" + super.toString() + "//SubItems" + mSubItems + "]"; + } + +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ExpandableLevel0Item.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ExpandableLevel0Item.java similarity index 93% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ExpandableLevel0Item.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ExpandableLevel0Item.java index 712a28c0..41e6612b 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ExpandableLevel0Item.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ExpandableLevel0Item.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.support.v7.widget.StaggeredGridLayoutManager; import android.util.Log; @@ -15,7 +15,7 @@ import eu.davidea.flexibleadapter.items.IExpandable; import eu.davidea.flexibleadapter.items.IHeader; import eu.davidea.samples.flexibleadapter.R; -import eu.davidea.samples.flexibleadapter.models.ExpandableLevel0Item.L0ViewHolder; +import eu.davidea.samples.flexibleadapter.items.ExpandableLevel0Item.L0ViewHolder; import eu.davidea.samples.flexibleadapter.services.DatabaseService; import eu.davidea.samples.flexibleadapter.services.DatabaseType; import eu.davidea.viewholders.ExpandableViewHolder; @@ -26,11 +26,9 @@ * It's important to note that, the ViewHolder must be specified in all <diamond> signature. */ public class ExpandableLevel0Item - extends AbstractModelItem + extends AbstractItem implements IExpandable, IHeader { - private static final long serialVersionUID = -1882711111814491060L; - /* Flags for FlexibleAdapter */ private boolean mExpanded = false; @@ -109,7 +107,7 @@ public L0ViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inf @Override public void bindViewHolder(FlexibleAdapter adapter, L0ViewHolder holder, int position, List payloads) { if (payloads.size() > 0) { - Log.i(this.getClass().getSimpleName(), "ExpandableHeaderItem Payload " + payloads); + Log.d(this.getClass().getSimpleName(), "ExpandableHeaderItem Payload " + payloads); } else { holder.mTitle.setText(getTitle()); } diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ExpandableLevel1Item.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ExpandableLevel1Item.java similarity index 64% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ExpandableLevel1Item.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ExpandableLevel1Item.java index b17830a9..30beaf42 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ExpandableLevel1Item.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ExpandableLevel1Item.java @@ -1,5 +1,8 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.Drawable; import android.util.Log; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -7,6 +10,7 @@ import java.util.ArrayList; import java.util.List; +import eu.davidea.flexibleadapter.utils.DrawableUtils; import eu.davidea.samples.flexibleadapter.R; import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.items.IExpandable; @@ -17,10 +21,8 @@ * It's important to note that, the ViewHolder must be specified in all <diamond> signature. */ public class ExpandableLevel1Item - extends AbstractModelItem - implements IExpandable { - - private static final long serialVersionUID = -1882711111814491060L; + extends AbstractItem + implements IExpandable { /* Flags for FlexibleAdapter */ private boolean mExpanded = false; @@ -73,7 +75,7 @@ public boolean removeSubItem(int position) { public void addSubItem(SubItem subItem) { if (mSubItems == null) - mSubItems = new ArrayList(); + mSubItems = new ArrayList<>(); mSubItems.add(subItem); } @@ -90,33 +92,36 @@ public int getLayoutRes() { } @Override - public SimpleItem.ParentViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { - return new SimpleItem.ParentViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); + public ExpandableItem.ParentViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { + return new ExpandableItem.ParentViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); } @Override - public void bindViewHolder(final FlexibleAdapter adapter, SimpleItem.ParentViewHolder holder, int position, List payloads) { - if (payloads.size() > 0) { - Log.i(this.getClass().getSimpleName(), "ExpandableHeaderItem Payload " + payloads); - } else { - holder.mTitle.setText(getTitle()); - } + public void bindViewHolder(final FlexibleAdapter adapter, ExpandableItem.ParentViewHolder holder, int position, List payloads) { + Context context = holder.itemView.getContext(); + setSubtitle(adapter.getCurrentChildren(this).size() + " subItems"); holder.mSubtitle.setText(getSubtitle()); - //ANIMATION EXAMPLE!! ImageView - Handle Flip Animation on Select ALL and Deselect ALL + // Background, when bound the first time + if (payloads.size() == 0) { + holder.mTitle.setText(getTitle()); + + Drawable drawable = DrawableUtils.getSelectableBackgroundCompat( + Color.WHITE, Color.parseColor("#dddddd"), // Same color of divider + DrawableUtils.getColorControlHighlight(context)); + DrawableUtils.setBackgroundCompat(holder.itemView, drawable); + DrawableUtils.setBackgroundCompat(holder.frontView, drawable); + } else { + Log.d(this.getClass().getSimpleName(), "ExpandableHeaderItem Payload " + payloads); + } + + // ANIMATION EXAMPLE!! ImageView - Handle Flip Animation on Select ALL and Deselect ALL if (adapter.isSelectAll() || adapter.isLastItemInActionMode()) { - //Reset the flags with delay - holder.itemView.postDelayed(new Runnable() { - @Override - public void run() { - adapter.resetActionModeFlags(); - } - }, 200L); - //Consume the Animation + // Consume the Animation holder.mFlipView.flip(adapter.isSelected(position), 200L); } else { - //Display the current flip status + // Display the current flip status holder.mFlipView.flipSilently(adapter.isSelected(position)); } } diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/HeaderItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/HeaderItem.java similarity index 87% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/HeaderItem.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/HeaderItem.java index 27ef51c6..31484fdd 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/HeaderItem.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/HeaderItem.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.support.v7.widget.StaggeredGridLayoutManager; import android.util.Log; @@ -18,14 +18,12 @@ import eu.davidea.viewholders.FlexibleViewHolder; /** - * This is a simple item with custom layout for headers. - *

A Section should not contain others Sections!

- * Headers are not Sectionable! + * This is a header item with custom layout for section headers. + *

Note: THIS ITEM IS NOT A SCROLLABLE HEADER.

+ * A Section should not contain others Sections and headers are not Sectionable! */ public class HeaderItem extends AbstractHeaderItem implements IFilterable { - private static final long serialVersionUID = -7408637077727563374L; - private String id; private String title; private String subtitle; @@ -88,7 +86,7 @@ public HeaderViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater @SuppressWarnings("unchecked") public void bindViewHolder(FlexibleAdapter adapter, HeaderViewHolder holder, int position, List payloads) { if (payloads.size() > 0) { - Log.i(this.getClass().getSimpleName(), "HeaderItem " + id + " Payload " + payloads); + Log.d(this.getClass().getSimpleName(), "HeaderItem " + id + " Payload " + payloads); } else { holder.mTitle.setText(getTitle()); } @@ -105,11 +103,11 @@ public boolean filter(String constraint) { static class HeaderViewHolder extends FlexibleViewHolder { - public TextView mTitle; - public TextView mSubtitle; - public ImageView mHandleView; + TextView mTitle; + TextView mSubtitle; + ImageView mHandleView; - public HeaderViewHolder(View view, FlexibleAdapter adapter) { + HeaderViewHolder(View view, FlexibleAdapter adapter) { super(view, adapter, true);//True for sticky mTitle = (TextView) view.findViewById(R.id.title); mSubtitle = (TextView) view.findViewById(R.id.subtitle); diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/InstagramHeaderItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/InstagramHeaderItem.java similarity index 94% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/InstagramHeaderItem.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/InstagramHeaderItem.java index 9a0a86b6..85380739 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/InstagramHeaderItem.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/InstagramHeaderItem.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.support.v7.widget.StaggeredGridLayoutManager; import android.util.Log; @@ -73,7 +73,7 @@ public HeaderViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater @SuppressWarnings("unchecked") public void bindViewHolder(FlexibleAdapter adapter, HeaderViewHolder holder, int position, List payloads) { if (payloads.size() > 0) { - Log.i(this.getClass().getSimpleName(), "InstagramHeaderItem Payload " + payloads); + Log.d(this.getClass().getSimpleName(), "InstagramHeaderItem Payload " + payloads); } else { holder.mTitle.setText(getTitle()); } @@ -82,9 +82,9 @@ public void bindViewHolder(FlexibleAdapter adapter, HeaderViewHolder holder, int static class HeaderViewHolder extends FlexibleViewHolder { - public FlipView mAccountImage; - public TextView mTitle; - public TextView mSubtitle; + FlipView mAccountImage; + TextView mTitle; + TextView mSubtitle; public HeaderViewHolder(View view, FlexibleAdapter adapter) { super(view, adapter, true);//True for sticky diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/InstagramItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/InstagramItem.java similarity index 93% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/InstagramItem.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/InstagramItem.java index e1589d25..2214d6b3 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/InstagramItem.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/InstagramItem.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.content.Context; import android.view.LayoutInflater; @@ -86,11 +86,11 @@ public void bindViewHolder(final FlexibleAdapter adapter, ViewHolder holder, int static final class ViewHolder extends FlexibleViewHolder { - public ImageView mImage; - public FlipView mImageFavourite; - public ImageView mImageComment; - public ImageView mImageShare; - public TextView mQuantityLikes; + ImageView mImage; + FlipView mImageFavourite; + ImageView mImageComment; + ImageView mImageShare; + TextView mQuantityLikes; public ViewHolder(View view, FlexibleAdapter adapter) { super(view, adapter); diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/OverallItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/OverallItem.java similarity index 98% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/OverallItem.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/OverallItem.java index 7674cfda..6e9401df 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/OverallItem.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/OverallItem.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.animation.Animator; import android.graphics.drawable.Drawable; diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ProgressItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ProgressItem.java new file mode 100644 index 00000000..e11a960c --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ProgressItem.java @@ -0,0 +1,121 @@ +package eu.davidea.samples.flexibleadapter.items; + +import android.animation.Animator; +import android.content.Context; +import android.support.annotation.NonNull; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.TextView; + +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.Payload; +import eu.davidea.flexibleadapter.helpers.AnimatorHelper; +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; +import eu.davidea.samples.flexibleadapter.R; +import eu.davidea.samples.flexibleadapter.items.ProgressItem.ProgressViewHolder; +import eu.davidea.viewholders.FlexibleViewHolder; + +/** + * @author Davide Steduto + * @since 22/04/2016 + */ +public class ProgressItem extends AbstractFlexibleItem { + + private StatusEnum status = StatusEnum.MORE_TO_LOAD; + + @Override + public boolean equals(Object o) { + return this == o;//The default implementation + } + + public StatusEnum getStatus() { + return status; + } + + public void setStatus(StatusEnum status) { + this.status = status; + } + + @Override + public int getLayoutRes() { + return R.layout.progress_item; + } + + @Override + public ProgressViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { + return new ProgressViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); + } + + @Override + public void bindViewHolder(FlexibleAdapter adapter, ProgressViewHolder holder, + int position, List payloads) { + + Context context = holder.itemView.getContext(); + holder.progressBar.setVisibility(View.GONE); + holder.progressMessage.setVisibility(View.VISIBLE); + + if (!adapter.isEndlessScrollEnabled()) { + setStatus(StatusEnum.DISABLE_ENDLESS); + } else if (payloads.contains(Payload.NO_MORE_LOAD)) { + setStatus(StatusEnum.NO_MORE_LOAD); + } + + switch (this.status) { + case NO_MORE_LOAD: + holder.progressMessage.setText( + context.getString(R.string.no_more_load_retry)); + // Reset to default status for next binding + setStatus(StatusEnum.MORE_TO_LOAD); + break; + case DISABLE_ENDLESS: + holder.progressMessage.setText(context.getString(R.string.endless_disabled)); + break; + case ON_CANCEL: + holder.progressMessage.setText(context.getString(R.string.endless_cancel)); + // Reset to default status for next binding + setStatus(StatusEnum.MORE_TO_LOAD); + break; + case ON_ERROR: + holder.progressMessage.setText(context.getString(R.string.endless_error)); + // Reset to default status for next binding + setStatus(StatusEnum.MORE_TO_LOAD); + break; + default: + holder.progressBar.setVisibility(View.VISIBLE); + holder.progressMessage.setVisibility(View.GONE); + break; + } + } + + static class ProgressViewHolder extends FlexibleViewHolder { + + @BindView(R.id.progress_bar) + ProgressBar progressBar; + @BindView(R.id.progress_message) + TextView progressMessage; + + ProgressViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter); + ButterKnife.bind(this, view); + } + @Override + public void scrollAnimators(@NonNull List animators, int position, boolean isForward) { + AnimatorHelper.scaleAnimator(animators, itemView, 0f); + } + } + + public enum StatusEnum { + MORE_TO_LOAD, //Default = should have an empty Payload + DISABLE_ENDLESS, //Endless is disabled because user has set limits + NO_MORE_LOAD, //Non-empty Payload = Payload.NO_MORE_LOAD + ON_CANCEL, + ON_ERROR + } + +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableExpandableItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableExpandableItem.java new file mode 100644 index 00000000..600bca7d --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableExpandableItem.java @@ -0,0 +1,112 @@ +package eu.davidea.samples.flexibleadapter.items; + +import android.animation.Animator; +import android.support.annotation.NonNull; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.helpers.AnimatorHelper; +import eu.davidea.flexibleadapter.items.IExpandable; +import eu.davidea.samples.flexibleadapter.R; +import eu.davidea.utils.Utils; +import eu.davidea.viewholders.ExpandableViewHolder; + +import eu.davidea.samples.flexibleadapter.items.ScrollableExpandableItem.ScrollableExpandableViewHolder; + +/** + * Scrollable Header and Footer Item that can be expanded too. When visible, all the subItems + * will be Headers or Footers as well, depending where the parent has been initially added! + */ +public class ScrollableExpandableItem extends AbstractItem + implements IExpandable { + + /* Flags for FlexibleAdapter */ + private boolean mExpanded = false; + + /* subItems list */ + private List mSubItems; + + + public ScrollableExpandableItem(String id) { + super(id); + } + + @Override + public int getLayoutRes() { + return R.layout.recycler_scrollable_expandable_item; + } + + @Override + public ScrollableExpandableViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { + return new ScrollableExpandableViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); + } + + @Override + public void bindViewHolder(FlexibleAdapter adapter, ScrollableExpandableViewHolder holder, int position, List payloads) { + holder.mTitle.setSelected(true);//For marquee!! + holder.mTitle.setText(Utils.fromHtmlCompat(getTitle())); + holder.mSubtitle.setText(Utils.fromHtmlCompat(getSubtitle())); + + //Support for StaggeredGridLayoutManager + if (holder.itemView.getLayoutParams() instanceof StaggeredGridLayoutManager.LayoutParams) { + ((StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams()).setFullSpan(true); + Log.d("ScrollableExpandable", "ScrollableExpandableItem configured fullSpan for StaggeredGridLayout"); + } + } + + @Override + public boolean isExpanded() { + return mExpanded; + } + + @Override + public void setExpanded(boolean expanded) { + this.mExpanded = expanded; + } + + @Override + public int getExpansionLevel() { + return 0; + } + + @Override + public List getSubItems() { + return mSubItems; + } + + public void addSubItem(ScrollableSubItem subItem) { + if (mSubItems == null) + mSubItems = new ArrayList<>(); + mSubItems.add(subItem); + } + + static class ScrollableExpandableViewHolder extends ExpandableViewHolder { + + public TextView mTitle; + public TextView mSubtitle; + + ScrollableExpandableViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter, true); + mTitle = (TextView) view.findViewById(R.id.title); + mSubtitle = (TextView) view.findViewById(R.id.subtitle); + } + + @Override + public void scrollAnimators(@NonNull List animators, int position, boolean isForward) { + AnimatorHelper.slideInFromTopAnimator(animators, itemView, mAdapter.getRecyclerView()); + } + } + + @Override + public String toString() { + return "ScrollableExpandableItem[" + super.toString() + "]"; + } +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableFooterItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableFooterItem.java new file mode 100644 index 00000000..d52f5321 --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableFooterItem.java @@ -0,0 +1,117 @@ +package eu.davidea.samples.flexibleadapter.items; + +import android.animation.Animator; +import android.support.annotation.NonNull; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewPropertyAnimatorListener; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.helpers.AnimatorHelper; +import eu.davidea.samples.flexibleadapter.R; +import eu.davidea.utils.Utils; +import eu.davidea.viewholders.AnimatedViewHolder; +import eu.davidea.viewholders.FlexibleViewHolder; + +/** + * This item is a Scrollable Footer. + */ +public class ScrollableFooterItem extends AbstractItem { + + public ScrollableFooterItem(String id) { + super(id); + } + + @Override + public int getLayoutRes() { + return R.layout.recycler_scrollable_footer_item; + } + + @Override + public FooterViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { + return new FooterViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); + } + + @Override + public void bindViewHolder(FlexibleAdapter adapter, FooterViewHolder holder, int position, List payloads) { + holder.mTitle.setText(Utils.fromHtmlCompat(getTitle())); + holder.mSubtitle.setText(Utils.fromHtmlCompat(getSubtitle())); + } + + class FooterViewHolder extends FlexibleViewHolder implements AnimatedViewHolder { + + TextView mTitle; + TextView mSubtitle; + ImageView mDismissIcon; + + FooterViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter); + mTitle = (TextView) view.findViewById(R.id.title); + mSubtitle = (TextView) view.findViewById(R.id.subtitle); + mDismissIcon = (ImageView) view.findViewById(R.id.dismiss_icon); + mDismissIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + //Don't need anymore to set permanent deletion for Scrollable Headers and Footers + //mAdapter.setPermanentDelete(true); + //noinspection unchecked + mAdapter.removeScrollableFooter(ScrollableFooterItem.this); + //mAdapter.setPermanentDelete(false); + } + }); + + //Support for StaggeredGridLayoutManager + if (itemView.getLayoutParams() instanceof StaggeredGridLayoutManager.LayoutParams) { + ((StaggeredGridLayoutManager.LayoutParams) itemView.getLayoutParams()).setFullSpan(true); + } + } + + @Override + public void scrollAnimators(@NonNull List animators, int position, boolean isForward) { + AnimatorHelper.slideInFromBottomAnimator(animators, itemView, mAdapter.getRecyclerView()); + } + + @Override + public boolean preAnimateAddImpl() { + ViewCompat.setTranslationY(itemView, itemView.getHeight()); + ViewCompat.setAlpha(itemView, 0); + return true; + } + + @Override + public boolean preAnimateRemoveImpl() { + return false; + } + + @Override + public boolean animateAddImpl(ViewPropertyAnimatorListener listener, long addDuration, int index) { + ViewCompat.animate(itemView) + .translationY(0) + .alpha(1) + .setDuration(addDuration) + .setInterpolator(new DecelerateInterpolator()) + .setListener(listener) + .start(); + return true; + } + + @Override + public boolean animateRemoveImpl(ViewPropertyAnimatorListener listener, long removeDuration, int index) { + return false; + } + } + + @Override + public String toString() { + return "ScrollableFooterItem[" + super.toString() + "]"; + } + +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableLayoutItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableLayoutItem.java new file mode 100644 index 00000000..59b49515 --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableLayoutItem.java @@ -0,0 +1,73 @@ +package eu.davidea.samples.flexibleadapter.items; + +import android.animation.Animator; +import android.support.annotation.NonNull; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.helpers.AnimatorHelper; +import eu.davidea.samples.flexibleadapter.R; +import eu.davidea.utils.Utils; +import eu.davidea.viewholders.FlexibleViewHolder; + +/** + * Item dedicated to display which Layout is currently displayed. + * This item is a Scrollable Header. + */ +public class ScrollableLayoutItem extends AbstractItem { + + public ScrollableLayoutItem(String id) { + super(id); + } + + @Override + public int getLayoutRes() { + return R.layout.recycler_scrollable_layout_item; + } + + @Override + public LayoutViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { + return new LayoutViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); + } + + @Override + public void bindViewHolder(FlexibleAdapter adapter, LayoutViewHolder holder, int position, List payloads) { + holder.mTitle.setText(Utils.fromHtmlCompat(getTitle())); + holder.mSubtitle.setText(Utils.fromHtmlCompat(getSubtitle())); + + //Support for StaggeredGridLayoutManager + if (holder.itemView.getLayoutParams() instanceof StaggeredGridLayoutManager.LayoutParams) { + ((StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams()).setFullSpan(true); + Log.d("ScrollableLayoutItem", "LayoutItem configured fullSpan for StaggeredGridLayout"); + } + } + + public static class LayoutViewHolder extends FlexibleViewHolder { + + public TextView mTitle; + public TextView mSubtitle; + + public LayoutViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter, true); + mTitle = (TextView) view.findViewById(R.id.title); + mSubtitle = (TextView) view.findViewById(R.id.subtitle); + } + + @Override + public void scrollAnimators(@NonNull List animators, int position, boolean isForward) { + AnimatorHelper.slideInFromTopAnimator(animators, itemView, mAdapter.getRecyclerView()); + } + } + + @Override + public String toString() { + return "ScrollableLayoutItem[" + super.toString() + "]"; + } +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableSubItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableSubItem.java new file mode 100644 index 00000000..71f77315 --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableSubItem.java @@ -0,0 +1,69 @@ +package eu.davidea.samples.flexibleadapter.items; + +import android.animation.Animator; +import android.support.annotation.NonNull; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.helpers.AnimatorHelper; +import eu.davidea.samples.flexibleadapter.R; +import eu.davidea.viewholders.FlexibleViewHolder; + +/** + * Scrollable SubHeader and SubFooter Item. When visible, will be a Header or a Footer, + * depending where the parent has been initially added! + */ +public class ScrollableSubItem extends AbstractItem { + + public ScrollableSubItem(String id) { + super(id); + } + + @Override + public int getLayoutRes() { + return R.layout.recycler_scrollable_sub_item; + } + + @Override + public ChildViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { + return new ChildViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); + } + + @Override + @SuppressWarnings("unchecked") + public void bindViewHolder(FlexibleAdapter adapter, ChildViewHolder holder, int position, List payloads) { + String title = "Scrollable SubItem " + adapter.getSubPositionOf(this); + holder.mTitle.setText(title); + } + + /** + * Provide a reference to the views for each data item. + * Complex data labels may need more than one view per item, and + * you provide access to all the views for a data item in a view holder. + */ + static final class ChildViewHolder extends FlexibleViewHolder { + + TextView mTitle; + + ChildViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter); + this.mTitle = (TextView) view.findViewById(R.id.title); + } + + @Override + public void scrollAnimators(@NonNull List animators, int position, boolean isForward) { + AnimatorHelper.scaleAnimator(animators, itemView, 0f); + } + } + + @Override + public String toString() { + return "ScrollableSubItem[" + super.toString() + "]"; + } + +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ULSItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableULSItem.java similarity index 54% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ULSItem.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableULSItem.java index ca18062d..904cf5e0 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ULSItem.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableULSItem.java @@ -1,9 +1,8 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.animation.Animator; import android.support.annotation.NonNull; import android.support.v7.widget.StaggeredGridLayoutManager; -import android.text.Html; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -16,62 +15,49 @@ import eu.davidea.flexibleadapter.helpers.AnimatorHelper; import eu.davidea.samples.flexibleadapter.R; import eu.davidea.samples.flexibleadapter.services.DatabaseConfiguration; +import eu.davidea.utils.Utils; import eu.davidea.viewholders.FlexibleViewHolder; /** - * Item dedicated only for User Learns Selection view (located always at position 0 in the Adapter). - *

If you don't have many fields in common better to extend directly from - * {@link eu.davidea.flexibleadapter.items.AbstractFlexibleItem} to benefit of the already - * implemented methods (getter and setters).

+ * Item dedicated only for User Learns Selection view (located always at the top in the Adapter). + * This item is a Scrollable Header. */ -public class ULSItem extends AbstractModelItem { +public class ScrollableULSItem extends AbstractItem { - private static final long serialVersionUID = -5041296095060813327L; - - public ULSItem(String id) { + public ScrollableULSItem(String id) { super(id); } - @Override - public boolean isEnabled() { - return false; - } - - @Override - public boolean isSelectable() { - return false; - } - @Override public int getLayoutRes() { - return R.layout.recycler_uls_item; + return R.layout.recycler_scrollable_uls_item; } @Override - public ExampleViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { - return new ExampleViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); + public ULSViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { + return new ULSViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); } @Override - public void bindViewHolder(FlexibleAdapter adapter, ExampleViewHolder holder, int position, List payloads) { + public void bindViewHolder(FlexibleAdapter adapter, ULSViewHolder holder, int position, List payloads) { holder.mImageView.setImageResource(R.drawable.ic_account_circle_white_24dp); holder.itemView.setActivated(true); - holder.mTitle.setSelected(true);//For marquee - holder.mTitle.setText(Html.fromHtml(getTitle())); - holder.mSubtitle.setText(Html.fromHtml(getSubtitle())); + holder.mTitle.setSelected(true);//For marquee!! + holder.mTitle.setText(Utils.fromHtmlCompat(getTitle())); + holder.mSubtitle.setText(Utils.fromHtmlCompat(getSubtitle())); } /** * Used for UserLearnsSelection. */ - static class ExampleViewHolder extends FlexibleViewHolder { + class ULSViewHolder extends FlexibleViewHolder { - public ImageView mImageView; - public TextView mTitle; - public TextView mSubtitle; - public ImageView mDismissIcon; + ImageView mImageView; + TextView mTitle; + TextView mSubtitle; + ImageView mDismissIcon; - public ExampleViewHolder(View view, FlexibleAdapter adapter) { + ULSViewHolder(View view, FlexibleAdapter adapter) { super(view, adapter); mTitle = (TextView) view.findViewById(R.id.title); mSubtitle = (TextView) view.findViewById(R.id.subtitle); @@ -81,9 +67,11 @@ public ExampleViewHolder(View view, FlexibleAdapter adapter) { @Override public void onClick(View v) { DatabaseConfiguration.userLearnedSelection = true; - mAdapter.setPermanentDelete(true); - mAdapter.removeItem(0); - mAdapter.setPermanentDelete(false); + //Don't need anymore to set permanent deletion for Scrollable Headers and Footers + //mAdapter.setPermanentDelete(true); + //noinspection unchecked + mAdapter.removeScrollableHeader(ScrollableULSItem.this); + //mAdapter.setPermanentDelete(false); } }); @@ -101,7 +89,7 @@ public void scrollAnimators(@NonNull List animators, int position, boo @Override public String toString() { - return "ULSItem[" + super.toString() + "]"; + return "ScrollableULSItem[" + super.toString() + "]"; } } \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableUseCaseItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableUseCaseItem.java new file mode 100644 index 00000000..d531c3cd --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/ScrollableUseCaseItem.java @@ -0,0 +1,94 @@ +package eu.davidea.samples.flexibleadapter.items; + +import android.animation.Animator; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.helpers.AnimatorHelper; +import eu.davidea.flexibleadapter.utils.DrawableUtils; +import eu.davidea.samples.flexibleadapter.R; +import eu.davidea.utils.Utils; +import eu.davidea.viewholders.FlexibleViewHolder; + +/** + * This item is a Scrollable Header. + */ +public class ScrollableUseCaseItem extends AbstractItem { + + public ScrollableUseCaseItem(String title, String subTitle) { + super("UC"); + setTitle(title); + setSubtitle(subTitle); + } + + @Override + public int getLayoutRes() { + return R.layout.recycler_scrollable_usecase_item; + } + + @Override + public UCViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { + return new UCViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); + } + + @Override + public void bindViewHolder(FlexibleAdapter adapter, UCViewHolder holder, int position, List payloads) { + Context context = holder.itemView.getContext(); + DrawableUtils.setBackgroundCompat(holder.itemView, DrawableUtils.getRippleDrawable( + DrawableUtils.getColorDrawable(context.getResources().getColor(R.color.material_color_blue_grey_50)), + DrawableUtils.getColorControlHighlight(context)) + ); + holder.mTitle.setText(Utils.fromHtmlCompat(getTitle())); + holder.mSubtitle.setText(Utils.fromHtmlCompat(getSubtitle())); + } + + public static class UCViewHolder extends FlexibleViewHolder { + + public TextView mTitle; + public TextView mSubtitle; + ImageView mDismissIcon; + + public UCViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter); + mTitle = (TextView) view.findViewById(R.id.title); + mSubtitle = (TextView) view.findViewById(R.id.subtitle); + mDismissIcon = (ImageView) view.findViewById(R.id.dismiss_icon); + mDismissIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + //Don't need anymore to set permanent deletion for Scrollable Headers and Footers + //mAdapter.setPermanentDelete(true); + //noinspection unchecked, ConstantConditions + mAdapter.removeScrollableHeader(mAdapter.getItem(getAdapterPosition())); + //mAdapter.setPermanentDelete(false); + } + }); + + //Support for StaggeredGridLayoutManager + if (itemView.getLayoutParams() instanceof StaggeredGridLayoutManager.LayoutParams) { + ((StaggeredGridLayoutManager.LayoutParams) itemView.getLayoutParams()).setFullSpan(true); + } + } + + @Override + public void scrollAnimators(@NonNull List animators, int position, boolean isForward) { + AnimatorHelper.flipAnimator(animators, itemView); + AnimatorHelper.setDuration(animators, 500L); + } + } + + @Override + public String toString() { + return "ScrollableUseCaseItem[" + super.toString() + "]"; + } + +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/SimpleItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/SimpleItem.java similarity index 57% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/SimpleItem.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/SimpleItem.java index 2a973054..76c56cdc 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/SimpleItem.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/SimpleItem.java @@ -1,11 +1,13 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.animation.Animator; import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.StaggeredGridLayoutManager; -import android.util.Log; +import android.support.v7.widget.helper.ItemTouchHelper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -18,30 +20,26 @@ import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.helpers.AnimatorHelper; -import eu.davidea.flexibleadapter.items.IExpandable; import eu.davidea.flexibleadapter.items.IFilterable; import eu.davidea.flexibleadapter.items.ISectionable; +import eu.davidea.flexibleadapter.utils.DrawableUtils; import eu.davidea.flexibleadapter.utils.Utils; import eu.davidea.flipview.FlipView; import eu.davidea.samples.flexibleadapter.R; -import eu.davidea.viewholders.ExpandableViewHolder; +import eu.davidea.viewholders.FlexibleViewHolder; /** * You should extend directly from * {@link eu.davidea.flexibleadapter.items.AbstractFlexibleItem} to benefit of the already * implemented methods (getter and setters). */ -public class SimpleItem extends AbstractModelItem - implements ISectionable, IFilterable, Serializable { +public class SimpleItem extends AbstractItem + implements ISectionable, IFilterable, Serializable { - private static final long serialVersionUID = -6882745111884490060L; - - /** - * The header of this item - */ + /* The header of this item */ HeaderItem header; - public SimpleItem(String id) { + private SimpleItem(String id) { super(id); setDraggable(true); setSwipeable(true); @@ -52,6 +50,13 @@ public SimpleItem(String id, HeaderItem header) { this.header = header; } + @Override + public String getSubtitle() { + return getId() + + (getHeader() != null ? " - " + getHeader().getId() : "") + + (getUpdates() > 0 ? " - u" + getUpdates() : ""); + } + @Override public HeaderItem getHeader() { return header; @@ -64,67 +69,44 @@ public void setHeader(HeaderItem header) { @Override public int getLayoutRes() { - return R.layout.recycler_expandable_item; + return R.layout.recycler_simple_item; } @Override - public ParentViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { - return new ParentViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); + public SimpleViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { + return new SimpleViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); } @Override - @SuppressWarnings({"unchecked", "deprecation"}) - public void bindViewHolder(final FlexibleAdapter adapter, ParentViewHolder holder, int position, List payloads) { - //Subtitle - if (adapter.isExpandable(this)) { - setSubtitle(adapter.getCurrentChildren((IExpandable) this).size() + " subItems"); - } else { - setSubtitle(getId()); - } - setSubtitle(getSubtitle() + (getHeader() != null ? " - " + getHeader().getId() : "")); - + @SuppressWarnings({"unchecked"}) + public void bindViewHolder(final FlexibleAdapter adapter, SimpleViewHolder holder, int position, List payloads) { Context context = holder.itemView.getContext(); - int defColorAccent = context.getResources().getColor(R.color.colorAccent_light); - if (adapter.isExpandable(this) && payloads.size() > 0) { - Log.i(this.getClass().getSimpleName(), "ExpandableItem Payload " + payloads); - if (adapter.hasSearchText()) { - Utils.highlightText(holder.itemView.getContext(), holder.mSubtitle, - getSubtitle(), adapter.getSearchText(), defColorAccent); - } else { - holder.mSubtitle.setText(getSubtitle()); - } - //We stop the process here, we only want to update the subtitle + // Background, when bound the first time + if (payloads.size() == 0) { + Drawable drawable = DrawableUtils.getSelectableBackgroundCompat( + Color.WHITE, Color.parseColor("#dddddd"), //Same color of divider + DrawableUtils.getColorControlHighlight(context)); + DrawableUtils.setBackgroundCompat(holder.itemView, drawable); + DrawableUtils.setBackgroundCompat(holder.frontView, drawable); + } + // DemoApp: INNER ANIMATION EXAMPLE! ImageView - Handle Flip Animation +// if (adapter.isSelectAll() || adapter.isLastItemInActionMode()) { +// // Consume the Animation +// holder.mFlipView.flip(adapter.isSelected(position), 200L); +// } else { + // Display the current flip status + holder.mFlipView.flipSilently(adapter.isSelected(position)); +// } + + // In case of searchText matches with Title or with a field this will be highlighted + if (adapter.hasSearchText()) { + Utils.highlightText(holder.mTitle, getTitle(), adapter.getSearchText()); + Utils.highlightText(holder.mSubtitle, getSubtitle(), adapter.getSearchText()); } else { - //DemoApp: INNER ANIMATION EXAMPLE! ImageView - Handle Flip Animation on Select ALL - // and Deselect ALL - if (adapter.isSelectAll() || adapter.isLastItemInActionMode()) { - //Reset the flags with delay - holder.itemView.postDelayed(new Runnable() { - @Override - public void run() { - adapter.resetActionModeFlags(); - } - }, 200L); - //Consume the Animation - holder.mFlipView.flip(adapter.isSelected(position), 200L); - } else { - //Display the current flip status - holder.mFlipView.flipSilently(adapter.isSelected(position)); - } - - //In case of searchText matches with Title or with an SimpleItem's field - // this will be highlighted - if (adapter.hasSearchText()) { - Utils.highlightText(context, holder.mTitle, - getTitle(), adapter.getSearchText(), defColorAccent); - Utils.highlightText(context, holder.mSubtitle, - getSubtitle(), adapter.getSearchText(), defColorAccent); - } else { - holder.mTitle.setText(getTitle()); - holder.mSubtitle.setText(getSubtitle()); - } + holder.mTitle.setText(getTitle()); + holder.mSubtitle.setText(getSubtitle()); } } @@ -134,21 +116,20 @@ public boolean filter(String constraint) { getSubtitle() != null && getSubtitle().toLowerCase().trim().contains(constraint); } - /** - * This ViewHolder is expandable and collapsible. - */ - static final class ParentViewHolder extends ExpandableViewHolder { - - public FlipView mFlipView; - public TextView mTitle; - public TextView mSubtitle; - public ImageView mHandleView; - public Context mContext; - private View frontView; - private View rearLeftView; - private View rearRightView; - - public ParentViewHolder(View view, FlexibleAdapter adapter) { + static final class SimpleViewHolder extends FlexibleViewHolder { + + FlipView mFlipView; + TextView mTitle; + TextView mSubtitle; + ImageView mHandleView; + Context mContext; + View frontView; + View rearLeftView; + View rearRightView; + + public boolean swiped = false; + + SimpleViewHolder(View view, FlexibleAdapter adapter) { super(view, adapter); this.mContext = view.getContext(); this.mTitle = (TextView) view.findViewById(R.id.title); @@ -157,9 +138,11 @@ public ParentViewHolder(View view, FlexibleAdapter adapter) { this.mFlipView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - mAdapter.mItemLongClickListener.onItemLongClick(getAdapterPosition()); - Toast.makeText(mContext, "ImageClick on " + mTitle.getText() + " position " + getAdapterPosition(), Toast.LENGTH_SHORT).show(); - toggleActivation(); + if (mAdapter.mItemLongClickListener != null) { + mAdapter.mItemLongClickListener.onItemLongClick(getAdapterPosition()); + Toast.makeText(mContext, "ImageClick on " + mTitle.getText() + " position " + getAdapterPosition(), Toast.LENGTH_SHORT).show(); + toggleActivation(); + } } }); this.mHandleView = (ImageView) view.findViewById(R.id.row_handle); @@ -193,9 +176,9 @@ public boolean onLongClick(View view) { } @Override - protected void toggleActivation() { + public void toggleActivation() { super.toggleActivation(); - //Here we use a custom Animation inside the ItemView + // Here we use a custom Animation inside the ItemView mFlipView.flip(mAdapter.isSelected(getAdapterPosition())); } @@ -245,12 +228,17 @@ public void scrollAnimators(@NonNull List animators, int position, boo AnimatorHelper.slideInFromLeftAnimator(animators, itemView, mAdapter.getRecyclerView(), 0.5f); } } + + @Override + public void onItemReleased(int position) { + swiped = (mActionState == ItemTouchHelper.ACTION_STATE_SWIPE); + super.onItemReleased(position); + } } @Override public String toString() { - return this instanceof ExpandableItem ? super.toString() : - "SimpleItem[" + super.toString() + "]"; + return "SimpleItem[" + super.toString() + "]"; } } \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/StaggeredHeaderItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/StaggeredHeaderItem.java similarity index 93% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/StaggeredHeaderItem.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/StaggeredHeaderItem.java index 3df16546..ec10506b 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/StaggeredHeaderItem.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/StaggeredHeaderItem.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.support.v7.widget.StaggeredGridLayoutManager; import android.util.Log; @@ -62,7 +62,7 @@ public HeaderViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater @SuppressWarnings("unchecked") public void bindViewHolder(FlexibleAdapter adapter, HeaderViewHolder holder, int position, List payloads) { if (payloads.size() > 0) { - Log.i(this.getClass().getSimpleName(), "StaggeredHeaderItem Payload " + payloads); + Log.d(this.getClass().getSimpleName(), "StaggeredHeaderItem Payload " + payloads); } else { String title = this.title + " (" + adapter.getSectionItems(this).size() + ")"; holder.title.setText(title); @@ -73,8 +73,6 @@ static class HeaderViewHolder extends FlexibleViewHolder { @BindView(R.id.title) TextView title; - @BindView(R.id.layout_elevation) - View elevationView; public HeaderViewHolder(View view, FlexibleAdapter adapter) { super(view, adapter, true);//True for sticky diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/StaggeredItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/StaggeredItem.java similarity index 93% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/StaggeredItem.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/StaggeredItem.java index 3a3c4453..d3aecb72 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/StaggeredItem.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/StaggeredItem.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.content.Context; import android.graphics.Color; @@ -83,7 +83,7 @@ public void setMergedItems(List mergedItems) { public void mergeItem(StaggeredItem staggeredItem) { if (mergedItems == null) { - mergedItems = new ArrayList(1); + mergedItems = new ArrayList<>(1); } mergedItems.add(staggeredItem); } @@ -116,17 +116,15 @@ public ViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater infla @Override public void bindViewHolder(final FlexibleAdapter adapter, final ViewHolder holder, int position, List payloads) { - Context context = holder.itemTextView.getContext(); + Context context = holder.itemView.getContext(); //Item Id holder.itemTextView.setText(toString()); //Item Status holder.statusTextView.setText(status.getResId()); - DrawableUtils.setBackground(holder.itemView, - DrawableUtils.getSelectableBackgroundCompat( - Color.WHITE, status.getColor(), - Utils.getColorAccent(holder.itemView.getContext()))); + DrawableUtils.setBackgroundCompat(holder.itemView, DrawableUtils.getSelectableBackgroundCompat( + status.getColor(), Utils.getColorAccent(context), Color.WHITE)); //Blink after moving the item for (Object payload : payloads) { diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/StaggeredItemStatus.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/StaggeredItemStatus.java similarity index 94% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/StaggeredItemStatus.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/StaggeredItemStatus.java index 39cc801f..f2722e9a 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/StaggeredItemStatus.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/StaggeredItemStatus.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.graphics.Color; diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/SubItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/SubItem.java similarity index 94% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/SubItem.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/SubItem.java index a3f0f1ff..5576859a 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/SubItem.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/items/SubItem.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter.models; +package eu.davidea.samples.flexibleadapter.items; import android.animation.Animator; import android.content.Context; @@ -25,11 +25,9 @@ * {@link eu.davidea.flexibleadapter.items.AbstractFlexibleItem} to benefit of the already * implemented methods (getter and setters). */ -public class SubItem extends AbstractModelItem +public class SubItem extends AbstractItem implements ISectionable, IFilterable { - private static final long serialVersionUID = 2519281529221244210L; - /** * The header of this item */ diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/AbstractModel.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/AbstractModel.java new file mode 100644 index 00000000..10482a26 --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/AbstractModel.java @@ -0,0 +1,76 @@ +package eu.davidea.samples.flexibleadapter.models; + +import java.io.Serializable; +import eu.davidea.flexibleadapter.items.IHolder; + +/** + * This class is Pojo for the {@link IHolder} items. + * It is used as base item for Item and Header model examples. + * + * Using Holder pattern, you can implement DB, XML & JSON (de)serialization libraries on this + * item as usual. + * + * @author Davide Steduto + * @since 19/10/2016 + */ +public abstract class AbstractModel implements Serializable { + + private static final long serialVersionUID = -7385882749119849060L; + + private String id; + private String title; + private String subtitle; + + public AbstractModel(String id) { + this.id = id; + } + + @Override + public boolean equals(Object inObject) { + if (inObject instanceof AbstractModel) { + AbstractModel inItem = (AbstractModel) inObject; + return this.id.equals(inItem.id); + } + return false; + } + + /** + * Override this method too, when using functionalities like StableIds, Filter or CollapseAll. + * FlexibleAdapter is making use of HashSet to improve performance, especially in big list. + */ + @Override + public int hashCode() { + return id.hashCode(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getSubtitle() { + return subtitle; + } + + public void setSubtitle(String subtitle) { + this.subtitle = subtitle; + } + + @Override + public String toString() { + return "id=" + id + + ", title=" + title; + } + +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ExpandableItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ExpandableItem.java deleted file mode 100644 index 0e947a84..00000000 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ExpandableItem.java +++ /dev/null @@ -1,92 +0,0 @@ -package eu.davidea.samples.flexibleadapter.models; - -import java.util.ArrayList; -import java.util.List; - -import eu.davidea.flexibleadapter.items.IExpandable; - -/** - * If you don't have fields in common (my example: SimpleItem) better to extend directly from - * {@link eu.davidea.flexibleadapter.items.AbstractExpandableItem} to benefit of the already - * implemented methods around subItems list. - */ -public class ExpandableItem extends SimpleItem - implements IExpandable { - - private static final long serialVersionUID = -6882745111884490060L; - - /* Flags for FlexibleAdapter */ - private boolean mExpanded = false; - - /* subItems list */ - private List mSubItems; - - - public ExpandableItem(String id) { - super(id); - } - - public ExpandableItem(String id, HeaderItem header) { - super(id, header); - } - - @Override - public boolean isExpanded() { - return mExpanded; - } - - @Override - public void setExpanded(boolean expanded) { - mExpanded = expanded; - } - - @Override - public int getExpansionLevel() { - return 0; - } - - @Override - public List getSubItems() { - return mSubItems; - } - - public final boolean hasSubItems() { - return mSubItems!= null && mSubItems.size() > 0; - } - - public boolean removeSubItem(SubItem item) { - return item != null && mSubItems.remove(item); - } - - public boolean removeSubItem(int position) { - if (mSubItems != null && position >= 0 && position < mSubItems.size()) { - mSubItems.remove(position); - return true; - } - return false; - } - - public void addSubItem(SubItem subItem) { - if (mSubItems == null) - mSubItems = new ArrayList(); - mSubItems.add(subItem); - } - - public void addSubItem(int position, SubItem subItem) { - if (mSubItems != null && position >= 0 && position < mSubItems.size()) { - mSubItems.add(position, subItem); - } else - addSubItem(subItem); - } - -// @Override -// public int getLayoutRes() { -// return R.layout.recycler_expandable_row; -// } - - @Override - public String toString() { - return "ExpandableItem[" + super.toString() + "//SubItems" + mSubItems + "]"; - } - -} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/HeaderModel.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/HeaderModel.java new file mode 100644 index 00000000..2e82d9c0 --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/HeaderModel.java @@ -0,0 +1,15 @@ +package eu.davidea.samples.flexibleadapter.models; + +/** + * Model item for HeaderHolder. + * + * @author Davide Steduto + * @since 19/10/2016 + */ +public class HeaderModel extends AbstractModel { + + public HeaderModel(String id) { + super(id); + } + +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ItemModel.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ItemModel.java new file mode 100644 index 00000000..22b8c93c --- /dev/null +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ItemModel.java @@ -0,0 +1,15 @@ +package eu.davidea.samples.flexibleadapter.models; + +/** + * Model item for ItemHolder. + * + * @author Davide Steduto + * @since 19/10/2016 + */ +public class ItemModel extends AbstractModel { + + public ItemModel(String id) { + super(id); + } + +} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/LayoutItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/LayoutItem.java deleted file mode 100644 index 691fe031..00000000 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/LayoutItem.java +++ /dev/null @@ -1,92 +0,0 @@ -package eu.davidea.samples.flexibleadapter.models; - -import android.animation.Animator; -import android.support.annotation.NonNull; -import android.support.v7.widget.StaggeredGridLayoutManager; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import java.util.List; - -import eu.davidea.flexibleadapter.FlexibleAdapter; -import eu.davidea.flexibleadapter.helpers.AnimatorHelper; -import eu.davidea.samples.flexibleadapter.R; -import eu.davidea.viewholders.FlexibleViewHolder; - -/** - * Item dedicated to display the status of the Layout currently displayed (located always at - * position 0 in the Adapter). - * - *

If you don't have many fields in common better to extend directly from - * {@link eu.davidea.flexibleadapter.items.AbstractFlexibleItem} to benefit of the already - * implemented methods (getter and setters).

- */ -public class LayoutItem extends AbstractModelItem { - - private static final long serialVersionUID = -5041296095060813327L; - - public LayoutItem(String id) { - super(id); - } - - @Override - public boolean isEnabled() { - return false; - } - - @Override - public boolean isSelectable() { - return false; - } - - @Override - public int getLayoutRes() { - return R.layout.recycler_layout_item; - } - - @Override - public ExampleViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { - return new ExampleViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); - } - - @Override - public void bindViewHolder(FlexibleAdapter adapter, ExampleViewHolder holder, int position, List payloads) { - holder.mTitle.setSelected(true);//For marquee - holder.mTitle.setText(getTitle()); - holder.mSubtitle.setText(getSubtitle()); - - //Support for StaggeredGridLayoutManager - if (holder.itemView.getLayoutParams() instanceof StaggeredGridLayoutManager.LayoutParams) { - ((StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams()).setFullSpan(true); - Log.d("LayoutItem", "LayoutItem configured fullSpan for StaggeredGridLayout"); - } - } - - /** - * Used for UserLearnsSelection. - */ - public static class ExampleViewHolder extends FlexibleViewHolder { - - public TextView mTitle; - public TextView mSubtitle; - - public ExampleViewHolder(View view, FlexibleAdapter adapter) { - super(view, adapter, true); - mTitle = (TextView) view.findViewById(R.id.title); - mSubtitle = (TextView) view.findViewById(R.id.subtitle); - } - - @Override - public void scrollAnimators(@NonNull List animators, int position, boolean isForward) { - AnimatorHelper.slideInFromTopAnimator(animators, itemView, mAdapter.getRecyclerView()); - } - } - - @Override - public String toString() { - return "LayoutItem[" + super.toString() + "]"; - } -} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ProgressItem.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ProgressItem.java deleted file mode 100644 index fa7aeb1c..00000000 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/models/ProgressItem.java +++ /dev/null @@ -1,59 +0,0 @@ -package eu.davidea.samples.flexibleadapter.models; - -import android.animation.Animator; -import android.support.annotation.NonNull; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ProgressBar; - -import java.util.List; - -import eu.davidea.flexibleadapter.FlexibleAdapter; -import eu.davidea.flexibleadapter.helpers.AnimatorHelper; -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; -import eu.davidea.samples.flexibleadapter.R; -import eu.davidea.samples.flexibleadapter.models.ProgressItem.ProgressViewHolder; -import eu.davidea.viewholders.FlexibleViewHolder; - -/** - * @author Davide Steduto - * @since 22/04/2016 - */ -public class ProgressItem extends AbstractFlexibleItem { - - @Override - public boolean equals(Object o) { - return this == o;//The default implementation - } - - @Override - public int getLayoutRes() { - return R.layout.progress_item; - } - - @Override - public ProgressViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { - return new ProgressViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); - } - - @Override - public void bindViewHolder(FlexibleAdapter adapter, ProgressViewHolder holder, int position, List payloads) { - //nothing to bind - } - - public static class ProgressViewHolder extends FlexibleViewHolder { - - public ProgressBar progressBar; - - public ProgressViewHolder(View view, FlexibleAdapter adapter) { - super(view, adapter); - progressBar = (ProgressBar) view.findViewById(R.id.progress_bar); - } - @Override - public void scrollAnimators(@NonNull List animators, int position, boolean isForward) { - AnimatorHelper.scaleAnimator(animators, itemView, 0f); - } - } - -} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/services/DatabaseConfiguration.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/services/DatabaseConfiguration.java index 3ed8868d..a4d0fef5 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/services/DatabaseConfiguration.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/services/DatabaseConfiguration.java @@ -18,11 +18,12 @@ public class DatabaseConfiguration { NOTIFY_MOVE = "notify_move"; //Values - public static int maxSize = 100000;//max number of items + public static int maxSize = 10000;//max number of items public static int maxSearchDelay = 1000;//max search delay in ms - public static int size = 10000;//items + public static int size = 1000;//items public static int delay = 200;//ms public static int animateToLimit = maxSize;//start with maxSize + public static boolean animateWithDiffUtil = false; public static boolean notifyChange = true; public static boolean notifyMove = false; public static boolean animateOnScrolling = true; diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/services/DatabaseService.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/services/DatabaseService.java index c3238f35..aa04412a 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/services/DatabaseService.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/services/DatabaseService.java @@ -17,23 +17,27 @@ import eu.davidea.flexibleadapter.items.IFlexible; import eu.davidea.flexibleadapter.items.IHeader; import eu.davidea.samples.flexibleadapter.R; -import eu.davidea.samples.flexibleadapter.models.AbstractModelItem; -import eu.davidea.samples.flexibleadapter.models.AnimatorExpandableItem; -import eu.davidea.samples.flexibleadapter.models.AnimatorSubItem; -import eu.davidea.samples.flexibleadapter.models.ConfigurationItem; -import eu.davidea.samples.flexibleadapter.models.ExpandableHeaderItem; -import eu.davidea.samples.flexibleadapter.models.ExpandableItem; -import eu.davidea.samples.flexibleadapter.models.ExpandableLevel0Item; -import eu.davidea.samples.flexibleadapter.models.ExpandableLevel1Item; -import eu.davidea.samples.flexibleadapter.models.HeaderItem; -import eu.davidea.samples.flexibleadapter.models.InstagramHeaderItem; -import eu.davidea.samples.flexibleadapter.models.InstagramItem; -import eu.davidea.samples.flexibleadapter.models.OverallItem; -import eu.davidea.samples.flexibleadapter.models.SimpleItem; -import eu.davidea.samples.flexibleadapter.models.StaggeredHeaderItem; -import eu.davidea.samples.flexibleadapter.models.StaggeredItem; -import eu.davidea.samples.flexibleadapter.models.StaggeredItemStatus; -import eu.davidea.samples.flexibleadapter.models.SubItem; +import eu.davidea.samples.flexibleadapter.items.AbstractItem; +import eu.davidea.samples.flexibleadapter.items.AnimatorExpandableItem; +import eu.davidea.samples.flexibleadapter.items.AnimatorSubItem; +import eu.davidea.samples.flexibleadapter.items.ConfigurationItem; +import eu.davidea.samples.flexibleadapter.items.ExpandableHeaderItem; +import eu.davidea.samples.flexibleadapter.items.ExpandableItem; +import eu.davidea.samples.flexibleadapter.items.ExpandableLevel0Item; +import eu.davidea.samples.flexibleadapter.items.ExpandableLevel1Item; +import eu.davidea.samples.flexibleadapter.holders.HeaderHolder; +import eu.davidea.samples.flexibleadapter.items.HeaderItem; +import eu.davidea.samples.flexibleadapter.models.HeaderModel; +import eu.davidea.samples.flexibleadapter.items.InstagramHeaderItem; +import eu.davidea.samples.flexibleadapter.items.InstagramItem; +import eu.davidea.samples.flexibleadapter.holders.ItemHolder; +import eu.davidea.samples.flexibleadapter.models.ItemModel; +import eu.davidea.samples.flexibleadapter.items.OverallItem; +import eu.davidea.samples.flexibleadapter.items.SimpleItem; +import eu.davidea.samples.flexibleadapter.items.StaggeredHeaderItem; +import eu.davidea.samples.flexibleadapter.items.StaggeredItem; +import eu.davidea.samples.flexibleadapter.items.StaggeredItemStatus; +import eu.davidea.samples.flexibleadapter.items.SubItem; /** * Created by Davide Steduto on 23/11/2015. @@ -51,7 +55,7 @@ public class DatabaseService { private Map headers; - DatabaseService() { + private DatabaseService() { } public static DatabaseService getInstance() { @@ -77,6 +81,8 @@ public DatabaseType getDatabaseType() { * List of CardView as entry list, showing the functionality of the library. * It also shows how adapter animation can be configured. */ + //TODO: Review the description of all examples + //TODO: Add ScrollableUseCaseItem header for each database public void createOverallDatabase(Resources resources) { databaseType = DatabaseType.OVERALL; mItems.clear(); @@ -116,12 +122,15 @@ public void createOverallDatabase(Resources resources) { mItems.add(new OverallItem(R.id.nav_model_holders, resources.getString(R.string.model_holders)) .withDescription(resources.getString(R.string.model_holders_description)) - .withIcon(resources.getDrawable(R.drawable.ic_select_inverse_grey600_24dp)) - .withEnabled(false)); + .withIcon(resources.getDrawable(R.drawable.ic_select_inverse_grey600_24dp))); mItems.add(new OverallItem(R.id.nav_staggered, resources.getString(R.string.staggered_layout)) .withDescription(resources.getString(R.string.staggered_description)) .withIcon(resources.getDrawable(R.drawable.ic_dashboard_grey600_24dp))); + + mItems.add(new OverallItem(R.id.nav_viewpager, resources.getString(R.string.viewpager)) + .withDescription(resources.getString(R.string.viewpager_description)) + .withIcon(resources.getDrawable(R.drawable.ic_view_carousel_grey600_24dp))); } /* @@ -203,6 +212,21 @@ public void createHeadersSectionsDatabase(int size, int headers) { } } + /* + * List of Holder Items and Header. Only Holder Simple Items will be + * added to the list. IHolder items hold the model data inside. + */ + public void createHolderSectionsDatabase(int size, int headers) { + databaseType = DatabaseType.MODEL_HOLDERS; + HeaderHolder header = null; + mItems.clear(); + int lastHeaderId = 0; + for (int i = 0; i < size; i++) { + header = i % Math.round(size / headers) == 0 ? newHeaderHolder(++lastHeaderId) : header; + mItems.add(newItemHolder(i + 1, header)); + } + } + /* * List of Expandable items (headers/sections) with SubItems with Header attached. */ @@ -387,6 +411,19 @@ public static StaggeredItem newStaggeredItem(int i, StaggeredHeaderItem header) return new StaggeredItem(i, header); } + private HeaderHolder newHeaderHolder(int i) { + HeaderModel model = new HeaderModel("H" + i); + model.setTitle("Header " + i); + return new HeaderHolder(model); + } + + private ItemHolder newItemHolder(int i, HeaderHolder header) { + ItemModel model = new ItemModel("I" + i); + model.setTitle("Holder Item " + i); + model.setSubtitle("Subtitle " + i); + return new ItemHolder(model, header); + } + /*-----------------------*/ /* MAIN DATABASE METHODS */ /*-----------------------*/ @@ -424,7 +461,7 @@ public void addAll(List newItems) { mItems.addAll(newItems); } - public void addItem(int position, AbstractModelItem item) { + public void addItem(int position, AbstractItem item) { if (position < mItems.size()) mItems.add(position, item); else @@ -443,7 +480,7 @@ public void addItem(AbstractFlexibleItem item) { public void addSubItem(int position, IExpandable parent, SubItem subItem) { //This split is for my examples if (parent instanceof ExpandableItem) - ((ExpandableItem) parent).removeSubItem(subItem); + ((ExpandableItem) parent).addSubItem(subItem); else if (parent instanceof ExpandableHeaderItem) ((ExpandableHeaderItem) parent).addSubItem(subItem); } @@ -524,6 +561,19 @@ public void splitItem(StaggeredItem mainItem, StaggeredItem itemToSplit) { Collections.sort(mItems, new ItemComparatorById()); } + /** + * This demonstrates that new content of existing items are really rebound and + * notified with CHANGE Payload in the Adapter list when refreshed. + */ + public void updateNewItems() { + for (IFlexible iFlexible : mItems) { + if (iFlexible instanceof AbstractItem) { + AbstractItem item = (AbstractItem) iFlexible; + item.increaseUpdates(); + } + } + } + /** * A simple item comparator by Id. */ diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/showcase/ExpandableHeaderItemExample.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/showcase/ExpandableHeaderItemExample.java deleted file mode 100644 index ae62c3c5..00000000 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/showcase/ExpandableHeaderItemExample.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2016 Davide Steduto - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package eu.davidea.samples.flexibleadapter.showcase; - -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import java.io.Serializable; -import java.util.List; - -import eu.davidea.samples.flexibleadapter.R; -import eu.davidea.samples.flexibleadapter.models.SubItem; -import eu.davidea.flexibleadapter.FlexibleAdapter; -import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem; -import eu.davidea.viewholders.ExpandableViewHolder; - -/** - * This is an another example (not used in the demo) of how a Section with header can also be - * expanded/collapsed.
- * The new object AbstractExpandableHeaderItem is an AbstractExpandableItem that implements IHeader. - * It's important to note that, the ViewHolder must be specified in all <diamond> signature. - */ -public class ExpandableHeaderItemExample - extends AbstractExpandableHeaderItem - implements Serializable { - - private static final long serialVersionUID = -1882711111814491060L; - - private String id; - private String title; - private String subtitle; - - public ExpandableHeaderItemExample(String id) { - super();//Call super to auto-configure the section status as shown, expanded, not selectable - this.id = id; - } - - @Override - public boolean equals(Object inObject) { - if (inObject instanceof ExpandableHeaderItemExample) { - ExpandableHeaderItemExample inItem = (ExpandableHeaderItemExample) inObject; - return this.id.equals(inItem.id); - } - return false; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getSubtitle() { - return subtitle; - } - - public void setSubtitle(String subtitle) { - this.subtitle = subtitle; - } - - @Override - public int getLayoutRes() { - return R.layout.recycler_expandable_header_item; - } - - @Override - public ExpandableHeaderViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { - return new ExpandableHeaderViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); - } - - @Override - public void bindViewHolder(FlexibleAdapter adapter, ExpandableHeaderViewHolder holder, int position, List payloads) { - if (payloads.size() > 0) { - Log.i(this.getClass().getSimpleName(), "ExpandableHeaderItem Payload " + payloads); - } else { - holder.mTitle.setText(getTitle()); - } - setSubtitle(adapter.getCurrentChildren(this).size() + " subItems"); - holder.mSubtitle.setText(getSubtitle()); - } - - /** - * Provide a reference to the views for each data item. - * Complex data labels may need more than one view per item, and - * you provide access to all the views for a data item in a view holder. - */ - public static class ExpandableHeaderViewHolder extends ExpandableViewHolder { - - public TextView mTitle; - public TextView mSubtitle; - - public ExpandableHeaderViewHolder(View view, FlexibleAdapter adapter) { - super(view, adapter); - mTitle = (TextView) view.findViewById(R.id.title); - mSubtitle = (TextView) view.findViewById(R.id.subtitle); - } - - @Override - protected boolean isViewExpandableOnClick() { - return true; - } - } - - @Override - public String toString() { - return "ExpandableHeaderItem[" + super.toString() + "//SubItems" + mSubItems + "]"; - } - -} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/showcase/FlexibleItemHolderExample.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/showcase/FlexibleItemHolderExample.java deleted file mode 100644 index 0661f31d..00000000 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/showcase/FlexibleItemHolderExample.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2016 Davide Steduto - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package eu.davidea.samples.flexibleadapter.showcase; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import java.util.List; - -import eu.davidea.flexibleadapter.FlexibleAdapter; -import eu.davidea.flexibleadapter.items.AbstractSectionableItem; -import eu.davidea.flexibleadapter.items.IFilterable; -import eu.davidea.flexibleadapter.items.IHolder; -import eu.davidea.samples.flexibleadapter.R; -import eu.davidea.samples.flexibleadapter.models.HeaderItem; -import eu.davidea.viewholders.FlexibleViewHolder; - -/** - * In case you need to display the same modelData in multiple RecyclerViews managed by different - * Adapters, you can implement a derived IFlexible item to HOLD your data model object! - * - *

In this way you can separate the memory zones of the flags (enabled, expanded, hidden, selectable, - * draggable, swipeable, etc...) used by an Adapter, to be independent by another Adapter. - * For instance an item can be Shown and Expanded in a RV, while in the other RV can be Hidden or - * Not Expanded!

- */ -public class FlexibleItemHolderExample extends AbstractSectionableItem - implements IFilterable, IHolder { - - /** - * Your complex data model object - */ - Model modelData; - - public FlexibleItemHolderExample(Model modelData, HeaderItem header) { - super(header); - this.modelData = modelData; - } - - @Override - public Model getModel() { - return modelData; - } - - @Override - public boolean equals(Object o) { - //TODO FOR YOU: Implement the equals() also for the custom Model object - if (o instanceof FlexibleItemHolderExample) { - FlexibleItemHolderExample inItem = (FlexibleItemHolderExample) o; - return modelData.equals(inItem.getModel()); - } - return false; - } - - @Override - public int getLayoutRes() { - return R.layout.recycler_expandable_item; - } - - @Override - public ViewHolder createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent) { - return new ViewHolder(inflater.inflate(getLayoutRes(), parent, false), adapter); - } - - @Override - public void bindViewHolder(final FlexibleAdapter adapter, ViewHolder holder, int position, List payloads) { - //TODO FOR YOU: Bind your VH, data comes from your Model object, so modelData always call a getter method - //holder.titleTextView.setText(modelData.getTitle()); - } - - @Override - public boolean filter(String constraint) { - //TODO FOR YOU: Customize your filter logic, filtering is done on Model object data such as getTitle() - //return modelData.getTitle().equals(constraint); - return true; - } - - public static final class ViewHolder extends FlexibleViewHolder { - - public ViewHolder(View view, FlexibleAdapter adapter) { - super(view, adapter); - //TODO FOR YOU: Initialize the Views - } - } - -} \ No newline at end of file diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/HeaderView.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/views/HeaderView.java similarity index 93% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/HeaderView.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/views/HeaderView.java index 4a22866a..72df197e 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/HeaderView.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/views/HeaderView.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter; +package eu.davidea.samples.flexibleadapter.views; import android.annotation.TargetApi; import android.content.Context; @@ -9,6 +9,7 @@ import butterknife.BindView; import butterknife.ButterKnife; +import eu.davidea.samples.flexibleadapter.R; public class HeaderView extends LinearLayout { diff --git a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/ProgressBar.java b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/views/ProgressBar.java similarity index 98% rename from flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/ProgressBar.java rename to flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/views/ProgressBar.java index b728042e..9579fb66 100644 --- a/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/ProgressBar.java +++ b/flexible-adapter-app/src/main/java/eu/davidea/samples/flexibleadapter/views/ProgressBar.java @@ -1,4 +1,4 @@ -package eu.davidea.samples.flexibleadapter; +package eu.davidea.samples.flexibleadapter.views; import android.animation.ValueAnimator; import android.content.Context; @@ -10,6 +10,8 @@ import android.view.View; import android.view.animation.Interpolator; +import eu.davidea.samples.flexibleadapter.R; + /** * Procedurally-drawn version of a horizontal indeterminate progress bar. Draws faster and more * frequently (by making use of the animation timer), requires minimal memory overhead, and allows diff --git a/flexible-adapter-app/src/main/res/drawable/myrect.xml b/flexible-adapter-app/src/main/res/drawable/myrect.xml new file mode 100644 index 00000000..8b3f8800 --- /dev/null +++ b/flexible-adapter-app/src/main/res/drawable/myrect.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/layout/activity_view_pager.xml b/flexible-adapter-app/src/main/res/layout/activity_view_pager.xml new file mode 100644 index 00000000..6d129c7f --- /dev/null +++ b/flexible-adapter-app/src/main/res/layout/activity_view_pager.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/layout/fragment_recycler_view.xml b/flexible-adapter-app/src/main/res/layout/fragment_recycler_view.xml index 88adac21..ecd876ed 100644 --- a/flexible-adapter-app/src/main/res/layout/fragment_recycler_view.xml +++ b/flexible-adapter-app/src/main/res/layout/fragment_recycler_view.xml @@ -10,10 +10,11 @@ android:id="@+id/swipeRefreshLayout" android:layout_width="match_parent" android:layout_height="match_parent" - android:enabled="true"> + android:enabled="false"> - - - + + - - - diff --git a/flexible-adapter-app/src/main/res/layout/fragment_view_pager.xml b/flexible-adapter-app/src/main/res/layout/fragment_view_pager.xml new file mode 100644 index 00000000..b5fbeecd --- /dev/null +++ b/flexible-adapter-app/src/main/res/layout/fragment_view_pager.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/layout/progress_bar.xml b/flexible-adapter-app/src/main/res/layout/progress_bar.xml index 41cf25b7..4c1990d3 100644 --- a/flexible-adapter-app/src/main/res/layout/progress_bar.xml +++ b/flexible-adapter-app/src/main/res/layout/progress_bar.xml @@ -1,5 +1,5 @@ - - + android:layout_gravity="center"/> - \ No newline at end of file + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/layout/recycler_animator_sub_item.xml b/flexible-adapter-app/src/main/res/layout/recycler_animator_sub_item.xml index 54993f80..149ad08a 100644 --- a/flexible-adapter-app/src/main/res/layout/recycler_animator_sub_item.xml +++ b/flexible-adapter-app/src/main/res/layout/recycler_animator_sub_item.xml @@ -6,7 +6,7 @@ android:layout_height="?android:attr/listPreferredItemHeight" android:paddingTop="@dimen/margin_top" android:paddingBottom="@dimen/margin_bottom" - android:background="@drawable/selector_item_grey"> + android:background="?attr/selectableItemBackground"> + android:paddingTop="@dimen/margin_top"> + android:background="#aa7e57c2" + android:elevation="5dp"> + + + + + + + + + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/layout/recycler_holder_item.xml b/flexible-adapter-app/src/main/res/layout/recycler_holder_item.xml new file mode 100644 index 00000000..c5d1a762 --- /dev/null +++ b/flexible-adapter-app/src/main/res/layout/recycler_holder_item.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/layout/recycler_label_item.xml b/flexible-adapter-app/src/main/res/layout/recycler_overall_item.xml similarity index 100% rename from flexible-adapter-app/src/main/res/layout/recycler_label_item.xml rename to flexible-adapter-app/src/main/res/layout/recycler_overall_item.xml diff --git a/flexible-adapter-app/src/main/res/layout/recycler_scrollable_expandable_item.xml b/flexible-adapter-app/src/main/res/layout/recycler_scrollable_expandable_item.xml new file mode 100644 index 00000000..02f6fa81 --- /dev/null +++ b/flexible-adapter-app/src/main/res/layout/recycler_scrollable_expandable_item.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/layout/recycler_scrollable_footer_item.xml b/flexible-adapter-app/src/main/res/layout/recycler_scrollable_footer_item.xml new file mode 100644 index 00000000..718b5708 --- /dev/null +++ b/flexible-adapter-app/src/main/res/layout/recycler_scrollable_footer_item.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/layout/recycler_scrollable_header_item.xml b/flexible-adapter-app/src/main/res/layout/recycler_scrollable_header_item.xml new file mode 100644 index 00000000..f08a76cc --- /dev/null +++ b/flexible-adapter-app/src/main/res/layout/recycler_scrollable_header_item.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/layout/recycler_layout_item.xml b/flexible-adapter-app/src/main/res/layout/recycler_scrollable_layout_item.xml similarity index 93% rename from flexible-adapter-app/src/main/res/layout/recycler_layout_item.xml rename to flexible-adapter-app/src/main/res/layout/recycler_scrollable_layout_item.xml index 76c0eb91..8b4287de 100644 --- a/flexible-adapter-app/src/main/res/layout/recycler_layout_item.xml +++ b/flexible-adapter-app/src/main/res/layout/recycler_scrollable_layout_item.xml @@ -16,9 +16,7 @@ android:layout_marginStart="@dimen/margin_left" android:layout_marginRight="@dimen/margin_right" android:layout_marginEnd="@dimen/margin_right" - android:ellipsize="marquee" - android:marqueeRepeatLimit="1" - android:singleLine="true" + android:maxLines="1" android:textAppearance="@style/TextAppearance.AppCompat.Button" android:textColor="?primaryTextSelector" tools:text="@string/staggered_layout"/> diff --git a/flexible-adapter-app/src/main/res/layout/recycler_scrollable_sub_item.xml b/flexible-adapter-app/src/main/res/layout/recycler_scrollable_sub_item.xml new file mode 100644 index 00000000..2c3d43e2 --- /dev/null +++ b/flexible-adapter-app/src/main/res/layout/recycler_scrollable_sub_item.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/layout/recycler_uls_item.xml b/flexible-adapter-app/src/main/res/layout/recycler_scrollable_uls_item.xml similarity index 93% rename from flexible-adapter-app/src/main/res/layout/recycler_uls_item.xml rename to flexible-adapter-app/src/main/res/layout/recycler_scrollable_uls_item.xml index 3e0a0099..405c8588 100644 --- a/flexible-adapter-app/src/main/res/layout/recycler_uls_item.xml +++ b/flexible-adapter-app/src/main/res/layout/recycler_scrollable_uls_item.xml @@ -30,8 +30,8 @@ android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:layout_centerInParent="true" - android:layout_marginRight="@dimen/margin_small" - android:layout_marginEnd="@dimen/margin_small"> + android:layout_marginRight="@dimen/margin_right" + android:layout_marginEnd="@dimen/margin_right"> + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/layout/recycler_simple_item.xml b/flexible-adapter-app/src/main/res/layout/recycler_simple_item.xml new file mode 100644 index 00000000..09140a05 --- /dev/null +++ b/flexible-adapter-app/src/main/res/layout/recycler_simple_item.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/layout/recycler_staggered_header_item.xml b/flexible-adapter-app/src/main/res/layout/recycler_staggered_header_item.xml index e49383fb..15f49915 100644 --- a/flexible-adapter-app/src/main/res/layout/recycler_staggered_header_item.xml +++ b/flexible-adapter-app/src/main/res/layout/recycler_staggered_header_item.xml @@ -5,28 +5,20 @@ android:layout_width="match_parent" android:layout_height="?android:attr/listPreferredItemHeightSmall" android:layout_centerVertical="true" - android:paddingBottom="2dp" - android:clipToPadding="false"> + android:paddingEnd="@dimen/margin_right" + android:paddingLeft="@dimen/margin_left" + android:paddingRight="@dimen/margin_right" + android:paddingStart="@dimen/margin_left" + android:background="@color/material_color_grey_50" + android:elevation="2dp"> - - - - + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/layout/recycler_vertical_item.xml b/flexible-adapter-app/src/main/res/layout/recycler_vertical_item.xml new file mode 100644 index 00000000..298245be --- /dev/null +++ b/flexible-adapter-app/src/main/res/layout/recycler_vertical_item.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/layout/toolbar_titles.xml b/flexible-adapter-app/src/main/res/layout/toolbar_titles.xml index 81d136ae..98d70544 100644 --- a/flexible-adapter-app/src/main/res/layout/toolbar_titles.xml +++ b/flexible-adapter-app/src/main/res/layout/toolbar_titles.xml @@ -1,5 +1,5 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/menu/activity_entry_drawer.xml b/flexible-adapter-app/src/main/res/menu/activity_entry_drawer.xml index 6b9dd2cb..7f01eb44 100644 --- a/flexible-adapter-app/src/main/res/menu/activity_entry_drawer.xml +++ b/flexible-adapter-app/src/main/res/menu/activity_entry_drawer.xml @@ -42,7 +42,6 @@ android:title="@string/instagram_headers"/> - - + - - + + + + - - + + @@ -84,7 +83,6 @@ android:title="@string/about_title"/> diff --git a/flexible-adapter-app/src/main/res/menu/menu_filter.xml b/flexible-adapter-app/src/main/res/menu/menu_filter.xml index bdb733e7..1f84c5a4 100644 --- a/flexible-adapter-app/src/main/res/menu/menu_filter.xml +++ b/flexible-adapter-app/src/main/res/menu/menu_filter.xml @@ -10,4 +10,9 @@ android:animateLayoutChanges="true" app:actionViewClass="android.support.v7.widget.SearchView"/> + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/menu/menu_holders.xml b/flexible-adapter-app/src/main/res/menu/menu_holders.xml new file mode 100644 index 00000000..ac6bf0f2 --- /dev/null +++ b/flexible-adapter-app/src/main/res/menu/menu_holders.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/menu/menu_view_pager.xml b/flexible-adapter-app/src/main/res/menu/menu_view_pager.xml new file mode 100644 index 00000000..c91d08a2 --- /dev/null +++ b/flexible-adapter-app/src/main/res/menu/menu_view_pager.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/values-v19/styles.xml b/flexible-adapter-app/src/main/res/values-v19/styles.xml index 307a9cf4..cc16e793 100644 --- a/flexible-adapter-app/src/main/res/values-v19/styles.xml +++ b/flexible-adapter-app/src/main/res/values-v19/styles.xml @@ -1,7 +1,7 @@ \ No newline at end of file diff --git a/flexible-adapter-app/src/main/res/values/dimens.xml b/flexible-adapter-app/src/main/res/values/dimens.xml index 2947b9c3..c14164a3 100644 --- a/flexible-adapter-app/src/main/res/values/dimens.xml +++ b/flexible-adapter-app/src/main/res/values/dimens.xml @@ -39,5 +39,6 @@ 40dp 14dp 14dp + 8dp diff --git a/flexible-adapter-app/src/main/res/values/strings.xml b/flexible-adapter-app/src/main/res/values/strings.xml index e9ec5f84..24143a37 100644 --- a/flexible-adapter-app/src/main/res/values/strings.xml +++ b/flexible-adapter-app/src/main/res/values/strings.xml @@ -18,6 +18,7 @@ Grid Layout Staggered Layout %1$s column(s) + Use DiffUtil* Entry Animation Only Scrolling Animation* Reverse scrolling @@ -34,9 +35,21 @@ Mode Single Undo - + Click on Image (or LongClick on Item) to select that Item]]> - This is an example of 2nd view type]]> + This is a scrollable header.]]> + Scrollable Header Item + Scrollable Headers are always displayed at the top of all main items.]]> + Scrollable Expandable Header Item + When expanded, subItems will be added as headers too.]]> + Scrollable Expandable Footer Item + When expanded, subItems will be added as footers too.]]> + Scrollable Footer Item + Scrollable Footers are always displayed at the bottom of all main items.]]> + No more items to load. Refresh to retry. + No more items to load (max reached). + Cancelled by the user. + An error occurred while loading more items. Title @@ -60,6 +73,7 @@ STATUS E Scrolling Animations Item Animators + Complete action using Configure the list ... @@ -79,23 +93,41 @@ text back to normal. This happens systematically when searchText is reduced in l Notify move of filtered items (Nicely animate the moved items) The process is very slow on big list of the order of ~3-5000 -items and higher, due to calculate the correct position for each item to shift. Use with caution! -
The slowness is higher when the searchText is cleared out. Try to disable to improve performance]]>
+items and higher, due to the calculation of the correct position for each item to shift. Use with caution! +
The slowness is higher when the searchText is cleared out. Try to disable to improve performance.]]> Overall + Showcase of the library capabilities + fragments where the +RecyclerView and the Adapter are reinitialized. Each example implements several features of +the library. Common listeners are instead implemented in the MainActivity.]]> Selection modes - IDLE, SINGLE, MULTI + ActionModeHelper + UndoHelper + IDLE, SINGLE, MULTI + ActionModeHelper + UndoHelper + Scrollable Headers + IDLE, SINGLE, MULTI + ActionModeHelper. You can experiment the MULTI selection starting from the SINGLE and from IDLE mode.
+Screen rotation is also supported. ActionMode is implemented in the MainActivity.]]>
Async Filter Big list with Asynchronous filter and refresh + Synchronization Animations + Configuration Item Animators ItemAnimators coherent with ScrollingAnimation + ItemAnimator coherent with ScrollingAnimation + AnimatedViewHolder is implemented.
+Index of section items is also provided to give a cascade effect when expanding/collapsing items.
+ScrollingAnimation gives an unique touch of animations when filling the RecyclerView or scrolling it.]]>
Headers and Sections - Clickable sticky headers for sections + Draggable items with Auto-Linkage + Filter + Clickable sticky headers for sections + Draggable items with Auto-Linkage + Filter + Scrollable Headers and Footers + IHeader)]]> + +Sticky header is a real view and it\'s fully clickable, elevation and transparency are supported too.
+Using filter will disable the sticky header.
+Follow the steps in the Wiki page of the library.]]>
Expandable Sections Sections with [sticky] headers that can expand/collapse + Draggable items + Filter + Scroll Animations @@ -104,28 +136,35 @@ items and higher, due to calculate the correct position for each item to shift. 2 levels of expandable + Selection Coherence + Swipeable & Draggable items + ActionModeHelper + UndoHelper Endless Scrolling - EndlessScrolling + Drag + Swipe-To-Delete w/rear-views + Scroll Animations + ActionModeHelper + UndoHelper + EndlessScrolling + Drag + Swipe-To-Delete w/rear-views + Scroll Animations + ActionModeHelper + UndoHelper + Scrollable Headers and Footers Instagram headers Internet is required.]]> Item Model Holder Check the code!]]> + IHolder item interface]]> + +Use can use IHolder interface when you have to serialize/deserialize the model object coming from +a network or from a different layer. Check the code!]]> Horizontal Layout Items are disposed in vertical - Cards with sticky headers disposed in Staggered Layout, dynamic background, insertion and sorting + Cards with Sticky Headers disposed in Staggered Layout, dynamic background, insertion and sorting + Support for Staggered Layout + calculatePositionFor().
+Example of dynamic background with ripple effect without XML configuration.]]>
View Pager - Example usage in ViewPager layout + Example usage with Sticky Headers in ViewPager layout Dialogs Example usage in Dialogs Flexible Adapter - GitHub page + GitHub page… About %1$s #%2$s Github Profile

Licence
-Copyright © 2015-2016 Davide Steduto. +Copyright © 2015-2017 Davide Steduto.

Licensed under the Apache License, Version 2.0 (the "License"); You may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -152,5 +191,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License. ]]>
+ ViewPagerActivity + Hello World from section: %1$d \ No newline at end of file diff --git a/flexible-adapter-app/src/test/java/eu/davidea/flexibleadapter/FilterTest.java b/flexible-adapter-app/src/test/java/eu/davidea/flexibleadapter/FilterTest.java new file mode 100644 index 00000000..941dbdca --- /dev/null +++ b/flexible-adapter-app/src/test/java/eu/davidea/flexibleadapter/FilterTest.java @@ -0,0 +1,44 @@ +package eu.davidea.flexibleadapter; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.List; + +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; +import eu.davidea.samples.flexibleadapter.services.DatabaseService; + +import static org.junit.Assert.assertEquals; + +/** + * @author Davide Steduto + * @since 18/10/2016 + */ +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 23) +public class FilterTest { + + FlexibleAdapter mAdapter; + List mItems; + + @Before + public void setUp() throws Exception { + DatabaseService.getInstance().createHeadersSectionsDatabase(30, 5); + mItems = DatabaseService.getInstance().getDatabaseList(); + } + + @Test + public void testNoDelayFilter() throws Exception { + mAdapter = new FlexibleAdapter<>(mItems); + mAdapter.showAllHeaders(); + mAdapter.setSearchText("1"); + System.out.println(mAdapter.getItemCount()); + mAdapter.filterItems(DatabaseService.getInstance().getDatabaseList()); + System.out.println(mAdapter.getItemCount()); + assertEquals(16, mAdapter.getItemCount()); + } + +} \ No newline at end of file diff --git a/flexible-adapter-app/src/test/java/eu/davidea/flexibleadapter/HeadersSectionsTest.java b/flexible-adapter-app/src/test/java/eu/davidea/flexibleadapter/HeadersSectionsTest.java index fd41f1ac..aead28ed 100644 --- a/flexible-adapter-app/src/test/java/eu/davidea/flexibleadapter/HeadersSectionsTest.java +++ b/flexible-adapter-app/src/test/java/eu/davidea/flexibleadapter/HeadersSectionsTest.java @@ -3,7 +3,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricGradleTestRunner; +import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import java.util.List; @@ -20,7 +20,7 @@ * @author Davide Steduto * @since 23/06/2016 */ -@RunWith(RobolectricGradleTestRunner.class) +@RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, sdk = 23) public class HeadersSectionsTest { diff --git a/flexible-adapter-app/src/test/java/eu/davidea/flexibleadapter/ItemComparatorTest.java b/flexible-adapter-app/src/test/java/eu/davidea/flexibleadapter/ItemComparatorTest.java index d9912680..705e8978 100644 --- a/flexible-adapter-app/src/test/java/eu/davidea/flexibleadapter/ItemComparatorTest.java +++ b/flexible-adapter-app/src/test/java/eu/davidea/flexibleadapter/ItemComparatorTest.java @@ -3,7 +3,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricGradleTestRunner; +import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import java.util.ArrayList; @@ -19,7 +19,7 @@ import static org.junit.Assert.assertEquals; -@RunWith(RobolectricGradleTestRunner.class) +@RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, sdk = 23) public class ItemComparatorTest { diff --git a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/AnimatorAdapter.java b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/AnimatorAdapter.java index 2c75eadd..63cadcca 100644 --- a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/AnimatorAdapter.java +++ b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/AnimatorAdapter.java @@ -21,12 +21,10 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.support.annotation.CallSuper; import android.support.annotation.FloatRange; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.v4.view.ViewCompat; -import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.util.SparseArray; @@ -42,6 +40,8 @@ import eu.davidea.flexibleadapter.utils.Utils; import eu.davidea.viewholders.FlexibleViewHolder; +import static eu.davidea.flexibleadapter.utils.Utils.getClassName; + /** * This class is responsible to animate items. Bounded items are animated initially and also * when user starts to scroll the list. @@ -54,11 +54,12 @@ * @see SelectableAdapter * @since 10/01/2016 Created *
30/01/2016 Class now extends {@link SelectableAdapter} + *
13/09/2016 {@link #animateView(RecyclerView.ViewHolder, int)} is now automatically called */ @SuppressWarnings({"unused", "WeakerAccess"}) public abstract class AnimatorAdapter extends SelectableAdapter { - protected static final String TAG = AnimatorAdapter.class.getSimpleName(); + private static final String TAG = AnimatorAdapter.class.getSimpleName(); private Interpolator mInterpolator = new LinearInterpolator(); private AnimatorAdapterDataObserver mAnimatorNotifierObserver; @@ -89,7 +90,7 @@ private enum AnimatorEnum { private EnumSet animatorsUsed = EnumSet.noneOf(AnimatorEnum.class); private boolean isReverseEnabled = false, shouldAnimate = false, - onlyEntryAnimation = false, isFastScroll = false, animateFromObserver = false; + onlyEntryAnimation = false, animateFromObserver = false; private long mInitialDelay = 0L, mStepDelay = 100L, @@ -104,10 +105,10 @@ private enum AnimatorEnum { * * @since 5.0.0-b1 */ - public AnimatorAdapter(boolean stableIds) { + AnimatorAdapter(boolean stableIds) { super(); - if (stableIds && DEBUG) Log.i(TAG, "Setting StableIds"); setHasStableIds(stableIds); + if (DEBUG) Log.i("FlexibleAdapter", "Initialized with StableIds=" + stableIds); //Get notified when an item is changed (should skip animation) mAnimatorNotifierObserver = new AnimatorAdapterDataObserver(); @@ -123,12 +124,12 @@ public AnimatorAdapter(boolean stableIds) { * animate items, false to inform that the operation is complete * @since 5.0.0-b6 */ - void setAnimate(boolean animate) { + void setScrollAnimate(boolean animate) { this.animateFromObserver = animate; } /** - * Customize the initial delay for the first item animation. + * Sets the initial delay for the first item animation. *

Default value is {@code 0ms}.

* * @param initialDelay any non negative delay @@ -136,12 +137,13 @@ void setAnimate(boolean animate) { * @since 5.0.0-b1 */ public AnimatorAdapter setAnimationInitialDelay(long initialDelay) { + if (DEBUG) Log.i(TAG, "Set animationInitialDelay=" + initialDelay); mInitialDelay = initialDelay; return this; } /** - * Customize the step delay between an animation and the next to be added to the initial delay. + * Sets the step delay between an animation and the next to be added to the initial delay. *

The delay is added on top of the previous delay.

* Default value is {@code 100ms}. * @@ -150,6 +152,7 @@ public AnimatorAdapter setAnimationInitialDelay(long initialDelay) { * @since 5.0.0-b1 */ public AnimatorAdapter setAnimationDelay(@IntRange(from = 0) long delay) { + if (DEBUG) Log.i(TAG, "Set animationDelay=" + delay); mStepDelay = delay; return this; } @@ -165,12 +168,13 @@ public AnimatorAdapter setAnimationDelay(@IntRange(from = 0) long delay) { * since 5.0.0-b8 */ public AnimatorAdapter setAnimationEntryStep(boolean entryStep) { + if (DEBUG) Log.i(TAG, "Set animationEntryStep=" + entryStep); this.mEntryStep = entryStep; return this; } /** - * Customize the duration of the animation for ALL items. + * Sets the duration of the animation for ALL items. *

Default value is {@code 300ms}.

* * @param duration any positive time @@ -178,41 +182,46 @@ public AnimatorAdapter setAnimationEntryStep(boolean entryStep) { * @since 5.0.0-b1 */ public AnimatorAdapter setAnimationDuration(@IntRange(from = 1) long duration) { + if (DEBUG) Log.i(TAG, "Set animationDuration=" + duration); mDuration = duration; return this; } /** - * Define a custom interpolator for ALL items. + * Sets a custom interpolator for ALL items. *

Default value is {@link LinearInterpolator}.

* * @param interpolator any valid non null interpolator * @return this AnimatorAdapter, so the call can be chained */ public AnimatorAdapter setAnimationInterpolator(@NonNull Interpolator interpolator) { + if (DEBUG) Log.i(TAG, "Set animationInterpolator=" + getClassName(interpolator)); mInterpolator = interpolator; return this; } /** - * Define an initial start animation adapter position. + * Sets an initial start animation adapter position. *

Default value is {@code 0} (1st position).

* * @param start non negative minimum position to start animation. * @since 5.0.0-b1 + * @deprecated Can't be supported anymore due to the new internal condition of non-animations. */ + @Deprecated public AnimatorAdapter setAnimationStartPosition(@IntRange(from = 0) int start) { + if (DEBUG) Log.i(TAG, "Set animationStartPosition=" + start); mLastAnimatedPosition = start; return this; } /** - * Enable/Disable item animation while scrolling and on loading. + * Enables/Disables item animation while scrolling and on loading. *

Enabling scrolling will disable onlyEntryAnimation.
- * Disabling scrolling will disable also reverse scrolling!

+ * Disabling scrolling will disable also reverse scrolling.

* Default value is {@code false}. - * Note: Loading animation can only be performed if the Adapter is initialized - * with some items using the constructor. + *

Note: Loading animation can only be performed if the Adapter is initialized + * with some items using the constructor.

* * @param enabled true to enable item animation, false to disable them all. * @return this AnimatorAdapter, so the call can be chained @@ -221,6 +230,7 @@ public AnimatorAdapter setAnimationStartPosition(@IntRange(from = 0) int start) * @since 5.0.0-b1 */ public AnimatorAdapter setAnimationOnScrolling(boolean enabled) { + if (DEBUG) Log.i(TAG, "Set animationOnScrolling=" + enabled); if (enabled) this.onlyEntryAnimation = false; shouldAnimate = enabled; return this; @@ -231,7 +241,7 @@ public boolean isAnimationOnScrollingEnabled() { } /** - * Enable reverse scrolling animation if AnimationOnScrolling is also enabled! + * Enables reverse scrolling animation if AnimationOnScrolling is also enabled! *

Value is ignored if basic animation on scrolling is disabled.

* Default value is {@code false} (only forward). * @@ -241,6 +251,7 @@ public boolean isAnimationOnScrollingEnabled() { * @since 5.0.0-b1 */ public AnimatorAdapter setAnimationOnReverseScrolling(boolean enabled) { + if (DEBUG) Log.i(TAG, "Set animationOnReverseScrolling=" + enabled); isReverseEnabled = enabled; return this; } @@ -248,11 +259,21 @@ public AnimatorAdapter setAnimationOnReverseScrolling(boolean enabled) { /** * @return true if items are animated also on reverse scrolling, false only forward * @since 5.0.0-b1 + * @deprecated use {@link #isAnimationOnReverseScrollingEnabled()} */ + @Deprecated public boolean isAnimationOnReverseScrolling() { return isReverseEnabled; } + /** + * @return true if items are animated also on reverse scrolling, false only forward + * @since 5.0.0-b1 + */ + public boolean isAnimationOnReverseScrollingEnabled() { + return isReverseEnabled; + } + /** * Performs only entry animation during the initial loading. Stops the animation after * the last visible item in the RecyclerView has been animated. @@ -266,6 +287,7 @@ public boolean isAnimationOnReverseScrolling() { * @since 5.0.0-b8 */ public AnimatorAdapter setOnlyEntryAnimation(boolean enabled) { + if (DEBUG) Log.i(TAG, "Set onlyEntryAnimation=" + enabled); if (enabled) this.shouldAnimate = true; this.onlyEntryAnimation = enabled; return this; @@ -280,34 +302,10 @@ public boolean isOnlyEntryAnimation() { return onlyEntryAnimation; } - /** - * Triggered by the FastScroller when handle is touched - * - * @param scrolling boolean to indicate that the handle is being fast scrolled - * @since 5.0.0-b1 - */ - @Override - public void onFastScrollerStateChange(boolean scrolling) { - super.onFastScrollerStateChange(scrolling); - isFastScroll = scrolling; - } - /*--------------*/ /* MAIN METHODS */ /*--------------*/ - @Override - @CallSuper - public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) { - int position = holder.getAdapterPosition(); -// if (DEBUG) { -// Log.v(TAG, "onViewAttached Holder=" + holder.getClass().getSimpleName() + -// " position=" + position + -// " itemId=" + holder.getItemId()); -// } - animateView(holder, position); - } - /** * Build your custom list of {@link Animator} to apply on the ItemView.
* Write the logic based on the position and/or viewType and/or the item selection. @@ -339,52 +337,83 @@ private void cancelExistingAnimation(final int hashCode) { if (animator != null) animator.end(); } - protected void animateView(final RecyclerView.ViewHolder holder, final int position) { - //FIXME: first completed visible item on rotation gets high delay + /** + * Checks if at the provided position, the item is a Header or Footer + * + * @param position the position to check + * @return true if is a scrollable item + */ + public abstract boolean isScrollableHeaderOrFooter(int position); -// if (DEBUG) + /** + * Performs checks to scroll animate the itemView and in case, it animates the view. + *

Note: If you have to change at runtime the LayoutManager and add + * Scrollable Headers too, consider to add them in post, using a {@code delay >= 0}, + * otherwise scroll animations on all items will not start correctly.

+ * + * @param holder the ViewHolder just bound + * @param position the current item position + */ + @SuppressWarnings("ConstantConditions") + protected final void animateView(final RecyclerView.ViewHolder holder, final int position) { + if (mRecyclerView == null) return; + + // Use always the max child count reached + if (mMaxChildViews < mRecyclerView.getChildCount()) { + mMaxChildViews = mRecyclerView.getChildCount(); + } + // Animate only during initial loading? + if (onlyEntryAnimation && mLastAnimatedPosition >= mMaxChildViews) { + shouldAnimate = false; + } + int lastVisiblePosition = Utils.findLastVisibleItemPosition(mRecyclerView.getLayoutManager()); +// if (DEBUG) { // Log.v(TAG, "shouldAnimate=" + shouldAnimate // + " isFastScroll=" + isFastScroll // + " isNotified=" + mAnimatorNotifierObserver.isPositionNotified() // + " isReverseEnabled=" + isReverseEnabled // + " mLastAnimatedPosition=" + mLastAnimatedPosition -// + (!isReverseEnabled ? " Pos>AniPos=" + (position > mLastAnimatedPosition) : "") +// + (!isReverseEnabled ? " Pos>LasVisPos=" + (position > lastVisiblePosition) : "") +// + " mMaxChildViews=" + mMaxChildViews // ); - +// } if (holder instanceof FlexibleViewHolder && shouldAnimate && !isFastScroll && !mAnimatorNotifierObserver.isPositionNotified() && - (isReverseEnabled || position > mLastAnimatedPosition || (position == 0 && mRecyclerView.getChildCount() == 0)) ) { + (position > lastVisiblePosition || isReverseEnabled || isScrollableHeaderOrFooter(position) || (position == 0 && mMaxChildViews == 0))) { - //Cancel animation is necessary when fling + // Cancel animation is necessary when fling int hashCode = holder.itemView.hashCode(); cancelExistingAnimation(hashCode); - //User animators + // User animators List animators = new ArrayList<>(); FlexibleViewHolder flexibleViewHolder = (FlexibleViewHolder) holder; - flexibleViewHolder.scrollAnimators(animators, position, position > mLastAnimatedPosition); + flexibleViewHolder.scrollAnimators(animators, position, position >= lastVisiblePosition); - //Execute the animations together + // Execute the animations together AnimatorSet set = new AnimatorSet(); set.playTogether(animators); set.setInterpolator(mInterpolator); - set.setDuration(mDuration); + // Single view duration + long duration = 0L; + for (Animator animator : animators) { + if (animator.getDuration() != mDuration) { + duration = animator.getDuration(); + } + } + //Log.v(TAG, "duration=" + duration); + set.setDuration(duration > 0 ? duration : mDuration); set.addListener(new HelperAnimatorListener(hashCode)); if (mEntryStep) { - //Stop stepDelay when screen is filled - set.setStartDelay(calculateAnimationDelay2(position)); + // Stop stepDelay when screen is filled + set.setStartDelay(calculateAnimationDelay(position)); } set.start(); mAnimators.put(hashCode, set); - if (DEBUG) Log.d(TAG, "Started Animation on position " + position); - - //Animate only during initial loading? - if (onlyEntryAnimation && position >= mMaxChildViews) { - shouldAnimate = false; - } + if (DEBUG) Log.v(TAG, "animateView Scroll animation on position " + position); } - mAnimatorNotifierObserver.clearNotified(); + // Update last animated position mLastAnimatedPosition = position; } @@ -418,7 +447,7 @@ public final void animateView(final View itemView, int position) { //Add Alpha animator ViewCompat.setAlpha(itemView, 0); animators.add(ObjectAnimator.ofFloat(itemView, "alpha", 0f, 1f)); - if (DEBUG) Log.d(TAG, "Started Deprecated Animation on position " + position); + Log.w(TAG, "Started Deprecated Animation on position " + position); //Execute the animations AnimatorSet set = new AnimatorSet(); @@ -427,8 +456,7 @@ public final void animateView(final View itemView, int position) { set.setDuration(mDuration); set.addListener(new HelperAnimatorListener(itemView.hashCode())); if (mEntryStep) { - //set.setStartDelay(calculateAnimationDelay1(position)); - set.setStartDelay(calculateAnimationDelay2(position)); + set.setStartDelay(calculateAnimationDelay(position)); } set.start(); mAnimators.put(itemView.hashCode(), set); @@ -444,87 +472,55 @@ public final void animateView(final View itemView, int position) { } /** - * Solution 1. - * Reset stepDelay. + * @param position the position just bound + * @return the delay in milliseconds after which, the animation for next ItemView should start. */ - private long calculateAnimationDelay1(int position) { + private long calculateAnimationDelay(int position) { + long delay; int firstVisiblePosition = Utils.findFirstCompletelyVisibleItemPosition(mRecyclerView.getLayoutManager()); int lastVisiblePosition = Utils.findLastCompletelyVisibleItemPosition(mRecyclerView.getLayoutManager()); - //Use always the max child count reached - if (mMaxChildViews < mRecyclerView.getChildCount()) - mMaxChildViews = mRecyclerView.getChildCount(); + // Fix for high delay on the first visible item on rotation + if (firstVisiblePosition < 0 && position >= 0) + firstVisiblePosition = position - 1; - if (mLastAnimatedPosition > lastVisiblePosition) - lastVisiblePosition = mLastAnimatedPosition; + // Last visible position is the last animated when initially loading + if (position - 1 > lastVisiblePosition) + lastVisiblePosition = position - 1; int visibleItems = lastVisiblePosition - firstVisiblePosition; - -// if (DEBUG) Log.v(TAG, "Position=" + position + -// " FirstVisible=" + firstVisiblePosition + -// " LastVisible=" + lastVisiblePosition + -// " LastAnimated=" + mLastAnimatedPosition + -// " VisibleItems=" + visibleItems + -// " ChildCount=" + mRecyclerView.getChildCount()); - - //Stop stepDelay when screen is filled - if (mLastAnimatedPosition > visibleItems || //Normal Forward scrolling - (firstVisiblePosition > 1 && firstVisiblePosition <= mMaxChildViews)) { //Reverse scrolling - if (DEBUG) Log.v(TAG, "Reset AnimationDelay on position " + position); - return 0L; - } - - return mInitialDelay += mStepDelay; - } - - /** - * Solution 2. - * Returns the delay in milliseconds after which, the animation for next ItemView should start. - */ - private long calculateAnimationDelay2(int position) { - long delay; - int firstVisiblePosition = Utils.findFirstCompletelyVisibleItemPosition(mRecyclerView.getLayoutManager()); - int lastVisiblePosition = Utils.findLastCompletelyVisibleItemPosition(mRecyclerView.getLayoutManager()); - - if (mLastAnimatedPosition > lastVisiblePosition) - lastVisiblePosition = mLastAnimatedPosition; - - int numberOfItemsOnScreen = lastVisiblePosition - firstVisiblePosition; int numberOfAnimatedItems = position - 1; - //Save max child count reached - if (mMaxChildViews < mRecyclerView.getChildCount()) - mMaxChildViews = mRecyclerView.getChildCount(); - - if (numberOfItemsOnScreen == 0 || numberOfItemsOnScreen < numberOfAnimatedItems || //Normal Forward scrolling after max itemOnScreen is reached + if (mMaxChildViews == 0 || visibleItems < numberOfAnimatedItems || //Normal Forward scrolling after max itemOnScreen is reached (firstVisiblePosition > 1 && firstVisiblePosition <= mMaxChildViews) || //Reverse scrolling (position > mMaxChildViews && firstVisiblePosition == -1 && mRecyclerView.getChildCount() == 0)) { //Reverse scrolling and click on FastScroller - //Base delay is step delay + // Base delay is step delay delay = mStepDelay; - if (numberOfItemsOnScreen <= 1) { - //When RecyclerView is initially loading no items are present - //Use InitialDelay only for the first item + if (visibleItems <= 1) { + // When RecyclerView is initially loading no items are present + // Use InitialDelay only for the first item delay += mInitialDelay; } else { - //Reset InitialDelay only when first item is already animated + // Reset InitialDelay only when first item is already animated mInitialDelay = 0L; } - if (mRecyclerView.getLayoutManager() instanceof GridLayoutManager) { - int numColumns = ((GridLayoutManager) mRecyclerView.getLayoutManager()).getSpanCount(); + int numColumns = Utils.getSpanCount(mRecyclerView.getLayoutManager()); + if (numColumns > 1) { delay = mInitialDelay + mStepDelay * (position % numColumns); } - } else {//forward scrolling before max itemOnScreen is reached + } else { //forward scrolling before max itemOnScreen is reached delay = mInitialDelay + (position * mStepDelay); } // if (DEBUG) Log.v(TAG, "Delay[" + position + "]=" + delay + // " FirstVisible=" + firstVisiblePosition + // " LastVisible=" + lastVisiblePosition + -// " LastAnimated=" + mLastAnimatedPosition + -// " VisibleItems=" + numberOfItemsOnScreen + -// " ChildCount=" + mRecyclerView.getChildCount()); +// " LastAnimated=" + numberOfAnimatedItems + +// " VisibleItems=" + visibleItems + +// " ChildCount=" + mRecyclerView.getChildCount() + +// " MaxChildCount=" + mMaxChildViews); return delay; } @@ -671,13 +667,13 @@ public void addScaleInAnimator( /** * Observer Class responsible to skip animation when items are notified to avoid * double animation with {@link android.support.v7.widget.RecyclerView.ItemAnimator}. - *

Also, some items at the edge, are rebounded by Android and should not be animated.

+ *

Also, some items at the edge, are rebound by Android and should not be animated.

*/ private class AnimatorAdapterDataObserver extends RecyclerView.AdapterDataObserver { private boolean notified; private Handler mAnimatorHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { public boolean handleMessage(Message message) { - if (DEBUG) Log.v(TAG, "Clear notified for binding Animations"); + if (DEBUG) Log.v(TAG, "Clear notified for scrolling Animations"); notified = false; return true; } @@ -696,6 +692,8 @@ public void clearNotified() { private void markNotified() { notified = !animateFromObserver; +// if (DEBUG) +// Log.v(TAG, "animateFromObserver=" + animateFromObserver + " notified=" + notified); } @Override @@ -737,7 +735,6 @@ private class HelperAnimatorListener implements Animator.AnimatorListener { @Override public void onAnimationStart(Animator animation) { - } @Override diff --git a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/FlexibleAdapter.java b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/FlexibleAdapter.java index 63022538..77a798df 100644 --- a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/FlexibleAdapter.java +++ b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/FlexibleAdapter.java @@ -22,10 +22,11 @@ import android.os.Looper; import android.os.Message; import android.support.annotation.CallSuper; +import android.support.annotation.FloatRange; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v4.view.ViewCompat; +import android.support.v7.util.DiffUtil; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; import android.util.Log; @@ -43,6 +44,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import eu.davidea.flexibleadapter.common.SmoothScrollGridLayoutManager; @@ -58,20 +60,21 @@ import eu.davidea.viewholders.ExpandableViewHolder; import eu.davidea.viewholders.FlexibleViewHolder; +import static eu.davidea.flexibleadapter.utils.Utils.getClassName; + /** - * This class is backed by an ArrayList of arbitrary objects of T, where T is - * your Model object containing the data, with version 5.0.0 it must implement {@link IFlexible} - * interface. Read Wiki page on - * Github for more details. - *

This class provides a set of standard methods to handle changes on the data set such as - * filtering, adding, removing, moving and animating an item.

- * With version 5.0.0, this Adapter supports a set of standard methods for Headers/Sections to - * expand and collapse an Expandable item, to Drag&Drop and Swipe any item. - *

NOTE: This Adapter supports multi level of Expandable, but do not enable Drag&Drop. - * Something might not work as expected, so better to change approach in favor of a clearer - * design/layout: Open the sub list in a new Activity/Fragment... - *
Instead, this extra level of expansion is useful in situations where information is in - * read only mode or with action buttons.

+ * This Adapter is backed by an ArrayList of arbitrary objects of class T, where T + * is your adapter/model object containing the data of a single item. This Adapter simplifies the + * development by providing a set of standard methods to handle changes on the data set such as: + * selecting, filtering, adding, removing, moving and animating an item. + *

+ * With version 5.0.0, T item must implement {@link IFlexible} item interface as base item + * for all view types and new methods have been added in order to: + *

    + *
  • handle Headers/Sections with {@link IHeader} and {@link ISectionable} items;
  • + *
  • expand and collapse {@link IExpandable} items;
  • + *
  • drag&drop and swipe any items.
  • + *
* * @author Davide Steduto * @see AnimatorAdapter @@ -88,7 +91,8 @@ *
10/02/2016 The class is not abstract anymore, it is ready to be used *
20/02/2016 Sticky headers *
22/04/2016 Endless Scrolling - *
09/07/2016 FilterAsyncTask (performance on big list) + *
13/07/2016 Update and Filter operations are executed asynchronously (high performance on big list) + *
25/11/2016 Scrollable Headers and Footers; Reviewed EndlessScroll */ @SuppressWarnings({"Range", "unused", "unchecked", "ConstantConditions", "SuspiciousMethodCalls", "WeakerAccess"}) public class FlexibleAdapter @@ -99,73 +103,41 @@ public class FlexibleAdapter private static final String EXTRA_PARENT = TAG + "_parentSelected"; private static final String EXTRA_CHILD = TAG + "_childSelected"; private static final String EXTRA_HEADERS = TAG + "_headersShown"; + private static final String EXTRA_STICKY = TAG + "_stickyHeaders"; private static final String EXTRA_LEVEL = TAG + "_selectedLevel"; private static final String EXTRA_SEARCH = TAG + "_searchText"; - /** - * Handler operations - */ - private static final int - UPDATE = 0, FILTER = 1, CONFIRM_DELETE = 2, - LOAD_MORE_COMPLETE = 8, LOAD_MORE_RESET = 9; - - /** - * The main container for ALL items. - */ + /* The main container for ALL items */ private List mItems, mTempItems; - /** - * HashSet and AsyncTask objects, will increase performance in big list - */ - private Set hashItems; - private List notifications; + /* HashSet, AsyncTask and DiffUtil objects, will increase performance in big list */ + private Set mHashItems; + private List mNotifications; private FilterAsyncTask mFilterAsyncTask; + private long start, time; + private boolean useDiffUtil = false; + private DiffUtil.DiffResult diffResult; + private DiffUtilCallback diffUtilCallback; - /** - * Handler for delayed actions. - *

You can use and override this Handler, but you must keep the "What" by calling super(): - *
0 = updateDataSet. - *
1 = filterItems, optionally delayed. - *
2 = deleteConfirmed when Undo timeout is over. - *
8 = remove the progress item from the list, optionally delayed. - *
9 = reset flag to load more items, delayed.

- * Note: numbers 0-9 are reserved for the Adapter, use others. - */ - protected Handler mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { - public boolean handleMessage(Message message) { - switch (message.what) { - case UPDATE: //updateDataSet OR - case FILTER: //filterItems - if (mFilterAsyncTask != null) mFilterAsyncTask.cancel(true); - mFilterAsyncTask = new FilterAsyncTask(message.what, (List) message.obj); - mFilterAsyncTask.execute(); - return true; - case CONFIRM_DELETE: //confirm delete - OnDeleteCompleteListener listener = (OnDeleteCompleteListener) message.obj; - if (listener != null) listener.onDeleteConfirmed(); - emptyBin(); - return true; - case LOAD_MORE_COMPLETE: //onLoadMore remove progress item - deleteProgressItem(); - return true; - case LOAD_MORE_RESET: //onLoadMore reset - resetOnLoadMore(); - return true; - } - return false; - } - }); + /* Handler for delayed actions */ + protected final int UPDATE = 0, FILTER = 1, CONFIRM_DELETE = 2, LOAD_MORE_COMPLETE = 8; + protected Handler mHandler = new Handler(Looper.getMainLooper(), new HandlerCallback()); - /* Used to save deleted items and to recover them (Undo) */ + /* Deleted items and RestoreList (Undo) */ public static final long UNDO_TIMEOUT = 5000L; private List mRestoreList; private boolean restoreSelection = false, multiRange = false, unlinkOnRemoveHeader = false, removeOrphanHeaders = false, permanentDelete = true, adjustSelected = true; - /* Header/Section items */ + /* Scrollable Headers/Footers items */ + private List mScrollableHeaders, mScrollableFooters; + + /* Section items (with sticky headers) */ private List mOrphanHeaders; - private boolean headersShown = false, headersSticky = false, recursive = false; + private boolean headersShown = false, recursive = false; + private float mStickyElevation; private StickyHeaderHelper mStickyHeaderHelper; + private ViewGroup mStickyContainer; /* ViewTypes */ protected LayoutInflater mInflater; @@ -178,21 +150,21 @@ public boolean handleMessage(Message message) { private Set mExpandedFilterFlags; private boolean notifyChangeOfUnfilteredItems = false, filtering = false, notifyMoveOfFilteredItems = false; - private static int mAnimateToLimit = 600; + private static int ANIMATE_TO_LIMIT = 700; + private int mAnimateToLimit = ANIMATE_TO_LIMIT; /* Expandable flags */ - private int minCollapsibleLevel = 0, selectedLevel = -1; + private int mMinCollapsibleLevel = 0, mSelectedLevel = -1; private boolean scrollOnExpand = false, collapseOnExpand = false, childSelected = false, parentSelected = false; /* Drag&Drop and Swipe helpers */ - private boolean handleDragEnabled = false; private ItemTouchHelperCallback mItemTouchHelperCallback; private ItemTouchHelper mItemTouchHelper; /* EndlessScroll */ - private int mEndlessScrollThreshold = 1; - private boolean mLoading = false; + private int mEndlessScrollThreshold = 1, mEndlessTargetCount = 0, mEndlessPageSize = 0; + private boolean endlessLoading = false, endlessScrollEnabled = false; private T mProgressItem; /* Listeners */ @@ -212,6 +184,8 @@ public boolean handleMessage(Message message) { * Simple Constructor with NO listeners! * * @param items items to display. + * @see #FlexibleAdapter(List, Object) + * @see #FlexibleAdapter(List, Object, boolean) * @since 4.2.0 */ public FlexibleAdapter(@Nullable List items) { @@ -220,21 +194,24 @@ public FlexibleAdapter(@Nullable List items) { /** * Main Constructor with all managed listeners for ViewHolder and the Adapter itself. - *

The listener must be a single instance of a class, usually Activity or Fragment, - * where you can implement how to handle the different events.

- * Any write operation performed on the items list is synchronized. - *

PASS ALWAYS A COPY OF THE ORIGINAL LIST: new ArrayList<T>(originalList);

+ *

The listener must be a single instance of a class, usually Activity or + * Fragment, where you can implement how to handle the different events.

+ *

PROVIDE ALWAYS A COPY OF THE ORIGINAL LIST: + * {@code new ArrayList(originalList);}

* * @param items items to display * @param listeners can be an instance of: *
    - *
  • {@link OnUpdateListener} *
  • {@link OnItemClickListener} *
  • {@link OnItemLongClickListener} *
  • {@link OnItemMoveListener} *
  • {@link OnItemSwipeListener} *
  • {@link OnStickyHeaderChangeListener} + *
  • {@link OnUpdateListener} *
+ * @see #FlexibleAdapter(List) + * @see #FlexibleAdapter(List, Object, boolean) + * @see #addListener(Object) * @since 5.0.0-b1 */ public FlexibleAdapter(@Nullable List items, @Nullable Object listeners) { @@ -244,25 +221,30 @@ public FlexibleAdapter(@Nullable List items, @Nullable Object listeners) { /** * Same as {@link #FlexibleAdapter(List, Object)} with possibility to set stableIds. *

Note: Setting true allows the RecyclerView to rebind only items really changed - * after a refresh or after swapping Adapter. This increase performance, you loose scrolling - * animations.

- * Set {@code true} if items implements {@code hashcode()} and have stable ids. The method + * after a refresh or after swapping Adapter. This increase performance, but you loose + * scrolling animations.

+ * Set {@code true} only if items implement {@code hashcode()} and have stable ids. The method * {@link #setHasStableIds(boolean)} will be called. * - * @param stableIds set {@code true} if items implements {@code hashcode()} and have stable ids. + * @param stableIds set {@code true} if item implements {@code hashcode()} and have stable ids. + * @see #FlexibleAdapter(List) + * @see #FlexibleAdapter(List, Object) + * @see #addListener(Object) * @since 5.0.0-b8 */ public FlexibleAdapter(@Nullable List items, @Nullable Object listeners, boolean stableIds) { super(stableIds); if (items == null) mItems = new ArrayList<>(); else mItems = items; + mScrollableHeaders = new ArrayList<>(); + mScrollableFooters = new ArrayList<>(); mRestoreList = new ArrayList<>(); mOrphanHeaders = new ArrayList<>(); - //Create listeners instances - initializeListeners(listeners); + // Create listeners instances + addListener(listeners); - //Get notified when items are inserted or removed (it adjusts selected positions) + // Get notified when items are inserted or removed (it adjusts selected positions) registerAdapterDataObserver(new AdapterDataObserver()); } @@ -270,31 +252,59 @@ public FlexibleAdapter(@Nullable List items, @Nullable Object listeners, bool * Initializes the listener(s) of this Adapter. *

This method is automatically called from the Constructor.

* - * @param listeners the object(s) instance(s) of any listener + * @param listener the object(s) instance(s) of any listener + * @return this Adapter, so the call can be chained + * @deprecated Use {@link #addListener(Object)} + */ + @Deprecated + public FlexibleAdapter initializeListeners(@Nullable Object listener) { + return addListener(listener); + } + + /** + * Initializes the listener(s) of this Adapter. + *

This method is automatically called from the Constructor.

+ * + * @param listener the object(s) instance(s) of any listener * @return this Adapter, so the call can be chained * @since 5.0.0-b6 */ - public FlexibleAdapter initializeListeners(@Nullable Object listeners) { - if (listeners instanceof OnUpdateListener) { - mUpdateListener = (OnUpdateListener) listeners; - mUpdateListener.onUpdateEmptyView(getItemCount()); - } - if (listeners instanceof OnItemClickListener) - mItemClickListener = (OnItemClickListener) listeners; - if (listeners instanceof OnItemLongClickListener) - mItemLongClickListener = (OnItemLongClickListener) listeners; - if (listeners instanceof OnItemMoveListener) - mItemMoveListener = (OnItemMoveListener) listeners; - if (listeners instanceof OnItemSwipeListener) - mItemSwipeListener = (OnItemSwipeListener) listeners; - if (listeners instanceof OnStickyHeaderChangeListener) - mStickyHeaderChangeListener = (OnStickyHeaderChangeListener) listeners; + @CallSuper + public FlexibleAdapter addListener(@Nullable Object listener) { + if (DEBUG && listener != null) { + Log.i(TAG, "Adding listener class " + getClassName(listener) + " as:"); + } + if (listener instanceof OnItemClickListener) { + if (DEBUG) Log.i(TAG, "- OnItemClickListener"); + mItemClickListener = (OnItemClickListener) listener; + } + if (listener instanceof OnItemLongClickListener) { + if (DEBUG) Log.i(TAG, "- OnItemLongClickListener"); + mItemLongClickListener = (OnItemLongClickListener) listener; + } + if (listener instanceof OnItemMoveListener) { + if (DEBUG) Log.i(TAG, "- OnItemMoveListener"); + mItemMoveListener = (OnItemMoveListener) listener; + } + if (listener instanceof OnItemSwipeListener) { + if (DEBUG) Log.i(TAG, "- OnItemSwipeListener"); + mItemSwipeListener = (OnItemSwipeListener) listener; + } + if (listener instanceof OnStickyHeaderChangeListener) { + if (DEBUG) Log.i(TAG, "- OnStickyHeaderChangeListener"); + mStickyHeaderChangeListener = (OnStickyHeaderChangeListener) listener; + } + if (listener instanceof OnUpdateListener) { + if (DEBUG) Log.i(TAG, "- OnUpdateListener"); + mUpdateListener = (OnUpdateListener) listener; + mUpdateListener.onUpdateEmptyView(getMainItemCount()); + } return this; } /** * {@inheritDoc} - *

Attaches the StickyHeaderHelper from the RecyclerView when necessary

+ *

Attaches the StickyHeaderHelper from the RecyclerView if necessary.

* * @since 5.0.0-b6 */ @@ -315,7 +325,7 @@ public void onAttachedToRecyclerView(RecyclerView recyclerView) { @Override public void onDetachedFromRecyclerView(RecyclerView recyclerView) { if (mStickyHeaderHelper != null) { - mStickyHeaderHelper.detachFromRecyclerView(mRecyclerView); + mStickyHeaderHelper.detachFromRecyclerView(); mStickyHeaderHelper = null; } super.onDetachedFromRecyclerView(recyclerView); @@ -325,17 +335,16 @@ public void onDetachedFromRecyclerView(RecyclerView recyclerView) { * Maps and expands items that are initially configured to be shown as expanded. *

This method should be called during the creation of the Activity/Fragment, useful also * after a screen rotation. - *
It is also called after DataSet is updated.

- * Note: Only items at level 0 are automatically expanded, ignored all sub-levels. + *
It is also called after the data set is updated.

* * @return this Adapter, so the call can be chained * @since 5.0.0-b6 */ public FlexibleAdapter expandItemsAtStartUp() { int position = 0; - setAnimate(true); + setScrollAnimate(true); multiRange = true; - while (position < mItems.size()) { + while (position < getItemCount()) { T item = getItem(position); if (isExpanded(item)) { expand(position, false, true); @@ -345,7 +354,7 @@ public FlexibleAdapter expandItemsAtStartUp() { position++; } multiRange = false; - setAnimate(false); + setScrollAnimate(false); return this; } @@ -355,6 +364,7 @@ public FlexibleAdapter expandItemsAtStartUp() { /** * Checks if the current item has the property {@code enabled = true}. + *

When an item is disabled, user cannot interact with it.

* * @param position the current position of the item to check * @return true if the item property enabled is set true, false otherwise @@ -379,33 +389,33 @@ public boolean isSelectable(int position) { /** * {@inheritDoc} * - * @param position Position of the item to toggle the selection status for. + * @param position position of the item to toggle the selection status for. * @since 5.0.0-b1 */ + //TODO: Review the logic of selection coherence @Override public void toggleSelection(@IntRange(from = 0) int position) { T item = getItem(position); - //Allow selection only for selectable items + // Allow selection only for selectable items if (item != null && item.isSelectable()) { IExpandable parent = getExpandableOf(item); boolean hasParent = parent != null; if ((isExpandable(item) || !hasParent) && !childSelected) { - //Allow selection of Parent if no Child has been previously selected + // Allow selection of Parent if no Child has been previously selected parentSelected = true; - if (hasParent) selectedLevel = parent.getExpansionLevel(); + if (hasParent) mSelectedLevel = parent.getExpansionLevel(); super.toggleSelection(position); - } else if (!parentSelected && hasParent && parent.getExpansionLevel() + 1 == selectedLevel - || selectedLevel == -1) { - //Allow selection of Child of same level and if no Parent has been previously selected + } else if (!parentSelected && hasParent && parent.getExpansionLevel() + 1 == mSelectedLevel + || mSelectedLevel == -1) { + // Allow selection of Child of same level and if no Parent has been previously selected childSelected = true; - selectedLevel = parent.getExpansionLevel() + 1; + mSelectedLevel = parent.getExpansionLevel() + 1; super.toggleSelection(position); } } - - //Reset flags if necessary, just to be sure + // Reset flags if necessary, just to be sure if (getSelectedItemCount() == 0) { - selectedLevel = -1; + mSelectedLevel = -1; parentSelected = childSelected = false; } } @@ -416,21 +426,21 @@ public void toggleSelection(@IntRange(from = 0) int position) { *

Examples: *
- if user initially selects an expandable of type A, then only expandable items of * type A will be selected. - *
- if user initially selects a non-expandable of type B, then only items of Type B + *
- if user initially selects a non-expandable of type B, then only items of type B * will be selected. *
- The developer can override this behaviour by passing a list of viewTypes for which * he wants to force the selection.

* - * @param viewTypes All the desired viewTypes to be selected, pass nothing to automatically - * select all the viewTypes of the first item user selected + * @param viewTypes All the desired viewTypes to be selected, providing no view types, will + * automatically select all the viewTypes of the first item user has selected * @since 5.0.0-b1 */ @Override public void selectAll(Integer... viewTypes) { if (getSelectedItemCount() > 0 && viewTypes.length == 0) { - super.selectAll(getItemViewType(getSelectedPositions().get(0)));//Priority on the first item + super.selectAll(getItemViewType(getSelectedPositions().get(0))); //Priority on the first item } else { - super.selectAll(viewTypes);//Force the selection for the viewTypes passed + super.selectAll(viewTypes); //Force the selection for the viewTypes passed } } @@ -467,8 +477,10 @@ public boolean isAnyChildSelected() { /*--------------*/ /** - * Convenience method of {@link #updateDataSet(List, boolean)}. - *

In this case changes will NOT be animated: {@link #notifyDataSetChanged()} will be invoked.

+ * Convenience method of {@link #updateDataSet(List, boolean)} (You should read the comments + * of this method). + *

In this call, changes will NOT be animated: Warning! + * {@link #notifyDataSetChanged()} will be invoked.

* * @param items the new data set * @see #updateDataSet(List, boolean) @@ -480,47 +492,59 @@ public void updateDataSet(List items) { } /** - * This method will refresh the entire DataSet content. - *

Optionally all changes can be animated, limited by the value previously set with - * {@link #setAnimateToLimit(int)} to improve performance on big list.

- * Pass {@code animate = false} to directly invoke {@link #notifyDataSetChanged()} - * without any animations. - *

Note: This methods calls {@link #expandItemsAtStartUp()} and - * {@link #showAllHeaders()} if headers are shown.

+ * This method will refresh the entire data set content. Optionally, all changes can be + * animated, limited by the value previously set with {@link #setAnimateToLimit(int)} + * to improve performance on very big list. Should provide {@code animate=false} to + * directly invoke {@link #notifyDataSetChanged()} without any animations, if {@code stableIds} + * is not set! + *

Note: + *

  • Scrollable Headers and Footers (if existent) will be restored in this call.
  • + *
  • I strongly recommend to implement {@link #hashCode()} to all adapter items along with + * {@link #equals(Object)}: This Adapter is making use of HashSet to improve performance.
  • + *

+ * Note: The following methods will be also called at the end of the operation: + *
  1. {@link #expandItemsAtStartUp()}
  2. + *
  3. {@link #showAllHeaders()} if headers are shown
  4. + *
  5. {@link #onPostUpdate()}
  6. + *
  7. {@link OnUpdateListener#onUpdateEmptyView(int)} if the listener is set
* * @param items the new data set - * @param animate true to animate the changes, false for a quick refresh + * @param animate true to animate the changes, false for an instant refresh * @see #updateDataSet(List) * @see #setAnimateToLimit(int) + * @see #onPostUpdate() * @since 5.0.0-b7 Created *
5.0.0-b8 Synchronization animations limit */ @CallSuper public void updateDataSet(@Nullable List items, boolean animate) { + if (items == null) items = new ArrayList<>(); + restoreScrollableHeadersAndFooters(items); if (animate) { mHandler.removeMessages(UPDATE); mHandler.sendMessage(Message.obtain(mHandler, UPDATE, items)); } else { - if (items == null) mItems = new ArrayList<>(); - else mItems = new ArrayList<>(items); + mItems = items; postUpdate(true); } } /** - * Returns the custom object "Item". - *

This cannot be overridden since the entire library relies on it.

+ * Returns the object of type T. + *

This method cannot be overridden since the entire library relies on it.

* * @param position the position of the item in the list - * @return The custom "Item" object or null if item not found + * @return The T object for the position provided or null if item not found * @since 1.0.0 */ public final T getItem(@IntRange(from = 0) int position) { - if (position < 0 || position >= mItems.size()) return null; + if (position < 0 || position >= getItemCount()) return null; return mItems.get(position); } /** + * This method is mostly used by the adapter if items have stableIds. + * * @param position the position of the current item * @return Hashcode of the item at the specific position * @since 5.0.0-b1 @@ -532,17 +556,39 @@ public long getItemId(int position) { } /** - * This cannot be overridden since the selection relies on it. + * Returns the total number of items in the data set held by the adapter (headers and footers + * INCLUDED). Use {@link #getMainItemCount()} with {@code false} as parameter to retrieve + * only real items excluding headers and footers. + *

Note: This method cannot be overridden since the selection and the internal + * methods rely on it.

* - * @return the total number of the items currently displayed by the adapter + * @return the total number of items (headers and footers INCLUDED) held by the adapter + * @see #getMainItemCount() * @see #getItemCountOfTypes(Integer...) - * @see #getItemCountOfTypesUntil(int, Integer...) * @see #isEmpty() * @since 1.0.0 */ @Override public final int getItemCount() { - return mItems != null ? mItems.size() : 0; + return mItems.size(); + } + + /** + * Returns the total number of items in the data set held by the adapter. + *
    + *
  • Provide {@code true} (default behavior) to count all items, same result of {@link #getItemCount()}.
  • + *
  • Provide {@code false} to count only main items (headers and footers are EXCLUDED).
  • + *
+ * Note: This method cannot be overridden since internal methods rely on it. + * + * @return the total number of items held by the adapter, with or without headers and footers, + * depending by the provided parameter + * @see #getItemCount() + * @see #getItemCountOfTypes(Integer...) + * @since 5.0.0-rc1 + */ + public final int getMainItemCount() { + return getItemCount() - mScrollableHeaders.size() - mScrollableFooters.size(); } /** @@ -551,12 +597,18 @@ public final int getItemCount() { * @param viewTypes the viewTypes to count * @return number of the viewTypes counted * @see #getItemCount() - * @see #getItemCountOfTypesUntil(int, Integer...) + * @see #getMainItemCount() * @see #isEmpty() * @since 5.0.0-b1 */ - public int getItemCountOfTypes(Integer... viewTypes) { - return getItemCountOfTypesUntil(getItemCount(), viewTypes); + public final int getItemCountOfTypes(Integer... viewTypes) { + List viewTypeList = Arrays.asList(viewTypes); + int count = 0; + for (int i = 0; i < getItemCount(); i++) { + if (viewTypeList.contains(getItemViewType(i))) + count++; + } + return count; } /** @@ -566,18 +618,18 @@ public int getItemCountOfTypes(Integer... viewTypes) { * @param position the position limit where to stop counting (included) * @param viewTypes the viewTypes to count * @see #getItemCount() + * @see #getMainItemCount() * @see #getItemCountOfTypes(Integer...) * @see #isEmpty() * @since 5.0.0-b5 + * @deprecated Not used anymore. */ - //TODO: deprecation? - public int getItemCountOfTypesUntil(@IntRange(from = 0) int position, Integer... viewTypes) { + @Deprecated + public final int getItemCountOfTypesUntil(@IntRange(from = 0) int position, Integer... viewTypes) { List viewTypeList = Arrays.asList(viewTypes); int count = 0; for (int i = 0; i < position; i++) { - //Privilege faster counting if autoMap is active - if ((autoMap && viewTypeList.contains(mItems.get(i).getLayoutRes())) || - viewTypeList.contains(getItemViewType(i))) + if (viewTypeList.contains(getItemViewType(i))) count++; } return count; @@ -597,14 +649,34 @@ public boolean isEmpty() { } /** - * Retrieve the global position of the Item in the Adapter list. + * Retrieves the global position of the item in the Adapter list. + * If no scrollable Headers are added, the global position coincides with the cardinal position. + *

This method cannot be overridden since the entire library relies on it.

* * @param item the item to find * @return the global position in the Adapter if found, -1 otherwise * @since 5.0.0-b1 */ - public int getGlobalPositionOf(@NonNull IFlexible item) { - return item != null && mItems != null && !mItems.isEmpty() ? mItems.indexOf(item) : -1; + public final int getGlobalPositionOf(@NonNull IFlexible item) { + return item != null ? mItems.indexOf(item) : -1; + } + + /** + * Retrieves the position of the Main item in the Adapter list excluding the scrollable Headers. + * If no scrollable Headers are added, the cardinal position coincides with the global position. + *

Note: + *
- This method is NOT suitable to call when managing items: ALL insert, remove, move and + * swap operations, should done with global position {@link #getGlobalPositionOf(IFlexible)}. + *
- This method cannot be overridden.

+ * + * @param item the item to find + * @return the position in the Adapter excluding the Scrollable Headers, -1 otherwise + * @since 5.0.0-rc1 + */ + public final int getCardinalPositionOf(@NonNull IFlexible item) { + int position = getGlobalPositionOf(item); + if (position > mScrollableHeaders.size()) position -= mScrollableHeaders.size(); + return position; } /** @@ -615,14 +687,14 @@ public int getGlobalPositionOf(@NonNull IFlexible item) { * @since 2.0.0 */ public boolean contains(@NonNull T item) { - return item != null && mItems != null && mItems.contains(item); + return item != null && mItems.contains(item); } /** * New method to extract the new position where the item should lay. - *

Note: The Comparator should be customized to support all the types of items - * this Adapter is displaying or a ClassCastException will be raised.

- * If Comparator is {@code null} the returned position is 0. + *

Note: The {@code Comparator} object should be customized to support all + * types of items this Adapter is managing or a {@code ClassCastException} will be raised.

+ * If the {@code Comparator} is {@code null} the returned position is 0 (first position). * * @param item the item to evaluate the insertion * @param comparator the Comparator object with the logic to sort the list @@ -630,10 +702,10 @@ public boolean contains(@NonNull T item) { * @since 5.0.0-b7 */ public int calculatePositionFor(@NonNull Object item, @Nullable Comparator comparator) { - //There's nothing to compare + // There's nothing to compare if (comparator == null) return 0; - //Header is visible + // Header is visible if (item instanceof ISectionable) { IHeader header = ((ISectionable) item).getHeader(); if (header != null && !header.isHidden()) { @@ -642,8 +714,8 @@ public int calculatePositionFor(@NonNull Object item, @Nullable Comparator compa Collections.sort(sortedList, comparator); int itemPosition = mItems.indexOf(item); int headerPosition = getGlobalPositionOf(header); - //#143 - calculatePositionFor() missing a +1 when addItem (fixed by condition: itemPosition != -1) - //fix represents the situation when item is before the target position (used in moveItem) + // #143 - calculatePositionFor() missing a +1 when addItem (fixed by condition: itemPosition != -1) + // fix represents the situation when item is before the target position (used in moveItem) int fix = itemPosition != -1 && itemPosition < headerPosition ? 0 : 1; int result = headerPosition + sortedList.indexOf(item) + fix; if (DEBUG) { @@ -654,7 +726,7 @@ public int calculatePositionFor(@NonNull Object item, @Nullable Comparator compa return result; } } - //All other cases + // All other cases List sortedList = new ArrayList(mItems); if (!sortedList.contains(item)) sortedList.add(item); Collections.sort(sortedList, comparator); @@ -663,6 +735,280 @@ public int calculatePositionFor(@NonNull Object item, @Nullable Comparator compa return Math.max(0, sortedList.indexOf(item)); } + /*------------------------------------*/ + /* SCROLLABLE HEADERS/FOOTERS METHODS */ + /*------------------------------------*/ + + /** + * @return unmodifiable list of Scrollable Headers currently held by the Adapter + * @see #addScrollableHeader(IFlexible) + * @see #addScrollableHeaderWithDelay(IFlexible, long, boolean) + * @since 5.0.0-rc1 + */ + public final List getScrollableHeaders() { + return Collections.unmodifiableList(mScrollableHeaders); + } + + /** + * @return unmodifiable list of Scrollable Footers currently held by the Adapter + * @see #addScrollableFooter(IFlexible) + * @see #addScrollableFooterWithDelay(IFlexible, long, boolean) + * @since 5.0.0-rc1 + */ + public final List getScrollableFooters() { + return Collections.unmodifiableList(mScrollableFooters); + } + + /** + * {@inheritDoc} + */ + @Override + public final boolean isScrollableHeaderOrFooter(int position) { + T item = getItem(position); + return item != null && mScrollableHeaders.contains(item) || mScrollableFooters.contains(item); + } + + /** + * Adds a Scrollable Header. + *

Scrollable Headers have the following characteristic: + *

    + *
  • lay always before any main item.
  • + *
  • cannot be selectable nor draggable.
  • + *
  • cannot be inserted twice, but many can be inserted.
  • + *
  • any new header will be inserted before the existent.
  • + *
  • can be of any type so they can be bound at runtime with any data inside.
  • + *
  • won't be filtered because they won't be part of the main list, but added separately + * at the initialization phase
  • + *
  • can be added and removed with certain delay.
  • + *

+ * + * @param headerItem the header item to be added + * @return true if the header has been successfully added, false if the header already exists + * @see #getScrollableHeaders() + * @see #addScrollableHeaderWithDelay(IFlexible, long, boolean) + * @since 5.0.0-rc1 + */ + //TODO: Endless Top Scrolling + public final boolean addScrollableHeader(@NonNull T headerItem) { + if (DEBUG) Log.d(TAG, "Add scrollable header " + getClassName(headerItem)); + if (!mScrollableHeaders.contains(headerItem)) { + headerItem.setSelectable(false); + headerItem.setDraggable(false); + int progressFix = 0;//(headerItem == mProgressItem) ? mScrollableHeaders.size() : 0; + mScrollableHeaders.add(headerItem); + setScrollAnimate(true); //Headers will scroll animate + performInsert(progressFix, Collections.singletonList(headerItem), true); + setScrollAnimate(false); + return true; + } else { + Log.w(TAG, "Scrollable header " + getClassName(headerItem) + " already exists"); + return false; + } + } + + /** + * Adds a Scrollable Footer. + *

Scrollable Footers have the following characteristic: + *

    + *
  • lay always after any main item.
  • + *
  • cannot be selectable nor draggable.
  • + *
  • cannot be inserted twice, but many can be inserted.
  • + *
  • cannot scroll animate, when inserted for the first time.
  • + *
  • any new footer will be inserted after the existent.
  • + *
  • can be of any type so they can be bound at runtime with any data inside.
  • + *
  • won't be filtered because they won't be part of the main list, but added separately + * at the initialization phase
  • + *
  • can be added and removed with certain delay.
  • + *
  • endless {@code progressItem} is handled as a Scrollable Footer, but it will be always + * displayed between the main items and the others footers.
  • + *

+ * + * @param footerItem the footer item to be added + * @return true if the footer has been successfully added, false if the footer already exists + * @see #getScrollableFooters() + * @see #addScrollableFooterWithDelay(IFlexible, long, boolean) + * @since 5.0.0-rc1 + */ + public final boolean addScrollableFooter(@NonNull T footerItem) { + if (!mScrollableFooters.contains(footerItem)) { + if (DEBUG) Log.d(TAG, "Add scrollable footer " + getClassName(footerItem)); + footerItem.setSelectable(false); + footerItem.setDraggable(false); + int progressFix = (footerItem == mProgressItem) ? mScrollableFooters.size() : 0; + //Prevent wrong position after a possible updateDataSet + if (progressFix > 0 && mScrollableFooters.size() > 0) { + mScrollableFooters.add(0, footerItem); + } else { + mScrollableFooters.add(footerItem); + } + performInsert(getItemCount() - progressFix, Collections.singletonList(footerItem), true); + return true; + } else { + Log.w(TAG, "Scrollable footer " + getClassName(footerItem) + " already exists"); + return false; + } + } + + /** + * Removes the provided Scrollable Header. + * + * @param headerItem the header to remove + * @see #removeScrollableHeaderWithDelay(IFlexible, long) + * @see #removeAllScrollableHeaders() + * @since 5.0.0-rc1 + */ + public final void removeScrollableHeader(@NonNull T headerItem) { + if (mScrollableHeaders.remove(headerItem)) { + if (DEBUG) Log.d(TAG, "Remove scrollable header " + getClassName(headerItem)); + performRemove(headerItem, true); + } + } + + /** + * Removes the provided Scrollable Footer. + * + * @param footerItem the footer to remove + * @see #removeScrollableFooterWithDelay(IFlexible, long) + * @see #removeAllScrollableFooters() + * @since 5.0.0-rc1 + */ + public final void removeScrollableFooter(@NonNull T footerItem) { + if (mScrollableFooters.remove(footerItem)) { + if (DEBUG) Log.d(TAG, "Remove scrollable footer " + getClassName(footerItem)); + performRemove(footerItem, true); + } + } + + /** + * Removes all Scrollable Headers at once. + * + * @see #removeScrollableHeader(IFlexible) + * @see #removeScrollableHeaderWithDelay(IFlexible, long) + * @since 5.0.0-rc1 + */ + public final void removeAllScrollableHeaders() { + if (mScrollableHeaders.size() > 0) { + if (DEBUG) Log.d(TAG, "Remove all scrollable headers"); + mItems.removeAll(mScrollableHeaders); + notifyItemRangeRemoved(0, mScrollableHeaders.size()); + mScrollableHeaders.clear(); + } + } + + /** + * Removes all Scrollable Footers at once. + * + * @see #removeScrollableFooter(IFlexible) + * @see #removeScrollableFooterWithDelay(IFlexible, long) + * @since 5.0.0-rc1 + */ + public final void removeAllScrollableFooters() { + if (mScrollableFooters.size() > 0) { + if (DEBUG) Log.d(TAG, "Remove all scrollable footers"); + mItems.removeAll(mScrollableFooters); + notifyItemRangeRemoved(getItemCount() - 1 - mScrollableHeaders.size(), mScrollableFooters.size()); + mScrollableFooters.clear(); + } + } + + /** + * Same as {@link #addScrollableHeader(IFlexible)} but with a delay and the possibility to + * scroll to it. + * + * @param headerItem the header item to be added + * @param delay the delay in ms + * @param scrollToPosition true to scroll to the header item position once it has been added + * @see #addScrollableHeader(IFlexible) + * @since 5.0.0-rc1 + */ + public final void addScrollableHeaderWithDelay(@NonNull final T headerItem, @IntRange(from = 0) long delay, + final boolean scrollToPosition) { + if (DEBUG) + Log.d(TAG, "Enqueued adding scrollable header (" + delay + "ms) " + getClassName(headerItem)); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (addScrollableHeader(headerItem) && scrollToPosition) + performScroll(getGlobalPositionOf(headerItem)); + } + }, delay); + } + + /** + * Same as {@link #addScrollableFooter(IFlexible)} but with a delay and the possibility to + * scroll to it. + * + * @param footerItem the footer item to be added + * @param delay the delay in ms + * @param scrollToPosition true to scroll to the footer item position once it has been added + * @see #addScrollableFooter(IFlexible) + * @since 5.0.0-rc1 + */ + public final void addScrollableFooterWithDelay(@NonNull final T footerItem, @IntRange(from = 0) long delay, + final boolean scrollToPosition) { + if (DEBUG) + Log.d(TAG, "Enqueued adding scrollable footer (" + delay + "ms) " + getClassName(footerItem)); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (addScrollableFooter(footerItem) && scrollToPosition) + performScroll(getGlobalPositionOf(footerItem)); + } + }, delay); + } + + /** + * Same as {@link #removeScrollableHeader(IFlexible)} but with a delay. + * + * @param headerItem the header item to be removed + * @param delay the delay in ms + * @see #removeScrollableHeader(IFlexible) + * @see #removeAllScrollableHeaders() + * @since 5.0.0-rc1 + */ + public final void removeScrollableHeaderWithDelay(@NonNull final T headerItem, @IntRange(from = 0) long delay) { + if (DEBUG) + Log.d(TAG, "Enqueued removing scrollable header (" + delay + "ms) " + getClassName(headerItem)); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + removeScrollableHeader(headerItem); + } + }, delay); + } + + /** + * Same as {@link #removeScrollableFooter(IFlexible)} but with a delay. + * + * @param footerItem the footer item to be removed + * @param delay the delay in ms + * @see #removeScrollableFooter(IFlexible) + * @see #removeAllScrollableFooters() + * @since 5.0.0-rc1 + */ + public final void removeScrollableFooterWithDelay(@NonNull final T footerItem, @IntRange(from = 0) long delay) { + if (DEBUG) + Log.d(TAG, "Enqueued removing scrollable footer (" + delay + "ms) " + getClassName(footerItem)); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + removeScrollableFooter(footerItem); + } + }, delay); + } + + /** + * Helper method to restore the scrollable headers/footers along with the main items. + * After the update and the filter operations. + */ + private void restoreScrollableHeadersAndFooters(List items) { + for (T item : mScrollableHeaders) + if (items.size() > 0) items.add(0, item); + else items.add(item); + for (T item : mScrollableFooters) + items.add(item); + } + /*--------------------------*/ /* HEADERS/SECTIONS METHODS */ /*--------------------------*/ @@ -671,8 +1017,10 @@ public int calculatePositionFor(@NonNull Object item, @Nullable Comparator compa * @return true if orphan headers will be removed when unlinked, false if are kept unlinked * @see #setRemoveOrphanHeaders(boolean) * @since 5.0.0-b6 + * @deprecated Orphan headers methods series: There's no advantage to keep in the adapter such + * references, most of the time, user has the control on the items to remove, headers as well. */ - //TODO: deprecation? + @Deprecated public boolean isRemoveOrphanHeaders() { return removeOrphanHeaders; } @@ -685,23 +1033,27 @@ public boolean isRemoveOrphanHeaders() { * @return this Adapter, so the call can be chained * @see #getOrphanHeaders() * @since 5.0.0-b6 + * @deprecated Orphan headers methods series: There's no advantage to keep in the adapter such + * references, most of the time, user has the control on the items to remove, headers as well. */ - //TODO: deprecation? + @Deprecated public FlexibleAdapter setRemoveOrphanHeaders(boolean removeOrphanHeaders) { + if (DEBUG) Log.i(TAG, "Set removeOrphanHeaders=" + removeOrphanHeaders); this.removeOrphanHeaders = removeOrphanHeaders; return this; } /** - * Setting to automatically unlink the just deleted header from items having that header linked. + * Setting to automatically unlink the deleted header from items having that header linked. *

Default value is {@code false}.

* - * @param unlinkOnRemoveHeader true to unlink also all items with the just deleted header, - * false otherwise + * @param unlinkOnRemoveHeader true to unlink the deleted header from items having that header + * linked, false otherwise * @return this Adapter, so the call can be chained * @since 5.0.0-b6 */ public FlexibleAdapter setUnlinkAllItemsOnRemoveHeaders(boolean unlinkOnRemoveHeader) { + if (DEBUG) Log.i(TAG, "Set unlinkOnRemoveHeader=" + unlinkOnRemoveHeader); this.unlinkOnRemoveHeader = unlinkOnRemoveHeader; return this; } @@ -713,8 +1065,10 @@ public FlexibleAdapter setUnlinkAllItemsOnRemoveHeaders(boolean unlinkOnRemoveHe * @return the list of the orphan headers collected until this moment * @see #setRemoveOrphanHeaders(boolean) * @since 5.0.0-b6 + * @deprecated Orphan headers methods series: There's no advantage to keep in the adapter such + * references, most of the time, user has the control on the items to remove, headers as well. */ - //TODO: deprecation? + @Deprecated @NonNull public List getOrphanHeaders() { return mOrphanHeaders; @@ -729,8 +1083,10 @@ public List getOrphanHeaders() { * @param header the header item * @return this Adapter, so the call can be chained * @since 5.0.0-b6 + * @deprecated We simply can use {@code item.setHeader(header)}. Also, it was not obvious + * this method also show header, for that we should use add item or section. */ - //TODO: deprecation? + @Deprecated public FlexibleAdapter linkHeaderTo(@NonNull T item, @NonNull IHeader header) { linkHeaderTo(item, header, Payload.LINK); if (header.isHidden() && headersShown) { @@ -745,8 +1101,10 @@ public FlexibleAdapter linkHeaderTo(@NonNull T item, @NonNull IHeader header) { * * @param item the item that holds the header * @since 5.0.0-b6 + * @deprecated We simply can use {@code item.setHeader(null)}. Also, it was not obvious + * this method also hide header, for that we should use remove item or section. */ - //TODO: deprecation? + @Deprecated public IHeader unlinkHeaderFrom(@NonNull T item) { IHeader header = unlinkHeaderFrom(item, Payload.UNLINK); if (header != null && !header.isHidden()) { @@ -773,7 +1131,7 @@ public List getHeaderItems() { /** * @param item the item to check - * @return true if the item is an instance of {@link IHeader} interface + * @return true if the item is an instance of {@link IHeader} interface, false otherwise * @since 5.0.0-b6 */ public boolean isHeader(T item) { @@ -781,7 +1139,7 @@ public boolean isHeader(T item) { } /** - * Helper for the Adapter to check if an item holds a header + * Helper method to check if an item holds a header. * * @param item the identified item * @return true if the item holds a header, false otherwise @@ -794,7 +1152,7 @@ public boolean hasHeader(@NonNull T item) { /** * Checks if the item has a header and that header is the same of the provided one. * - * @param item the item supposing having a header + * @param item the item supposing having the header * @param header the header to compare * @return true if the item has a header and it is the same of the provided one, false otherwise * @since 5.0.0-b6 @@ -805,7 +1163,7 @@ public boolean hasSameHeader(@NonNull T item, @NonNull IHeader header) { } /** - * Provides the header of the specified Sectionable. + * Provides the header of the provided {@code ISectionable} item. * * @param item the ISectionable item holding a header * @return the header of the passed Sectionable, null otherwise @@ -825,11 +1183,10 @@ public IHeader getHeaderOf(@NonNull T item) { * @return the IHeader item linked to the specified item position * @since 5.0.0-b6 */ - //TODO: rename to getSectionByItemPosition? public IHeader getSectionHeader(@IntRange(from = 0) int position) { - //Headers are not visible nor sticky + // Headers are not visible nor sticky if (!headersShown) return null; - //When headers are visible and sticky, get the previous header + // When headers are visible and sticky, get the previous header for (int i = position; i >= 0; i--) { T item = getItem(i); if (isHeader(item)) return (IHeader) item; @@ -844,8 +1201,10 @@ public IHeader getSectionHeader(@IntRange(from = 0) int position) { * @param header the header/section item * @return the index of the specified header/section * @since 5.0.0-b6 + * @deprecated The library doesn't use the concept of "Section index": we don't add sections + * using "Section index". */ - //TODO: deprecation? + @Deprecated public int getSectionIndex(@NonNull IHeader header) { int position = getGlobalPositionOf(header); return getSectionIndex(position); @@ -858,8 +1217,10 @@ public int getSectionIndex(@NonNull IHeader header) { * @param position any item position * @return the index of the specified item position * @since 5.0.0-b6 + * @deprecated The library doesn't use the concept of "Section index": we don't add sections + * using "Section index". */ - //TODO: deprecation? + @Deprecated public int getSectionIndex(@IntRange(from = 0) int position) { int sectionIndex = 0; for (int i = 0; i <= position; i++) { @@ -869,10 +1230,10 @@ public int getSectionIndex(@IntRange(from = 0) int position) { } /** - * Provides all the items that belongs to the section represented by the specified header. + * Provides all the items that belongs to the section represented by the provided header. * - * @param header the header that represents the section - * @return NonNull list of all items in the specified section. + * @param header the {@code IHeader} item that represents the section + * @return NonNull list of all items in the provided section * @since 5.0.0-b6 */ @NonNull @@ -888,10 +1249,11 @@ public List getSectionItems(@NonNull IHeader header) { } /** - * Provides all the item positions that belongs to the section represented by the specified header. + * Provides all the item positions that belongs to the section represented by the provided + * header. * - * @param header the header that represents the section - * @return NonNull list of all item positions in the specified section. + * @param header the {@code IHeader} item that represents the section + * @return NonNull list of all item positions in the provided section * @since 5.0.0-b8 */ @NonNull @@ -914,29 +1276,30 @@ public boolean areHeadersShown() { } /** - * Returns if Adapter will display sticky headers on the top. + * Returns if Adapter can actually display sticky headers on the top. * * @return true if headers can be sticky, false if headers are scrolled together with all items * @since 5.0.0-b6 */ public boolean areHeadersSticky() { - return headersSticky; + return mStickyHeaderHelper != null; } /** * Enables the sticky header functionality. - *

Headers can be sticky only if they are shown. Command is otherwise ignored!

- * NOTE: - *
- You must read {@link #getStickySectionHeadersHolder()}. + *

Headers can be sticky only if they are shown.

+ * Note: + *
- You must read {@link #getStickyHeaderContainer()}. *
- Sticky headers are now clickable as any Views, but cannot be dragged nor swiped. *
- Content and linkage are automatically updated. * * @return this Adapter, so the call can be chained - * @see #getStickySectionHeadersHolder() + * @see #getStickyHeaderContainer() * @since 5.0.0-b6 + * @deprecated Use {@link #setStickyHeaders(boolean)} with {@code true} as parameter. */ - //TODO: deprecation use setStickyHeaders(true)? - public FlexibleAdapter enableStickyHeaders() { + @Deprecated + public FlexibleAdapter enableStickyHeader() { return setStickyHeaders(true); } @@ -944,27 +1307,96 @@ public FlexibleAdapter enableStickyHeaders() { * Disables the sticky header functionality. * * @since 5.0.0-b6 + * @deprecated Use {@link #setStickyHeaders(boolean)} with {@code false} as parameter. */ - //TODO: deprecation use setStickyHeaders(false)? + @Deprecated public void disableStickyHeaders() { setStickyHeaders(false); } - private FlexibleAdapter setStickyHeaders(final boolean sticky) { + /** + * Enables/Disables the sticky header feature with default sticky layout container. + *

Note: + *

    + *
  • You should consider to display headers with {@link #setDisplayHeadersAtStartUp(boolean)}: + * Feature can enabled/disabled freely, but if headers are hidden nothing will happen.
  • + *
  • Only in case of "Sticky Header" items you must provide {@code true} to the + * constructor: {@link FlexibleViewHolder#FlexibleViewHolder(View, FlexibleAdapter, boolean)}.
  • + *
  • Optionally, you can set a custom sticky layout container that must be already + * inflated. + *
    Check {@link #setStickyHeaders(boolean, ViewGroup)}.
  • + *
  • Optionally, you can set a layout elevation: Header item elevation is used first, + * if not set, default elevation of {@code 21f} pixel is used. + *
  • Sticky headers are clickable as any views, but cannot be dragged nor swiped.
  • + *
  • Content and linkage are automatically updated.
  • + *
  • Sticky layout container is fade-in and fade-out animated when feature + * is respectively enabled and disabled.
  • + *
  • Sticky container can be elevated only if the header item layout has elevation (Do not + * exaggerate with elevation).
  • + *
+ *

+ * Important! In order to display the Refresh circle AND the FastScroller on the Top of + * the sticky container, the RecyclerView must be wrapped with a {@code FrameLayout} as following: + *
+	 * <FrameLayout
+	 *     android:layout_width="match_parent"
+	 *     android:layout_height="match_parent">
+	 *
+	 *     <android.support.v7.widget.RecyclerView
+	 *         android:id="@+id/recycler_view"
+	 *         android:layout_width="match_parent"
+	 *         android:layout_height="match_parent"/>
+	 *
+	 * </FrameLayout>
+	 * 
+ * + * @param sticky true to initialize sticky headers with default container, false to disable them + * @return this Adapter, so the call can be chained + * @throws IllegalStateException if this Adapter was not attached to the RecyclerView + * @see #setStickyHeaders(boolean, ViewGroup) + * @see #setDisplayHeadersAtStartUp(boolean) + * @see #setStickyHeaderElevation(float) + * @since 5.0.0-rc1 + */ + public FlexibleAdapter setStickyHeaders(boolean sticky) { + return setStickyHeaders(sticky, mStickyContainer); + } + + /** + * Enables/Disables the sticky header feature with a custom sticky layout container. + *

Important: Read the javaDoc of the overloaded method {@link #setStickyHeaders(boolean)}.

+ * + * @param sticky true to initialize sticky headers, false to disable them + * @param stickyContainer user defined and already inflated sticky layout that will + * hold the sticky header itemViews + * @return this Adapter, so the call can be chained + * @throws IllegalStateException if this Adapter was not attached to the RecyclerView + * @see #setStickyHeaders(boolean) + * @see #setDisplayHeadersAtStartUp(boolean) + * @see #setStickyHeaderElevation(float) + * @since 5.0.0-rc1 + */ + public FlexibleAdapter setStickyHeaders(final boolean sticky, @NonNull ViewGroup stickyContainer) { + if (DEBUG) Log.i(TAG, "Set stickyHeaders=" + sticky + " (in Post!)" + + (stickyContainer != null ? " with user defined Sticky Container" : "")); + + // With user defined container + mStickyContainer = stickyContainer; + + // Run in post to be sure about the RecyclerView initialization mHandler.post(new Runnable() { @Override public void run() { - // Add or Remove the sticky headers + // Enable or Disable the sticky headers layout if (sticky) { - headersSticky = true; - if (mStickyHeaderHelper == null) - mStickyHeaderHelper = new StickyHeaderHelper(FlexibleAdapter.this, mStickyHeaderChangeListener); - if (!mStickyHeaderHelper.isAttachedToRecyclerView()) + if (mStickyHeaderHelper == null) { + mStickyHeaderHelper = new StickyHeaderHelper(FlexibleAdapter.this, + mStickyHeaderChangeListener, mStickyContainer); mStickyHeaderHelper.attachToRecyclerView(mRecyclerView); - if (DEBUG) Log.i(TAG, "Sticky headers enabled"); + if (DEBUG) Log.i(TAG, "Sticky headers enabled"); + } } else if (mStickyHeaderHelper != null) { - headersSticky = false; - mStickyHeaderHelper.detachFromRecyclerView(mRecyclerView); + mStickyHeaderHelper.detachFromRecyclerView(); mStickyHeaderHelper = null; if (DEBUG) Log.i(TAG, "Sticky headers disabled"); } @@ -973,6 +1405,35 @@ public void run() { return this; } + /** + * Gets the layout elevation for sticky header. + *

Note: This setting is ignored if the header item has already an elevation. The + * header elevation overrides this setting.

+ * + * @return the elevation in pixel + * @see #setStickyHeaderElevation(float) + * @since 5.0.0-rc1 + */ + public float getStickyHeaderElevation() { + return mStickyElevation; + } + + /** + * Sets the elevation for the sticky header layout. + *

Note: This setting is ignored if the header item has already an elevation. The + * header elevation overrides this setting.

+ * Default value is 0. + * + * @param stickyElevation the elevation in pixel + * @return this Adapter, so the call can be chained + * @see #getStickyHeaderElevation() + * @since 5.0.0-rc1 + */ + public FlexibleAdapter setStickyHeaderElevation(@FloatRange(from = 0) float stickyElevation) { + mStickyElevation = stickyElevation; + return this; + } + /** * Returns the ViewGroup (FrameLayout) that will hold the headers when sticky. *

INCLUDE the predefined layout after the RecyclerView widget, example: @@ -987,21 +1448,57 @@ public void run() { * * @return ViewGroup layout that will hold the sticky header ItemViews * @since 5.0.0-b6 + * @deprecated Unused. Use {@link #setStickyHeaders(boolean, ViewGroup)}. */ + @Deprecated public ViewGroup getStickySectionHeadersHolder() { - return (ViewGroup) Utils.scanForActivity(mRecyclerView.getContext()).findViewById(R.id.sticky_header_container); + if (mStickyContainer == null) { + mStickyContainer = (ViewGroup) Utils + .scanForActivity(mRecyclerView.getContext()) + .findViewById(R.id.sticky_header_container); + } + return mStickyContainer; } /** - * Sets if all headers should be shown at startup. 2 effects are possible depending by - * the current flag of scrolling animation: - *

- if enabled, scrolling animations are performed as configured for the header item; - *
- if disabled, {@code notifyItemInserted()} is instead performed for each header - * item.

- * Note: - *
- {@code showAllHeaders()} is already called by this method. - *
- You should call this method before enabling sticky headers! - *

Default value is {@code false} (headers are NOT shown at startup).

+ * Returns the ViewGroup (FrameLayout) that will hold the headers when sticky. + * Set a custom container with {@link #setStickyHeaderContainer(ViewGroup)} before enabling + * sticky headers. + * + * @return ViewGroup layout that will hold the sticky header itemViews + * @since 5.0.0-rc1 + * @deprecated Unused, not needed anymore. + */ + @Deprecated + public ViewGroup getStickyHeaderContainer() { + return mStickyContainer; + } + + /** + * Sets a custom {@link ViewGroup} container for Sticky Headers when the default can't be used. + * + * @param stickyContainer custom container for Sticky Headers + * @since 5.0.0-rc1 + * @deprecated Use {@link #setStickyHeaders(boolean, ViewGroup)}. + */ + @Deprecated + public FlexibleAdapter setStickyHeaderContainer(@NonNull ViewGroup stickyContainer) { + if (mStickyHeaderHelper != null) { + Log.w(TAG, "StickyHeader has been already initialized! Call this method before enabling StickyHeaders"); + } + if (DEBUG && stickyContainer != null) + Log.i(TAG, "Set stickyHeaderContainer=" + getClassName(stickyContainer)); + this.mStickyContainer = stickyContainer; + return this; + } + + /** + * Sets if all headers should be shown at startup. + *

If called, this method won't trigger {@code notifyItemInserted()} and scrolling + * animations are instead performed if the header item was configured with animation: + * Headers will be loaded/bound along with others items.

+ * Note: Headers can only be shown or hidden all together. + *

Default value is {@code false} (headers are not shown at startup).

* * @param displayHeaders true to display headers, false to keep them hidden * @return this Adapter, so the call can be chained @@ -1009,51 +1506,54 @@ public ViewGroup getStickySectionHeadersHolder() { * @see #setAnimationOnScrolling(boolean) * @since 5.0.0-b6 */ - //TODO: deprecation, rename to displayHeadersAtStartUp() with no parameters? public FlexibleAdapter setDisplayHeadersAtStartUp(boolean displayHeaders) { if (!headersShown && displayHeaders) { - showAllHeaders(isAnimationOnScrollingEnabled()); + showAllHeaders(true); } return this; } /** - * Shows all headers in the RecyclerView at their linked position. Not intended to be called at - * startup.
To display headers at startup please use {@code setDisplayHeadersAtStartUp()} - * instead. - *

Note: Headers can only be shown or hidden all together.

+ * Shows all headers in the RecyclerView at their linked position. + *

If called at startup, this method will trigger {@code notifyItemInserted} for a + * different loading effect: Headers will be inserted after the items!

+ * Note: Headers can only be shown or hidden all together. * + * @return this Adapter, so the call can be chained * @see #hideAllHeaders() * @see #setDisplayHeadersAtStartUp(boolean) * @since 5.0.0-b1 */ - public void showAllHeaders() { + public FlexibleAdapter showAllHeaders() { showAllHeaders(false); + return this; } /** - * @param init true to skip {@code notifyItemInserted}, false to make the call in Post and - * notify single insertion + * @param init true to skip {@code notifyItemInserted}, false to make the call in Post + * and notify single insertion */ private void showAllHeaders(boolean init) { if (init) { - //No notifyItemInserted! + if (DEBUG) Log.i(TAG, "showAllHeaders at startup"); + // No notifyItemInserted! showAllHeadersWithReset(true); } else { - //In post, let's notifyItemInserted! + if (DEBUG) Log.i(TAG, "showAllHeaders with insert notification (in Post!)"); + // In post, let's notifyItemInserted! mHandler.post(new Runnable() { @Override public void run() { - //#144 - Check if headers are already shown, discard the call to not duplicate headers + // #144 - Check if headers are already shown, discard the call to not duplicate headers if (headersShown) { - Log.w(TAG, "Headers already shown OR the method setDisplayHeadersAtStartUp() was already called!"); + Log.w(TAG, "Double call detected! Headers already shown OR the method showAllHeaders() was already called!"); return; } showAllHeadersWithReset(false); - //#142 - At startup, when scrolling animation is disabled, insert notifications - // are performed to show headers for the first time. Header item is not visible - // at position 0: it has to be displayed by scrolling to it. This resolves the - // first item below sticky header when enabled as well. + // #142 - At startup, when insert notifications are performed to show headers + // for the first time. Header item is not visible at position 0: it has to be + // displayed by scrolling to it. This resolves the first item below sticky + // header when enabled as well. if (mRecyclerView != null) { int firstVisibleItem = Utils.findFirstCompletelyVisibleItemPosition(mRecyclerView.getLayoutManager()); if (firstVisibleItem == 0 && isHeader(getItem(0)) && !isHeader(getItem(1))) { @@ -1069,16 +1569,21 @@ public void run() { * @param init true to skip the call to notifyItemInserted, false otherwise */ private void showAllHeadersWithReset(boolean init) { - multiRange = true; int position = 0; - resetHiddenStatus();//Necessary after the filter and the update - while (position < mItems.size()) { - if (showHeaderOf(position, mItems.get(position), init)) - position++;//It's the same element, skip it + IHeader sameHeader = null; + while (position < getItemCount() - mScrollableFooters.size()) { + T item = mItems.get(position); + // Reset hidden status! Necessary after the filter and the update + IHeader header = getHeaderOf(item); + if (header != sameHeader && header != null && !isExpandable((T) header)) { + sameHeader = header; + header.setHidden(true); + } + if (showHeaderOf(position, item, init)) + position++; //It's the same element, skip it position++; } headersShown = true; - multiRange = false; } /** @@ -1086,27 +1591,20 @@ private void showAllHeadersWithReset(boolean init) { * * @param position the position where the header will be displayed * @param item the item that holds the header - * @param init for silent initialization + * @param init for silent initialization: skip notifyItemInserted * @since 5.0.0-b1 */ private boolean showHeaderOf(int position, @NonNull T item, boolean init) { - //Take the header + // Take the header IHeader header = getHeaderOf(item); - //Check header existence + // Check header existence if (header == null || getPendingRemovedItem(item) != null) return false; if (header.isHidden()) { if (DEBUG) Log.v(TAG, "Showing header at position " + position + " header=" + header); header.setHidden(false); - if (init) {//Skip notifyItemInserted! - if (position < mItems.size()) { - mItems.add(position, (T) header); - } else { - mItems.add((T) header); - } - return true; - } else { - return addItem(position, (T) header); - } + // Insert header, but skip notifyItemInserted when init=true! + performInsert(position, Collections.singletonList((T) header), !init); + return true; } return false; } @@ -1116,6 +1614,7 @@ private boolean showHeaderOf(int position, @NonNull T item, boolean init) { *

Headers can be shown or hidden all together.

* * @see #showAllHeaders() + * @see #setDisplayHeadersAtStartUp(boolean) * @since 5.0.0-b1 */ public void hideAllHeaders() { @@ -1123,20 +1622,20 @@ public void hideAllHeaders() { @Override public void run() { multiRange = true; - //Hide orphan headers first - for (IHeader header : getOrphanHeaders()) { - hideHeader(getGlobalPositionOf(header), header); - } - //Hide linked headers - int position = mItems.size() - 1; - while (position >= 0) { + // Hide linked headers between Scrollable Headers and Footers + int position = getItemCount() - mScrollableFooters.size() - 1; + while (position >= Math.max(0, mScrollableHeaders.size() - 1)) { T item = mItems.get(position); if (isHeader(item)) hideHeader(position, (IHeader) item); position--; } headersShown = false; - setStickyHeaders(false); + // Clear the header currently sticky + if (areHeadersSticky()) { + mStickyHeaderHelper.clearHeaderWithAnimation(); + } + // setStickyHeaders(false); multiRange = false; } }); @@ -1149,9 +1648,9 @@ public void run() { * @since 5.0.0-b1 */ private boolean hideHeaderOf(@NonNull T item) { - //Take the header + // Take the header IHeader header = getHeaderOf(item); - //Check header existence + // Check header existence return header != null && !header.isHidden() && hideHeader(getGlobalPositionOf(header), header); } @@ -1159,7 +1658,7 @@ private boolean hideHeader(int position, IHeader header) { if (position >= 0) { if (DEBUG) Log.v(TAG, "Hiding header at position " + position + " header=" + header); header.setHidden(true); - //Remove and notify removals + // Remove and notify removals mItems.remove(position); notifyItemRemoved(position); return true; @@ -1167,23 +1666,6 @@ private boolean hideHeader(int position, IHeader header) { return false; } - /** - * Helper method to ensure that all current headers are hidden before they are shown again. - *

This method is already called inside {@link #showAllHeadersWithReset(boolean)}.

- * This is necessary when {@link #setDisplayHeadersAtStartUp(boolean)} is set true and also - * if an Activity/Fragment has been closed and then reopened. We need to reset hidden status, - * the process is very fast. - * - * @since 5.0.0-b6 - */ - private void resetHiddenStatus() { - for (T item : mItems) { - IHeader header = getHeaderOf(item); - if (header != null && !isExpandable((T) header)) - header.setHidden(true); - } - } - /** * Internal method to link the header to the new item. *

Used by the Adapter during the Remove/Restore/Move operations.

@@ -1201,16 +1683,17 @@ private boolean linkHeaderTo(@NonNull T item, @NonNull IHeader header, @Nullable boolean linked = false; if (item != null && item instanceof ISectionable) { ISectionable sectionable = (ISectionable) item; - //Unlink header only if different + // Unlink header only if different if (sectionable.getHeader() != null && !sectionable.getHeader().equals(header)) { unlinkHeaderFrom((T) sectionable, Payload.UNLINK); } if (sectionable.getHeader() == null && header != null) { if (DEBUG) Log.v(TAG, "Link header " + header + " to " + sectionable); + //TODO: try-catch for when sectionable item has a different header class signature, if so, they just can't accept that header! sectionable.setHeader(header); linked = true; removeFromOrphanList(header); - //Notify items + // Notify items if (payload != null) { if (!header.isHidden()) notifyItemChanged(getGlobalPositionOf(header), payload); if (!item.isHidden()) notifyItemChanged(getGlobalPositionOf(item), payload); @@ -1240,7 +1723,7 @@ private IHeader unlinkHeaderFrom(@NonNull T item, @Nullable Object payload) { if (DEBUG) Log.v(TAG, "Unlink header " + header + " from " + sectionable); sectionable.setHeader(null); addToOrphanListIfNeeded(header, getGlobalPositionOf(item), 1); - //Notify items + // Notify items if (payload != null) { if (!header.isHidden()) notifyItemChanged(getGlobalPositionOf(header), payload); if (!item.isHidden()) notifyItemChanged(getGlobalPositionOf(item), payload); @@ -1250,9 +1733,9 @@ private IHeader unlinkHeaderFrom(@NonNull T item, @Nullable Object payload) { return null; } - //TODO: deprecation? + @Deprecated private void addToOrphanListIfNeeded(IHeader header, int positionStart, int itemCount) { - //Check if the header is not already added (happens after un-linkage with un-success linkage) + // Check if the header is not already added (happens after un-linkage with un-success linkage) if (!mOrphanHeaders.contains(header) && !isHeaderShared(header, positionStart, itemCount)) { mOrphanHeaders.add(header); if (DEBUG) @@ -1260,22 +1743,22 @@ private void addToOrphanListIfNeeded(IHeader header, int positionStart, int item } } - //TODO: deprecation? + @Deprecated private void removeFromOrphanList(IHeader header) { if (mOrphanHeaders.remove(header) && DEBUG) Log.v(TAG, "Removed from orphan list [" + mOrphanHeaders.size() + "] Header " + header); } - //TODO: deprecation? + @Deprecated private boolean isHeaderShared(IHeader header, int positionStart, int itemCount) { int firstElementWithHeader = getGlobalPositionOf(header) + 1; - for (int i = firstElementWithHeader; i < mItems.size(); i++) { + for (int i = firstElementWithHeader; i < getItemCount() - mScrollableFooters.size(); i++) { T item = getItem(i); - //Another header is met, we can stop here + // Another header is met, we can stop here if (item instanceof IHeader) break; - //Skip the items under modification + // Skip the items under modification if (i >= positionStart && i < positionStart + itemCount) continue; - //An element with same header is met + // An element with same header is met if (hasSameHeader(item, header)) return true; } @@ -1287,45 +1770,47 @@ private boolean isHeaderShared(IHeader header, int positionStart, int itemCount) /*---------------------*/ /** - * Returns the ViewType for all Items depends by the current position. - *

You can override this method to return specific values (don't call super) or you can + * Returns the ViewType for all Items depends by the provided position. + *

You can override this method to return specific values (don't call super) OR you can * let this method to call the implementation of {@code IFlexible#getLayoutRes()} so ViewTypes * are automatically mapped (AutoMap).

* * @param position position for which ViewType is requested - * @return if Item is found, any integer value from user layout resource if defined in + * @return if item is found, any integer value from user layout resource if defined in * {@code IFlexible#getLayoutRes()} * @since 5.0.0-b1 */ @Override public int getItemViewType(int position) { T item = getItem(position); - //Map the view type if not done yet + // Map the view type if not done yet mapViewTypeFrom(item); autoMap = true; return item.getLayoutRes(); } /** - * You can override this method to create ViewHolder from inside the Adapter or you can let + * You can override this method to create ViewHolder from inside the Adapter OR you can let * this method to call the implementation of {@code IFlexible#createViewHolder()} to create - * ViewHolder from inside the Item (AutoMap). + * ViewHolder from inside the item (AutoMap). + *

HELP: To know how to implement AutoMap for ViewTypes please refer to the + * FlexibleAdapter Wiki Page + * on GitHub. *

{@inheritDoc} * * @return a new ViewHolder that holds a View of the given view type - * @throws IllegalStateException if {@code IFlexible#createViewHolder()} is not implemented and - * if this method is not overridden OR if ViewType instance has - * not been correctly mapped. + * @throws IllegalStateException if this method is not overridden OR AutoMap is not active + * OR if ViewType instance has not been properly mapped. * @see IFlexible#createViewHolder(FlexibleAdapter, LayoutInflater, ViewGroup) * @since 5.0.0-b1 */ @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { T item = getViewTypeInstance(viewType); - if (item == null) { - //If everything has been set properly, this should never happen ;-) - throw new IllegalStateException("ViewType instance has not been correctly mapped for viewType " - + viewType + " or AutoMap is not active: super() cannot be called."); + if (item == null || !autoMap) { + // If everything has been set properly, this should never happen ;-) + throw new IllegalStateException("ViewType instance not found for viewType " + viewType + + ". Override this method or implement the AutoMap properly."); } if (mInflater == null) { mInflater = LayoutInflater.from(parent.getContext()); @@ -1335,12 +1820,15 @@ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType /** * You can override this method to bind the items into the corresponding ViewHolder from - * inside the Adapter or you can let this method to call the implementation of + * inside the Adapter OR you can let this method to call the implementation of * {@code IFlexible#bindViewHolder()} to bind the item inside itself (AutoMap). + *

HELP: To know how to implement AutoMap for ViewTypes please refer to the + * FlexibleAdapter Wiki Page + * on GitHub. *

{@inheritDoc} * - * @throws IllegalStateException if {@code IFlexible#bindViewHolder()} is not implemented OR - * if {@code super()} is called when AutoMap is not active. + * @throws IllegalStateException if this method is not overridden OR AutoMap has not been + * properly implemented. * @see IFlexible#bindViewHolder(FlexibleAdapter, RecyclerView.ViewHolder, int, List) * @since 5.0.0-b1 */ @@ -1353,50 +1841,42 @@ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { * Same concept of {@code #onBindViewHolder()} but with Payload. *

{@inheritDoc} * - * @throws IllegalStateException if {@code IFlexible#bindViewHolder()} is not implemented OR - * if {@code super()} is called when AutoMap is not active. + * @throws IllegalStateException if this method is not overridden OR AutoMap has not been + * properly implemented. * @see IFlexible#bindViewHolder(FlexibleAdapter, RecyclerView.ViewHolder, int, List) * @see #onBindViewHolder(RecyclerView.ViewHolder, int) * @since 5.0.0-b1 */ @Override - public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) { + public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position, List payloads) { if (DEBUG) { - Log.v(TAG, "onViewBound Holder=" + holder.getClass().getSimpleName() + - " position=" + position + - " itemId=" + holder.getItemId() + - " layoutPosition=" + holder.getLayoutPosition()); - } - //This check is necessary when using Expandable items, it helps to optimize binding. - // Expandable items can lay out of the screen during the initialization/refresh - // as soon as they are expanded one by one. -// if (holder.getLayoutPosition() > mRecyclerView.getChildCount()) { -// Log.w(TAG, "onViewBound Skip binding for view out of screen " + -// holder.getLayoutPosition() + "/" + mRecyclerView.getChildCount()); -// return; -// } + Log.v(TAG, "onViewBound Holder=" + getClassName(holder) + " position=" + position + + " itemId=" + holder.getItemId() + " layoutPosition=" + holder.getLayoutPosition()); + } if (!autoMap) { - throw new IllegalStateException("AutoMap is not active: super() cannot be called."); - } - //When user scrolls, this line binds the correct selection status - holder.itemView.setActivated(isSelected(position)); - //Bind the correct view elevation - if (holder instanceof FlexibleViewHolder) { - FlexibleViewHolder flexHolder = (FlexibleViewHolder) holder; - float elevation = flexHolder.getActivationElevation(); - if (holder.itemView.isActivated() && elevation > 0) - ViewCompat.setElevation(holder.itemView, elevation); - else if (elevation > 0)//Leave unaltered the default elevation - ViewCompat.setElevation(holder.itemView, 0); - } - //Bind the item + // If everything has been set properly, this should never happen ;-) + throw new IllegalStateException("AutoMap is not active, this method cannot be called." + + " Override this method or implement the AutoMap properly."); + } + // Bind view activation with current selection + super.onBindViewHolder(holder, position, payloads); + // Bind the item T item = getItem(position); if (item != null) { holder.itemView.setEnabled(item.isEnabled()); item.bindViewHolder(this, holder, position, payloads); + // Avoid to show the double background in case header has transparency + // The visibility will be restored when header is reset in StickyHeaderHelper + if (areHeadersSticky() && !isFastScroll && mStickyHeaderHelper.getStickyPosition() >= 0 && payloads.isEmpty()) { + int headerPos = Utils.findFirstVisibleItemPosition(mRecyclerView.getLayoutManager()) - 1; + if (headerPos == position && isHeader(item)) + holder.itemView.setVisibility(View.INVISIBLE); + } } - //Endless Scroll + // Endless Scroll onLoadMore(position); + // Scroll Animation + animateView(holder, position); } /*------------------------*/ @@ -1404,39 +1884,133 @@ else if (elevation > 0)//Leave unaltered the default elevation /*------------------------*/ /** - * Sets the ProgressItem to be displayed at the end of the list and activate the Loading More - * functionality. + * Evaluates if the Adapter is in Endless Scroll mode. When no more load, this method will + * return {@code false}. To enable again the progress item you MUST be set again. + * + * @return true if the progress item is set, false otherwise + * @see #setEndlessProgressItem(IFlexible) + * @since 5.0.0-rc1 + */ + public boolean isEndlessScrollEnabled() { + return endlessScrollEnabled; + } + + /** + * Provides the current endless page if the page size limit is set, if not set the returned + * value is always 1. + * + * @return the current endless page + * @see #getEndlessPageSize() + * @see #setEndlessPageSize(int) + * @since 5.0.0-rc1 + */ + public int getEndlessCurrentPage() { + return Math.max(1, mEndlessPageSize > 0 ? getMainItemCount() / mEndlessPageSize : 0); + } + + /** + * The current setting for the endless page size limit. + *

Note: This limit is ignored if value is 0.

+ * + * @return the page size limit, if the limit is not set, 0 is returned. + * @see #getEndlessCurrentPage() + * @see #setEndlessPageSize(int) + * @since 5.0.0-rc1 + */ + public int getEndlessPageSize() { + return mEndlessPageSize; + } + + /** + * Sets the limit to automatically disable the endless feature when coming items size is less + * than the page size. + *

When endless feature is disabled a {@link #notifyItemChanged(int, Object)} with payload + * {@link Payload#NO_MORE_LOAD} will be triggered on the progressItem, so you can display a + * message or change the views in this item.

+ * Default value is 0 (limit is ignored). + * + * @param endlessPageSize the size limit for each page + * @return this Adapter, so the call can be chained + * @since 5.0.0-rc1 + */ + public FlexibleAdapter setEndlessPageSize(@IntRange(from = 0) int endlessPageSize) { + mEndlessPageSize = endlessPageSize; + return this; + } + + /** + * The current setting for the endless target item count limit. + *

Note: This limit is ignored if value is 0.

+ * + * @return the target items count limit, if the limit is not set, 0 is returned. + * @see #setEndlessTargetCount(int) + * @since 5.0.0-rc1 + */ + public int getEndlessTargetCount() { + return mEndlessTargetCount; + } + + /** + * Sets the limit to automatically disable the endless feature when the total items count in + * the Adapter is equals or bigger than the target count. + *

When endless feature is disabled a {@link #notifyItemChanged(int, Object)} with payload + * {@link Payload#NO_MORE_LOAD} will be triggered on the progressItem, so you can display a + * message or change the views in this item.

+ * Default value is 0 (limit is ignored). + * + * @param endlessTargetCount the total items count limit + * @return this Adapter, so the call can be chained + * @see #getEndlessTargetCount() + * @since 5.0.0-rc1 + */ + public FlexibleAdapter setEndlessTargetCount(@IntRange(from = 0) int endlessTargetCount) { + mEndlessTargetCount = endlessTargetCount; + return this; + } + + /** + * Sets the progressItem to be displayed at the end of the list and activate the Loading More + * feature. *

Using this method, the {@link EndlessScrollListener} won't be called so that you can * handle a click event to load more items upon a user request.

* To correctly implement "Load more upon a user request" check the Wiki page of this library. * * @param progressItem the item representing the progress bar * @return this Adapter, so the call can be chained + * @see #isEndlessScrollEnabled() * @see #setEndlessScrollListener(EndlessScrollListener, IFlexible) * @since 5.0.0-b8 */ - public FlexibleAdapter setEndlessProgressItem(@NonNull T progressItem) { + public FlexibleAdapter setEndlessProgressItem(@Nullable T progressItem) { + endlessScrollEnabled = progressItem != null; if (progressItem != null) { setEndlessScrollThreshold(mEndlessScrollThreshold); - progressItem.setEnabled(false); mProgressItem = progressItem; + if (DEBUG) { + Log.i(TAG, "Set progressItem=" + getClassName(progressItem)); + Log.i(TAG, "Enabled EndlessScrolling"); + } + } else if (DEBUG) { + Log.i(TAG, "Disabled EndlessScrolling"); } return this; } /** - * Sets the ProgressItem to be displayed at the end of the list and Sets the callback to - * automatically load more items asynchronously (no further user action is needed but the - * scroll). + * Sets the progressItem to be displayed at the end of the list and Sets the callback to + * automatically load more items asynchronously(your duty) (no further user action is needed + * but the scroll). * * @param endlessScrollListener the callback to invoke the asynchronous loading * @param progressItem the item representing the progress bar * @return this Adapter, so the call can be chained + * @see #isEndlessScrollEnabled() * @see #setEndlessProgressItem(IFlexible) * @since 5.0.0-b6 */ public FlexibleAdapter setEndlessScrollListener(@Nullable EndlessScrollListener endlessScrollListener, @NonNull T progressItem) { + if (DEBUG) Log.i(TAG, "Set endlessScrollListener=" + getClassName(endlessScrollListener)); mEndlessScrollListener = endlessScrollListener; return setEndlessProgressItem(progressItem); } @@ -1450,37 +2024,57 @@ public FlexibleAdapter setEndlessScrollListener(@Nullable EndlessScrollListener * @since 5.0.0-b6 */ public FlexibleAdapter setEndlessScrollThreshold(@IntRange(from = 1) int thresholdItems) { - //Increase visible threshold based on number of columns + // Increase visible threshold based on number of columns if (mRecyclerView != null) { - int spanCount = getSpanCount(mRecyclerView.getLayoutManager()); + int spanCount = Utils.getSpanCount(mRecyclerView.getLayoutManager()); thresholdItems = thresholdItems * spanCount; } mEndlessScrollThreshold = thresholdItems; + if (DEBUG) Log.i(TAG, "Set endlessScrollThreshold=" + mEndlessScrollThreshold); return this; } /** * This method is called automatically if METHOD A is implemented. If instead you chose the - * classic way (METHOD B) to bind the items, you have to manually call this method at the end - * of {@code onBindViewHolder()}. + * classic way (METHOD B) to bind the items, you have to manually call this method + * at the end of {@code onBindViewHolder()}. * * @param position the current binding position + * @since 5.0.0-b6 + *
5.0.0-rc1 Added limits check and progressItem with Scrollable Footers */ protected void onLoadMore(int position) { - if (mProgressItem != null && !mLoading - && position >= getItemCount() - mEndlessScrollThreshold - && getGlobalPositionOf(mProgressItem) < 0) { - mLoading = true; - mRecyclerView.post(new Runnable() { - @Override - public void run() { - mItems.add(mProgressItem); - notifyItemInserted(getItemCount()); - if (mEndlessScrollListener != null) - mEndlessScrollListener.onLoadMore(); + // Skip everything when loading more is unused OR currently loading + if (!isEndlessScrollEnabled() || endlessLoading) + return; + + // Check next loading threshold + int threshold = getItemCount() - mEndlessScrollThreshold - (hasSearchText() ? 0 : mScrollableFooters.size()); + if (position == getGlobalPositionOf(mProgressItem) || position < threshold) { + return; + } else if (DEBUG) { + Log.v(TAG, "onLoadMore loading=" + endlessLoading + ", position=" + position + + ", itemCount=" + getItemCount() + ", threshold=" + mEndlessScrollThreshold + + ", inside the threshold? " + (position >= getItemCount() - mEndlessScrollThreshold - (hasSearchText() ? 0 : mScrollableFooters.size()))); + } + // Load more if not loading and inside the threshold + endlessLoading = true; + // Insertion is in post, as suggested by Android because: java.lang.IllegalStateException: + // Cannot call notifyItemInserted while RecyclerView is computing a layout or scrolling + mHandler.post(new Runnable() { + @Override + public void run() { + // Clear previous delayed message + mHandler.removeMessages(LOAD_MORE_COMPLETE); + // Add progressItem if not already shown + addScrollableFooter(mProgressItem); + // When the listener is not set, loading more is called upon a user request + if (mEndlessScrollListener != null) { + if (DEBUG) Log.d(TAG, "onLoadMore invoked!"); + mEndlessScrollListener.onLoadMore(getMainItemCount(), getEndlessCurrentPage()); } - }); - } + } + }); } /** @@ -1497,61 +2091,78 @@ public void onLoadMoreComplete(@Nullable List newItems) { } /** - * To call to complete the action of the Loading more items. + * Call this method to complete the action of the Loading more items. *

When noMoreLoad OR onError OR onCancel, pass empty list or null to hide the - * progressItem.

+ * progressItem. When limits are set, endless feature will be disabled. To enable + * again call {@link #setEndlessProgressItem(IFlexible)}.

* Optionally you can pass a delay time to still display the item with the latest information - * inside. The message has to be handled inside the bindViewHolder of the item. + * inside. The message has to be handled inside the {@code bindViewHolder} of the item. + *

A {@link #notifyItemChanged(int, Object)} with payload {@link Payload#NO_MORE_LOAD} + * will be triggered on the progressItem, so you can display a message or change the views in + * this item.

* * @param newItems the list of the new items, can be empty or null * @param delay the delay used to remove the progress item or -1 to disable the - * loading forever and to keep the progress item. + * loading forever and to keep the progress item visible. * @since 5.0.0-b8 + *
5.0.0-rc1 Added limits check and changed progressItem to Scrollable Footer */ + //TODO: Endless Top Scrolling public void onLoadMoreComplete(@Nullable List newItems, @IntRange(from = -1) long delay) { - //Handling the delay - if (delay < 0) { - //Disable the Endless functionality and keep the item - mProgressItem = null; - } else { - //Delete the progress item with delay + // 1. Calculate new items count + int newItemsSize = newItems == null ? 0 : newItems.size(); + int totalItemCount = newItemsSize + getMainItemCount(); + // 2. Add any new items + if (newItemsSize > 0) { + if (DEBUG) + Log.v(TAG, "onLoadMore performing adding " + newItemsSize + " new items on Page=" + getEndlessCurrentPage()); + //TODO: 0 + headers for Endless Top Scrolling + addItems(getGlobalPositionOf(mProgressItem), newItems); + } + // 3. Check if features are enabled and the limits have been reached + if (mEndlessPageSize > 0 && newItemsSize < mEndlessPageSize || // Is feature enabled and Not enough items? + mEndlessTargetCount > 0 && totalItemCount >= mEndlessTargetCount) { // Is feature enabled and Max limit has been reached? + // Disable the EndlessScroll feature + setEndlessProgressItem(null); + } + // 4. Remove the progressItem if needed + if (delay > 0 && (newItemsSize == 0 || !isEndlessScrollEnabled())) { + if (DEBUG) + Log.v(TAG, "onLoadMore enqueued removing progressItem (" + delay + "ms)"); mHandler.sendEmptyMessageDelayed(LOAD_MORE_COMPLETE, delay); + } else if (isEndlessScrollEnabled()) { + hideProgressItem(); } - //Add the new items or reset the loading status - if (newItems != null && newItems.size() > 0) { - if (DEBUG) - Log.i(TAG, "onLoadMore performing adding " + newItems.size() + " new Items!"); - addItems(getItemCount(), newItems); - //Reset OnLoadMore delayed - mHandler.sendEmptyMessageDelayed(LOAD_MORE_RESET, 200L); - } else { - noMoreLoad(); + // 5. Reset the loading status + endlessLoading = false; + // 6. Eventually notify noMoreLoad + if (newItemsSize == 0 || !isEndlessScrollEnabled()) { + noMoreLoad(newItemsSize); } } /** * Called when loading more should continue. */ - private void deleteProgressItem() { - int progressPosition = getGlobalPositionOf(mProgressItem); - if (progressPosition >= 0) { - mItems.remove(mProgressItem); - notifyItemRemoved(progressPosition); + private void hideProgressItem() { + int positionToNotify = getGlobalPositionOf(mProgressItem); + if (positionToNotify >= 0) { + if (DEBUG) Log.v(TAG, "onLoadMore remove progressItem"); + removeScrollableFooter(mProgressItem); } } /** * Called when no more items are loaded. */ - private void noMoreLoad() { - if (DEBUG) Log.v(TAG, "onLoadMore noMoreLoad!"); - notifyItemChanged(getItemCount() - 1, Payload.NO_MORE_LOAD); - //Reset OnLoadMore delayed - mHandler.sendEmptyMessageDelayed(LOAD_MORE_RESET, 200L); - } - - private void resetOnLoadMore() { - mLoading = false; + private void noMoreLoad(int newItemsSize) { + if (DEBUG) Log.i(TAG, "noMoreLoad!"); + int positionToNotify = getGlobalPositionOf(mProgressItem); + if (positionToNotify >= 0) + notifyItemChanged(positionToNotify, Payload.NO_MORE_LOAD); + if (mEndlessScrollListener != null) { + mEndlessScrollListener.noMoreLoad(newItemsSize); + } } /*--------------------*/ @@ -1559,7 +2170,7 @@ private void resetOnLoadMore() { /*--------------------*/ /** - * @return true if autoCollapseOnExpand is enabled, false otherwise + * @return true if {@code collapseOnExpand} is enabled, false otherwise * @since 5.0.0-b8 */ public boolean isAutoCollapseOnExpand() { @@ -1567,20 +2178,21 @@ public boolean isAutoCollapseOnExpand() { } /** - * Automatically collapse all previous expanded parents before expand the clicked parent. - *

Default value is disabled.

+ * Automatically collapse all previous expanded parents before expand the new clicked parent. + *

Default value is {@code false} (disabled).

* * @param collapseOnExpand true to collapse others items, false to just expand the current * @return this Adapter, so the call can be chained * @since 5.0.0-b1 */ public FlexibleAdapter setAutoCollapseOnExpand(boolean collapseOnExpand) { + if (DEBUG) Log.i(TAG, "Set autoCollapseOnExpand=" + collapseOnExpand); this.collapseOnExpand = collapseOnExpand; return this; } /** - * @return true if autoScrollOnExpand is enabled, false otherwise + * @return true if {@code scrollOnExpand} is enabled, false otherwise * @since 5.0.0-b8 */ public boolean isAutoScrollOnExpand() { @@ -1589,15 +2201,16 @@ public boolean isAutoScrollOnExpand() { /** * Automatically scroll the clicked expandable item to the first visible position.
- * Default value is disabled. - *

This works ONLY in combination with {@link SmoothScrollLinearLayoutManager} or with - * {@link SmoothScrollGridLayoutManager}.

+ *

Default value is {@code false} (disabled).

+ * This works ONLY in combination with {@link SmoothScrollLinearLayoutManager} or with + * {@link SmoothScrollGridLayoutManager}. * * @param scrollOnExpand true to enable automatic scroll, false to disable * @return this Adapter, so the call can be chained * @since 5.0.0-b1 */ public FlexibleAdapter setAutoScrollOnExpand(boolean scrollOnExpand) { + if (DEBUG) Log.i(TAG, "Set setAutoScrollOnExpand=" + scrollOnExpand); this.scrollOnExpand = scrollOnExpand; return this; } @@ -1636,23 +2249,24 @@ public boolean isExpandable(@NonNull T item) { } /** - * @return the level of the minium collapsible level used in MultiLevel expandable + * @return the level of the minimum collapsible level used in MultiLevel expandable * @since 5.0.0-b6 */ public int getMinCollapsibleLevel() { - return minCollapsibleLevel; + return mMinCollapsibleLevel; } /** * Sets the minimum level which all sub expandable items will be collapsed too. - *

Default value is {@link #minCollapsibleLevel} (All levels including 0).

+ *

Default value is {@link #mMinCollapsibleLevel} (All levels including 0).

* * @param minCollapsibleLevel the minimum level to auto-collapse sub expandable items * @return this Adapter, so the call can be chained * @since 5.0.0-b6 */ public FlexibleAdapter setMinCollapsibleLevel(int minCollapsibleLevel) { - this.minCollapsibleLevel = minCollapsibleLevel; + if (DEBUG) Log.i(TAG, "Set minCollapsibleLevel=" + minCollapsibleLevel); + this.mMinCollapsibleLevel = minCollapsibleLevel; return this; } @@ -1669,7 +2283,7 @@ public boolean hasSubItems(@NonNull IExpandable expandable) { } /** - * Retrieves the parent of a child. + * Retrieves the parent of a child for the provided position. *

Only for a real child of an expanded parent.

* * @param position the position of the child item @@ -1687,7 +2301,7 @@ public IExpandable getExpandableOf(@IntRange(from = 0) int position) { * @param child the child item * @return the parent of this child item or null if item has no parent * @see #getExpandablePositionOf(IFlexible) - * @see #getRelativePositionOf(IFlexible) + * @see #getSubPositionOf(IFlexible) * @since 5.0.0-b1 */ public IExpandable getExpandableOf(@NonNull T child) { @@ -1714,7 +2328,7 @@ public IExpandable getExpandableOf(@NonNull T child) { * @param child the child item * @return the parent position of this child item or -1 if not found * @see #getExpandableOf(IFlexible) - * @see #getRelativePositionOf(IFlexible) + * @see #getSubPositionOf(IFlexible) * @since 5.0.0-b1 */ public int getExpandablePositionOf(@NonNull T child) { @@ -1722,34 +2336,50 @@ public int getExpandablePositionOf(@NonNull T child) { } /** - * Provides the list where the child currently lays. + * Retrieves the position of a child item in the list where it lays. + *

Only for a real child of an expanded parent.

* * @param child the child item - * @return the list of the child element, or a new list if item + * @return the position in the parent or -1 if the child is a parent itself or not found * @see #getExpandableOf(IFlexible) * @see #getExpandablePositionOf(IFlexible) - * @see #getRelativePositionOf(IFlexible) - * @see #getExpandedItems() * @since 5.0.0-b1 + * @deprecated Use {@link #getSubPositionOf(IFlexible)} */ - @NonNull - public List getSiblingsOf(@NonNull T child) { - IExpandable expandable = getExpandableOf(child); - return expandable != null ? expandable.getSubItems() : new ArrayList<>(); + @Deprecated + public int getRelativePositionOf(@NonNull T child) { + return getSiblingsOf(child).indexOf(child); + } + + /** + * Retrieves the position of a child item in the list where it lays. + *

Only for a real child of an expanded parent.

+ * + * @param child the child item + * @return the position in the parent or -1 if the child is a parent itself or not found + * @see #getExpandableOf(IFlexible) + * @see #getExpandablePositionOf(IFlexible) + * @since 5.0.0-b1 + */ + public int getSubPositionOf(@NonNull T child) { + return getSiblingsOf(child).indexOf(child); } /** - * Retrieves the position of a child in the list where it lays. - *

Only for a real child of an expanded parent.

+ * Provides the full sub list where the child currently lays. * * @param child the child item - * @return the position in the parent or -1 if, child is a parent itself or not found + * @return the list of the child element, or an empty list if the child item has no parent * @see #getExpandableOf(IFlexible) * @see #getExpandablePositionOf(IFlexible) + * @see #getSubPositionOf(IFlexible) + * @see #getExpandedItems() * @since 5.0.0-b1 */ - public int getRelativePositionOf(@NonNull T child) { - return getSiblingsOf(child).indexOf(child); + @NonNull + public List getSiblingsOf(@NonNull T child) { + IExpandable expandable = getExpandableOf(child); + return expandable != null ? expandable.getSubItems() : new ArrayList<>(); } /** @@ -1781,15 +2411,16 @@ public List getExpandedItems() { @NonNull public List getExpandedPositions() { List expandedPositions = new ArrayList<>(); - for (int i = 0; i < mItems.size() - 1; i++) { - if (isExpanded(mItems.get(i))) - expandedPositions.add(i); + int startPosition = Math.max(0, mScrollableHeaders.size() - 1); + int endPosition = getItemCount() - mScrollableFooters.size() - 1; + for (int i = startPosition; i < endPosition; i++) { + if (isExpanded(mItems.get(i))) expandedPositions.add(i); } return expandedPositions; } /** - * Expands an item that is IExpandable type, not yet expanded and if has subItems. + * Expands an item that is {@code IExpandable} type, not yet expanded and if has subItems. *

If configured, automatic smooth scroll will be performed when necessary.

* * @param position the position of the item to expand @@ -1826,8 +2457,8 @@ public int expand(T item) { * initially expanded.

* WARNING! *
Expanded status is ignored if {@code init = true}: it will always attempt to expand - * the item: If subItems are already visible and the new item has status expanded, the - * subItems will appear duplicated and the automatic smooth scroll will be skipped! + * the item: If subItems are already visible and the new item has status expanded, the + * subItems will appear duplicated(!) and the automatic smooth scroll will be skipped! * * @param item the item to expand, must be an Expandable and present in the list * @param init true to initially expand item @@ -1853,22 +2484,22 @@ private int expand(int position, boolean expandAll, boolean init) { " expanded " + expandable.isExpanded()); return 0; } -// if (DEBUG && !init) { -// Log.v(TAG, "Request to Expand on position=" + position + -// " expanded=" + expandable.isExpanded() + -// " anyParentSelected=" + parentSelected); -// } + if (DEBUG && !init && !expandAll) { + Log.v(TAG, "Request to Expand on position=" + position + + " expanded=" + expandable.isExpanded() + + " anyParentSelected=" + parentSelected); + } int subItemsCount = 0; if (init || !expandable.isExpanded() && - (!parentSelected || expandable.getExpansionLevel() <= selectedLevel)) { + (!parentSelected || expandable.getExpansionLevel() <= mSelectedLevel)) { - //Collapse others expandable if configured so Skip when expanding all is requested - //Fetch again the new position after collapsing all!! - if (collapseOnExpand && !expandAll && collapseAll(minCollapsibleLevel) > 0) { + // Collapse others expandable if configured so Skip when expanding all is requested + // Fetch again the new position after collapsing all!! + if (collapseOnExpand && !expandAll && collapseAll(mMinCollapsibleLevel) > 0) { position = getGlobalPositionOf(item); } - //Every time an expansion is requested, subItems must be taken from the + // Every time an expansion is requested, subItems must be taken from the // original Object and without the subItems marked hidden (removed) List subItems = getExpandableList(expandable); mItems.addAll(position + 1, subItems); @@ -1876,31 +2507,48 @@ private int expand(int position, boolean expandAll, boolean init) { //Save expanded state expandable.setExpanded(true); - //Automatically smooth scroll the current expandable item to show as much + // Automatically smooth scroll the current expandable item to show as much // children as possible if (!init && scrollOnExpand && !expandAll) { autoScrollWithDelay(position, subItemsCount, 150L); } - //Expand! + // Expand! notifyItemRangeInserted(position + 1, subItemsCount); - //Show also the headers of the subItems + // Show also the headers of the subItems if (!init && headersShown) { int count = 0; for (T subItem : subItems) { if (showHeaderOf(position + (++count), subItem, false)) count++; } } + + // Expandable as a Scrollable Header/Footer + if (!expandSHF(mScrollableHeaders, expandable)) + expandSHF(mScrollableFooters, expandable); + if (DEBUG) { - Log.i(TAG, (init ? "Initially expanded " : "Expanded ") + + Log.v(TAG, (init ? "Initially expanded " : "Expanded ") + subItemsCount + " subItems on position=" + position); } } return subItemsCount; } + private boolean expandSHF(List scrollables, IExpandable expandable) { + int index = scrollables.indexOf(expandable); + if (index >= 0) { + if (index + 1 < scrollables.size()) { + return scrollables.addAll(index + 1, expandable.getSubItems()); + } else { + return scrollables.addAll(expandable.getSubItems()); + } + } + return false; + } + /** - * Expands all IExpandable items with minimum of level {@link #minCollapsibleLevel}. + * Expands all IExpandable items with minimum of level {@link #mMinCollapsibleLevel}. * * @return the number of parent successfully expanded * @see #expandAll(int) @@ -1908,7 +2556,7 @@ private int expand(int position, boolean expandAll, boolean init) { * @since 5.0.0-b1 */ public int expandAll() { - return expandAll(minCollapsibleLevel); + return expandAll(mMinCollapsibleLevel); } /** @@ -1922,12 +2570,14 @@ public int expandAll() { */ public int expandAll(int level) { int expanded = 0; - //More efficient if we expand from First expandable position - for (int i = 0; i < mItems.size(); i++) { + // More efficient if we expand from First expandable position + int startPosition = Math.max(0, mScrollableHeaders.size() - 1); + for (int i = startPosition; i < (getItemCount() - mScrollableFooters.size()); i++) { T item = getItem(i); if (isExpandable(item)) { IExpandable expandable = (IExpandable) item; if (expandable.getExpansionLevel() <= level && expand(i, true, false) > 0) { + i += expandable.getSubItems().size(); expanded++; } } @@ -1936,8 +2586,8 @@ public int expandAll(int level) { } /** - * Collapses an IExpandable item that is already expanded, if no subItem is selected. - *

Multilevel behaviour: all IExpandable subItem, that are expanded, are recursively + * Collapses an {@code IExpandable} item that is already expanded, if no subItem is selected. + *

Multilevel behaviour: all {@code IExpandable} subItem, that are expanded, are recursively * collapsed.

* * @param position the position of the item to collapse @@ -1950,11 +2600,11 @@ public int collapse(@IntRange(from = 0) int position) { if (!isExpandable(item)) return 0; IExpandable expandable = (IExpandable) item; - //Take the current subList (will improve the performance when collapseAll) + // Take the current subList (will improve the performance when collapseAll) List subItems = getExpandableList(expandable); int subItemsCount = subItems.size(), recursiveCount = 0; - if (DEBUG && hashItems == null) { + if (DEBUG && mHashItems == null) { Log.v(TAG, "Request to Collapse on position=" + position + " expanded=" + expandable.isExpanded() + " hasSubItemsSelected=" + hasSubItemsSelected(position, subItems)); @@ -1963,29 +2613,38 @@ public int collapse(@IntRange(from = 0) int position) { if (expandable.isExpanded() && subItemsCount > 0 && (!hasSubItemsSelected(position, subItems) || getPendingRemovedItem(item) != null)) { - //Recursive collapse of all sub expandable + // Recursive collapse of all sub expandable recursiveCount = recursiveCollapse(position + 1, subItems, expandable.getExpansionLevel()); - //Use HashSet (will improve the performance when collapseAll) - if (hashItems != null) hashItems.removeAll(subItems); + // Use HashSet (will improve the performance when collapseAll) + if (mHashItems != null) mHashItems.removeAll(subItems); else mItems.removeAll(subItems); subItemsCount = subItems.size(); - //Save expanded state + // Save expanded state expandable.setExpanded(false); - //Collapse! + // Collapse! notifyItemRangeRemoved(position + 1, subItemsCount); - //Hide also the headers of the subItems + // Hide also the headers of the subItems if (headersShown && !isHeader(item)) { for (T subItem : subItems) { hideHeaderOf(subItem); } } + + // Expandable as a Scrollable Header/Footer + if (!collapseSHF(mScrollableHeaders, expandable)) + collapseSHF(mScrollableFooters, expandable); + if (DEBUG) Log.v(TAG, "Collapsed " + subItemsCount + " subItems on position " + position); } return subItemsCount + recursiveCount; } + private boolean collapseSHF(List scrollables, IExpandable expandable) { + return scrollables.contains(expandable) && scrollables.removeAll(expandable.getSubItems()); + } + private int recursiveCollapse(int startPosition, List subItems, int level) { int collapsed = 0; for (int i = 0; i < subItems.size(); i++) { @@ -2002,7 +2661,7 @@ private int recursiveCollapse(int startPosition, List subItems, int level) { } /** - * Collapses all expandable items with the minimum level of {@link #minCollapsibleLevel}. + * Collapses all expandable items with the minimum level of {@link #mMinCollapsibleLevel}. * * @return the number of parent successfully collapsed * @see #collapse(int) @@ -2011,7 +2670,7 @@ private int recursiveCollapse(int startPosition, List subItems, int level) { * @since 5.0.0-b1 */ public int collapseAll() { - return collapseAll(minCollapsibleLevel); + return collapseAll(mMinCollapsibleLevel); } /** @@ -2023,10 +2682,11 @@ public int collapseAll() { * @since 5.0.0-b6 */ public int collapseAll(int level) { - hashItems = new LinkedHashSet<>(mItems); + // Use hashSet to increase performance while removing subItems + mHashItems = new LinkedHashSet<>(mItems); int collapsed = recursiveCollapse(0, mItems, level); - mItems = new ArrayList<>(hashItems); - hashItems = null; + mItems = new ArrayList<>(mHashItems); + mHashItems = null; return collapsed; } @@ -2035,7 +2695,7 @@ public int collapseAll(int level) { /*----------------*/ /** - * Updates/Rebounds the ItemView corresponding to the current position of that item, with + * Updates/Rebounds the itemView corresponding to the current position of that item, with * the new content provided. * * @param item the item with the new content @@ -2049,7 +2709,7 @@ public void updateItem(@NonNull T item, @Nullable Object payload) { } /** - * Updates/Rebounds the ItemView corresponding to the provided position with the new + * Updates/Rebounds the itemView corresponding to the provided position with the new * provided content. Use {@link #updateItem(IFlexible, Object)} if the new content should * be bound on the same position. * @@ -2062,7 +2722,8 @@ public void updateItem(@NonNull T item, @Nullable Object payload) { */ public void updateItem(@IntRange(from = 0) int position, @NonNull T item, @Nullable Object payload) { - if (position < 0 || position >= mItems.size()) { + int itemCount = getItemCount(); + if (position < 0 || position >= itemCount) { Log.e(TAG, "Cannot updateItem on position out of OutOfBounds!"); return; } @@ -2076,7 +2737,7 @@ public void updateItem(@IntRange(from = 0) int position, @NonNull T item, /*----------------*/ /** - * Inserts the given Item at desired position or Add Item at last position with a delay + * Inserts the given item at desired position or Add item at last position with a delay * and auto-scroll to the position. *

Scrolling animation is automatically preserved, meaning that, notification for animation * is ignored.

@@ -2098,89 +2759,106 @@ public void addItemWithDelay(@IntRange(from = 0) final int position, @NonNull fi mHandler.postDelayed(new Runnable() { @Override public void run() { - setAnimate(true); - if (addItem(position, item) && scrollToPosition && mRecyclerView != null) { - mRecyclerView.smoothScrollToPosition( - Math.min(Math.max(0, position), getItemCount() - 1)); - } - setAnimate(false); + if (addItem(position, item) && scrollToPosition) performScroll(position); } }, delay); } /** - * Inserts the given item in the internal list at the specified position or Adds the item - * at last position. + * Simply append the provided item to the end of the list. + *

Convenience method of {@link #addItem(int, IFlexible)} with + * {@code position = getMainItemCount()}.

+ * + * @param item the item to add + * @return true if the internal list was successfully modified, false otherwise + */ + public boolean addItem(@NonNull T item) { + return addItem(getItemCount(), item); + } + + /** + * Inserts the given item at the specified position or Adds the item to the end of the list + * (no matters if the new position is out of bounds!). * - * @param position position of the item to add + * @param position position inside the list, if negative, items will be added to the end * @param item the item to add * @return true if the internal list was successfully modified, false otherwise - * @see #addItemWithDelay(int, IFlexible, long, boolean) + * @see #addItem(IFlexible) * @see #addItems(int, List) * @see #addSubItems(int, int, IExpandable, List, boolean, Object) + * @see #addItemWithDelay(int, IFlexible, long, boolean) * @since 1.0.0 */ public boolean addItem(@IntRange(from = 0) int position, @NonNull T item) { if (item == null) { - Log.e(TAG, "No items to add!"); + Log.e(TAG, "addItem No items to add!"); return false; } if (DEBUG) Log.v(TAG, "addItem delegates addition to addItems!"); - List items = new ArrayList<>(1); - items.add(item); - return addItems(position, items); + return addItems(position, Collections.singletonList(item)); } /** - * Inserts a set of items in the internal list at specified position or Adds the items - * at last position. - *

NOTE: When all headers are shown, the header (if exists) of this item will be - * shown as well, unless it's already shown, so it will not be shown twice.

+ * Inserts a set of items at specified position or Adds the items to the end of the list + * (no matters if the new position is out of bounds!). + *

Note: + *
- When all headers are shown, if exists, the header of this item will be shown as well, + * unless it's already shown, so it won't be shown twice. + *
- Items will be always added between any scrollable header and footers.

* - * @param position position inside the list, -1 to add the set the end of the list - * @param items the items to add + * @param position position inside the list, if negative, items will be added to the end + * @param items the set of items to add * @return true if the internal list was successfully modified, false otherwise + * @see #addItem(IFlexible) * @see #addItem(int, IFlexible) * @see #addSubItems(int, int, IExpandable, List, boolean, Object) * @since 5.0.0-b1 */ public boolean addItems(@IntRange(from = 0) int position, @NonNull List items) { - if (position < 0) { - Log.e(TAG, "Cannot addItems on negative position!"); - return false; - } if (items == null || items.isEmpty()) { - Log.e(TAG, "No items to add!"); + Log.e(TAG, "addItems No items to add!"); return false; } - if (DEBUG) Log.d(TAG, "addItems on position=" + position + " itemCount=" + items.size()); - - //Insert Items - int initialCount = getItemCount(); - if (position < mItems.size()) { - mItems.addAll(position, items); - } else { - mItems.addAll(items); + int initialCount = getMainItemCount();//Count only main items! + if (position < 0) { + Log.w(TAG, "addItems Position is negative! adding items to the end"); + position = initialCount; } - //Notify range addition - notifyItemRangeInserted(position, items.size()); + // Insert the item properly + performInsert(position, items, true); - //Show the headers of these items if all headers are already visible + // Show the headers of these items if all headers are already visible if (headersShown && !recursive) { recursive = true; for (T item : items) showHeaderOf(getGlobalPositionOf(item), item, false);//We have to find the correct position! recursive = false; } - //Call listener to update EmptyView + // Call listener to update EmptyView if (!recursive && mUpdateListener != null && !multiRange && initialCount == 0 && getItemCount() > 0) - mUpdateListener.onUpdateEmptyView(getItemCount()); + mUpdateListener.onUpdateEmptyView(getMainItemCount()); return true; } + private void performInsert(int position, List items, boolean notify) { + int itemCount = getItemCount(); + if (position < itemCount) { + mItems.addAll(position, items); + } else { + mItems.addAll(items); + position = itemCount; + } + // Notify range addition + if (notify) { + if (DEBUG) + Log.d(TAG, "addItems on position=" + position + " itemCount=" + items.size()); + notifyItemRangeInserted(position, items.size()); + } + } + /** * Convenience method of {@link #addSubItem(int, int, IFlexible, boolean, Object)}. - *
In this case parent item will never be notified nor expanded if it is collapsed. + *
In this case parent item will never be expanded if it is collapsed. * * @return true if the internal list was successfully modified, false otherwise * @see #addSubItems(int, int, IExpandable, List, boolean, Object) @@ -2215,11 +2893,8 @@ public boolean addSubItem(@IntRange(from = 0) int parentPosition, Log.e(TAG, "No items to add!"); return false; } - //Build a new list with 1 item to chain the methods of addSubItems - List subItems = new ArrayList<>(1); - subItems.add(item); - //Reuse the method for subItems - return addSubItems(parentPosition, subPosition, subItems, expandParent, payload); + // Build a new list with 1 item to chain the methods of addSubItems + return addSubItems(parentPosition, subPosition, Collections.singletonList(item), expandParent, payload); } /** @@ -2240,7 +2915,10 @@ public boolean addSubItem(@IntRange(from = 0) int parentPosition, * @return true if the internal list was successfully modified, false otherwise * @see #addSubItems(int, int, IExpandable, List, boolean, Object) * @since 5.0.0-b1 + * @deprecated This is like a command to expand an expandable. We should add items to + * expandable ourselves, then call expand! */ + @Deprecated public int addAllSubItemsFrom(@IntRange(from = 0) int parentPosition, @NonNull IExpandable parent, boolean expandParent, @Nullable Object payload) { @@ -2274,7 +2952,7 @@ public boolean addSubItems(@IntRange(from = 0) int parentPosition, IExpandable expandable = (IExpandable) parent; return addSubItems(parentPosition, subPosition, expandable, items, expandParent, payload); } - Log.e(TAG, "Passed parentPosition doesn't belong to an Expandable item!"); + Log.e(TAG, "Provided parentPosition doesn't belong to an Expandable item!"); return false; } @@ -2282,9 +2960,9 @@ public boolean addSubItems(@IntRange(from = 0) int parentPosition, * Adds new subItems on the specified parent item, to the internal list. *

In order to add subItems, the following condition must be satisfied: *
- The item resulting from the parent position is actually an {@link IExpandable}.

- * Optionally the parent can be expanded and subItems displayed. - *
Optionally you can pass any payload to notify the parent about the change and optimize - * the view binding. + * Optionally, the parent can be expanded and subItems displayed. + *
Optionally, you can pass any payload to notify the parent about the change and + * optimize the view binding. * * @param parentPosition position of the expandable item that shall contain the subItems * @param subPosition the start position in the parent where the new items shall be inserted @@ -2304,16 +2982,16 @@ private boolean addSubItems(@IntRange(from = 0) int parentPosition, @NonNull IExpandable parent, @NonNull List items, boolean expandParent, @Nullable Object payload) { boolean added = false; - //Expand parent if requested and not already expanded + // Expand parent if requested and not already expanded if (expandParent && !parent.isExpanded()) { expand(parentPosition); } - //Notify the adapter of the new addition to display it and animate it. - //If parent is collapsed there's no need to notify about the change. + // Notify the adapter of the new addition to display it and animate it. + // If parent is collapsed there's no need to add sub items. if (parent.isExpanded()) { added = addItems(parentPosition + 1 + Math.max(0, subPosition), items); } - //Notify the parent about the change if requested + // Notify the parent about the change if requested if (payload != null) notifyItemChanged(parentPosition, payload); return added; } @@ -2350,14 +3028,13 @@ public void addSection(@NonNull IHeader header, @Nullable IHeader refHeader) { } /** - * Adds and shows an empty section. - *

The new section is a {@link IHeader} item and the position is calculated after sorting - * the Data Set. - *
- To add Sections to the top, set null the Comparator object or simply call + * Adds and shows an empty section. The new section is a {@link IHeader} item and the + * position is calculated after sorting the data set. + *

- To add Sections to the top, set null the Comparator object or simply call * {@link #addSection(IHeader)}; *
- To add Sections to the bottom or in the middle, implement a Comparator * object able to support all the item types this Adapter is displaying or a - * ClassCastException will be raised. + * ClassCastException will be raised.

* * @param header the section header item to add * @param comparator the criteria to sort the Data Set used to extract the correct position @@ -2423,7 +3100,6 @@ public int addItemToSection(@NonNull ISectionable item, @NonNull IHeader header, else addItem(headerPosition + 1 + index, (T) item); } - //return the position return getGlobalPositionOf(item); } @@ -2431,6 +3107,51 @@ public int addItemToSection(@NonNull ISectionable item, @NonNull IHeader header, /* DELETE ITEMS METHODS */ /*----------------------*/ + /** + * This method clears everything: main items, Scrollable Headers and Footers and + * everything else is displayed. + * + * @see #clearAllBut(Integer...) + * @see #removeRange(int, int) + * @see #removeItemsOfType(Integer...) + * @since 5.0.0-rc1 + */ + public void clear() { + if (DEBUG) Log.d(TAG, "clearAll views"); + removeAllScrollableHeaders(); + removeAllScrollableFooters(); + removeRange(0, getItemCount(), null); + } + + /** + * Clears the Adapter list retaining Scrollable Headers and Footers and all items of the + * type provided as parameter. + *

Note:- This method is opposite of {@link #removeItemsOfType(Integer...)}. + * + * @param viewTypes the viewTypes to retain + * @see #clear() + * @see #removeItems(List) + * @see #removeItemsOfType(Integer...) + * @since 5.0.0-rc1 + */ + public void clearAllBut(Integer... viewTypes) { + List viewTypeList = Arrays.asList(viewTypes); + if (viewTypeList.size() > 0) { + if (DEBUG) Log.d(TAG, "clearAll retaining views " + viewTypeList); + List positionsToRemove = new ArrayList<>(); + int startPosition = Math.max(0, mScrollableHeaders.size() - 1); + int endPosition = getItemCount() - mScrollableFooters.size() - 1; + for (int i = startPosition; i < endPosition; i++) { + if (!viewTypeList.contains(getItemViewType(i))) + positionsToRemove.add(i); + } + // Remove items by ranges + removeItems(positionsToRemove); + } else { + clear(); + } + } + /** * @deprecated Param {@code resetLayoutAnimation} cannot be used anymore. Simply use * {@link #removeItemWithDelay(IFlexible, long, boolean)} @@ -2438,12 +3159,12 @@ public int addItemToSection(@NonNull ISectionable item, @NonNull IHeader header, @Deprecated public void removeItemWithDelay(@NonNull final T item, @IntRange(from = 0) long delay, final boolean permanent, boolean resetLayoutAnimation) { - Log.w(TAG, "Method removeItemWithDelay() with 'resetLayoutAnimation' has been deprecated, param 'resetLayoutAnimation'. Method will be removed with next release!"); + Log.w(TAG, "Method removeItemWithDelay() with 'resetLayoutAnimation' has been deprecated, param 'resetLayoutAnimation'. Method will be removed with final release!"); removeItemWithDelay(item, delay, permanent); } /** - * Removes the given Item after the given delay. + * Removes the given item after the given delay. *

Scrolling animation is automatically preserved, meaning that notification for animation * is ignored.

* @@ -2463,18 +3184,21 @@ public void removeItemWithDelay(@NonNull final T item, @IntRange(from = 0) long mHandler.postDelayed(new Runnable() { @Override public void run() { - setAnimate(true); - boolean tempPermanent = permanentDelete; - if (permanent) permanentDelete = true; - removeItem(getGlobalPositionOf(item)); - permanentDelete = tempPermanent; - setAnimate(false); + performRemove(item, permanent); } }, delay); } + private void performRemove(T item, boolean permanent) { + boolean tempPermanent = permanentDelete; + if (permanent) permanentDelete = true; + removeItem(getGlobalPositionOf(item)); + permanentDelete = tempPermanent; + } + /** - * Convenience method of {@link #removeItem(int, Object)} providing a null payload. + * Convenience method of {@link #removeItem(int, Object)} providing {@link Payload#CHANGE} + * as payload for the parent item. * * @param position the position of item to remove * @see #removeItems(List) @@ -2505,14 +3229,15 @@ public void removeItem(@IntRange(from = 0) int position) { * @since 5.0.0-b1 */ public void removeItem(@IntRange(from = 0) int position, @Nullable Object payload) { - //Request to collapse after the notification of remove range + // Request to collapse after the notification of remove range collapse(position); if (DEBUG) Log.v(TAG, "removeItem delegates removal to removeRange"); removeRange(position, 1, payload); } /** - * Convenience method of {@link #removeItems(List, Object)} providing a null payload. + * Convenience method of {@link #removeItems(List, Object)} providing the default + * {@link Payload#CHANGE} for the parent items. * * @see #removeItem(int) * @see #removeItemsOfType(Integer...) @@ -2526,10 +3251,10 @@ public void removeItems(@NonNull List selectedPositions) { } /** - * Removes a list of items from internal list and notify the change. + * Removes items by ranges and notify the change. *

Every item is retained for an eventual Undo.

- * Optionally you can pass any payload to notify the parent about the change and optimize the - * view binding. + * Optionally you can pass any payload to notify the parent items about the change to + * optimize the view binding. *

This method delegates the removal to removeRange.

* * @param selectedPositions list with item positions to remove @@ -2545,9 +3270,9 @@ public void removeItems(@NonNull List selectedPositions) { public void removeItems(@NonNull List selectedPositions, @Nullable Object payload) { if (DEBUG) Log.v(TAG, "removeItems selectedPositions=" + selectedPositions + " payload=" + payload); - //Check if list is empty + // Check if list is empty if (selectedPositions == null || selectedPositions.isEmpty()) return; - //Reverse-sort the list, start from last position for efficiency + // Reverse-sort the list, start from last position for efficiency Collections.sort(selectedPositions, new Comparator() { @Override public int compare(Integer lhs, Integer rhs) { @@ -2556,7 +3281,7 @@ public int compare(Integer lhs, Integer rhs) { }); if (DEBUG) Log.v(TAG, "removeItems after reverse sort selectedPositions=" + selectedPositions); - //Split the list in ranges + // Split the list in ranges int positionStart = 0, itemCount = 0; int lastPosition = selectedPositions.get(0); multiRange = true; @@ -2565,17 +3290,17 @@ public int compare(Integer lhs, Integer rhs) { itemCount++; // 1 2 3 //2 positionStart = position;//10 9 8 //4 } else { - //Remove range + // Remove range if (itemCount > 0) removeRange(positionStart, itemCount, payload);//8,3 //4,2 positionStart = lastPosition = position;//5 //1 itemCount = 1; } - //Request to collapse after the notification of remove range + // Request to collapse after the notification of remove range collapse(position); } multiRange = false; - //Remove last range + // Remove last range if (itemCount > 0) { removeRange(positionStart, itemCount, payload);//1,1 } @@ -2583,8 +3308,13 @@ public int compare(Integer lhs, Integer rhs) { /** * Selectively removes all items of the type provided as parameter. + *

Note: + *
- This method is opposite of {@link #clearAllBut(Integer...)}. + *
- View types of Scrollable Headers and Footers are ignored!

* * @param viewTypes the viewTypes to remove + * @see #clear() + * @see #clearAllBut(Integer...) * @see #removeItem(int, Object) * @see #removeItems(List) * @see #removeAllSelectedItems() @@ -2593,10 +3323,10 @@ public int compare(Integer lhs, Integer rhs) { public void removeItemsOfType(Integer... viewTypes) { List viewTypeList = Arrays.asList(viewTypes); List itemsToRemove = new ArrayList<>(); - for (int i = mItems.size() - 1; i >= 0; i--) { - //Privilege autoMap if active - if ((autoMap && viewTypeList.contains(mItems.get(i).getLayoutRes())) || - viewTypeList.contains(getItemViewType(i))) + int startPosition = Math.max(0, mScrollableHeaders.size() - 1); + int endPosition = getItemCount() - mScrollableFooters.size() - 1; + for (int i = endPosition; i >= startPosition; i--) { + if (viewTypeList.contains(getItemViewType(i))) itemsToRemove.add(i); } this.removeItems(itemsToRemove); @@ -2606,6 +3336,8 @@ public void removeItemsOfType(Integer... viewTypes) { * Same as {@link #removeRange(int, int, Object)}, but in this case the parent will not be * notified about the change, if children are removed. * + * @see #clear() + * @see #clearAllBut(Integer...) * @see #removeItem(int, Object) * @see #removeItems(List) * @see #removeItemsOfType(Integer...) @@ -2619,8 +3351,8 @@ public void removeRange(@IntRange(from = 0) int positionStart, } /** - * Removes a list of consecutive items from internal list and notify the change. - *

If the item, resulting from the passed position:

+ * Removes a list of consecutive items and notify the change. + *

If the item, resulting from the provided position:

* - is not expandable with no parent, it is removed as usual.
* - is not expandable with a parent, it is removed only if the parent is expanded.
* - is expandable implementing {@link IExpandable}, it is removed as usual, but @@ -2635,12 +3367,13 @@ public void removeRange(@IntRange(from = 0) int positionStart, * @param payload any non-null user object to notify the parent (the payload will be * therefore passed to the bind method of the parent ViewHolder), * pass null to not notify the parent + * @see #clear() + * @see #clearAllBut(Integer...) * @see #removeItem(int, Object) * @see #removeItems(List, Object) * @see #removeRange(int, int) * @see #removeAllSelectedItems(Object) * @see #setPermanentDelete(boolean) - * @see #setRemoveOrphanHeaders(boolean) * @see #setRestoreSelectionOnUndo(boolean) * @see #getDeletedItems() * @see #getDeletedChildren(IExpandable) @@ -2649,6 +3382,7 @@ public void removeRange(@IntRange(from = 0) int positionStart, * @see #emptyBin() * @since 5.0.0-b1 */ + //FIXME: Fix payload message if customized public void removeRange(@IntRange(from = 0) int positionStart, @IntRange(from = 0) int itemCount, @Nullable Object payload) { int initialCount = getItemCount(); @@ -2659,11 +3393,12 @@ public void removeRange(@IntRange(from = 0) int positionStart, return; } - //Handle header linkage - IHeader header = getHeaderOf(getItem(positionStart)); + // Update content of the header linked to first item of the range + T item = getItem(positionStart); + IHeader header = getHeaderOf(item); int headerPosition = getGlobalPositionOf(header); if (header != null && headerPosition >= 0) { - //The header does not represents a group anymore, add it to the Orphan list + // The header does not represents a group anymore, add it to the Orphan list addToOrphanListIfNeeded(header, positionStart, itemCount); notifyItemChanged(headerPosition, payload); } @@ -2671,7 +3406,7 @@ public void removeRange(@IntRange(from = 0) int positionStart, int parentPosition = -1; IExpandable parent = null; for (int position = positionStart; position < positionStart + itemCount; position++) { - T item = getItem(positionStart); + item = getItem(positionStart); if (!permanentDelete) { //When removing a range of children, parent is always the same :-) if (parent == null) parent = getExpandableOf(item); @@ -2682,37 +3417,33 @@ public void removeRange(@IntRange(from = 0) int positionStart, parentPosition = createRestoreSubItemInfo(parent, item, Payload.UNDO); } } - //Change to hidden status for section headers + // Change to hidden status for section headers if (isHeader(item)) { header = (IHeader) item; header.setHidden(true); - } - //If item is a Header, remove linkage from ALL Sectionable items if exist - if (unlinkOnRemoveHeader && isHeader(item)) { - List sectionableList = getSectionItems(header); - for (ISectionable sectionable : sectionableList) { - sectionable.setHeader(null); - if (payload != null) - notifyItemChanged(getGlobalPositionOf(sectionable), Payload.UNLINK); + // If item is a Header, remove linkage from ALL Sectionable items if exist + if (unlinkOnRemoveHeader) { + List sectionableList = getSectionItems(header); + for (ISectionable sectionable : sectionableList) { + sectionable.setHeader(null); + if (payload != null) + notifyItemChanged(getGlobalPositionOf(sectionable), Payload.UNLINK); + } } } - //Remove item from internal list + // Remove item from internal list mItems.remove(positionStart); removeSelection(position); } - //Notify removals - if (parentPosition >= 0) { - //Notify the Children removal only if Parent is expanded - notifyItemRangeRemoved(positionStart, itemCount); - //Notify the Parent about the change if requested - if (payload != null) notifyItemChanged(parentPosition, payload); - } else { - //Notify range removal - notifyItemRangeRemoved(positionStart, itemCount); + // Notify range removal + notifyItemRangeRemoved(positionStart, itemCount); + // Notify the Parent about the change if requested + if (parentPosition >= 0 && payload != null) { + notifyItemChanged(parentPosition, payload); } - //Remove orphan headers + // Remove orphan headers if (removeOrphanHeaders) { for (IHeader orphanHeader : mOrphanHeaders) { headerPosition = getGlobalPositionOf(orphanHeader); @@ -2727,15 +3458,17 @@ public void removeRange(@IntRange(from = 0) int positionStart, mOrphanHeaders.clear(); } - //Update empty view + // Update empty view if (mUpdateListener != null && !multiRange && initialCount > 0 && getItemCount() == 0) - mUpdateListener.onUpdateEmptyView(getItemCount()); + mUpdateListener.onUpdateEmptyView(getMainItemCount()); } /** - * Convenience method to remove all Items that are currently selected. - *

Parent will not be notified about the change, if a child is removed.

+ * Convenience method to remove all items that are currently selected. + *

Parent will not be notified about the change if a child is removed.

* + * @see #clear() + * @see #clearAllBut(Integer...) * @see #removeItem(int) * @see #removeItems(List) * @see #removeRange(int, int) @@ -2748,13 +3481,15 @@ public void removeAllSelectedItems() { } /** - * Convenience method to remove all Items that are currently selected.

- * Optionally the Parent can be notified about the change, if a child is removed, by passing - * any payload. + * Convenience method to remove all items that are currently selected. + *

Optionally, the parent item can be notified about the change if a child is removed, + * by providing any non-null payload.

* * @param payload any non-null user object to notify the parent (the payload will be * therefore passed to the bind method of the parent ViewHolder), * pass null to not notify the parent + * @see #clear() + * @see #clearAllBut(Integer...) * @see #removeItem(int, Object) * @see #removeItems(List, Object) * @see #removeRange(int, int, Object) @@ -2791,6 +3526,7 @@ public boolean isPermanentDelete() { * @since 5.0.0-b6 */ public FlexibleAdapter setPermanentDelete(boolean permanentDelete) { + if (DEBUG) Log.i(TAG, "Set permanentDelete=" + permanentDelete); this.permanentDelete = permanentDelete; return this; } @@ -2818,13 +3554,14 @@ public boolean isRestoreWithSelection() { * @since 5.0.0-b1 */ public FlexibleAdapter setRestoreSelectionOnUndo(boolean restoreSelection) { + if (DEBUG) Log.i(TAG, "Set restoreSelectionOnUndo=" + restoreSelection); this.restoreSelection = restoreSelection; return this; } /** * Restore items just removed. - *

NOTE: If filter is active, only items that match that filter will be shown(restored).

+ *

Note: If filter is active, only items that match that filter will be shown(restored).

* * @see #setRestoreSelectionOnUndo(boolean) * @since 3.0.0 @@ -2834,53 +3571,53 @@ public void restoreDeletedItems() { stopUndoTimer(); multiRange = true; int initialCount = getItemCount(); - //Selection coherence: start from a clear situation + // Selection coherence: start from a clear situation clearSelection(); - //Start from latest item deleted, since others could rely on it + // Start from latest item deleted, since others could rely on it for (int i = mRestoreList.size() - 1; i >= 0; i--) { adjustSelected = false; RestoreInfo restoreInfo = mRestoreList.get(i); - //Notify header if exists + // Notify header if exists IHeader header = getHeaderOf(restoreInfo.item); if (header != null) { notifyItemChanged(getGlobalPositionOf(header), restoreInfo.payload); } if (restoreInfo.relativePosition >= 0) { - //Restore child, if not deleted + // Restore child, if not deleted if (DEBUG) Log.d(TAG, "Restore Child " + restoreInfo); - //Skip subItem addition if filter is active + // Skip subItem addition if filter is active if (hasSearchText() && !filterObject(restoreInfo.item, getSearchText())) continue; - //Check if refItem is shown, if not, show it again + // Check if refItem is shown, if not, show it again if (hasSearchText() && getGlobalPositionOf(getHeaderOf(restoreInfo.item)) == RecyclerView.NO_POSITION) { - //Add parent + subItem + // Add parent + subItem restoreInfo.refItem.setHidden(false); addItem(restoreInfo.getRestorePosition(false), restoreInfo.refItem); addSubItem(restoreInfo.getRestorePosition(true), 0, restoreInfo.item, true, restoreInfo.payload); } else { - //Add subItem + // Add subItem addSubItem(restoreInfo.getRestorePosition(true), restoreInfo.relativePosition, restoreInfo.item, false, restoreInfo.payload); } } else { - //Restore parent or simple item, if not deleted + // Restore parent or simple item, if not deleted if (DEBUG) Log.d(TAG, "Restore Parent " + restoreInfo); - //Skip item addition if filter is active + // Skip item addition if filter is active if (hasSearchText() && !filterExpandableObject(restoreInfo.item)) continue; - //Add header if not visible + // Add header if not visible if (hasSearchText() && hasHeader(restoreInfo.item) && getGlobalPositionOf(getHeaderOf(restoreInfo.item)) == RecyclerView.NO_POSITION) getHeaderOf(restoreInfo.item).setHidden(true); - //Add item + // Add item addItem(restoreInfo.getRestorePosition(false), restoreInfo.item); } - //Item is again visible + // Item is again visible restoreInfo.item.setHidden(false); - //Restore header linkage + // Restore header linkage if (unlinkOnRemoveHeader && isHeader(restoreInfo.item)) { header = (IHeader) restoreInfo.item; List items = getSectionItems(header); @@ -2889,7 +3626,7 @@ public void restoreDeletedItems() { } } } - //Restore selection if requested, before emptyBin + // Restore selection if requested, before emptyBin if (restoreSelection && !mRestoreList.isEmpty()) { if (isExpandable(mRestoreList.get(0).item) || getExpandableOf(mRestoreList.get(0).item) == null) { parentSelected = true; @@ -2904,16 +3641,16 @@ public void restoreDeletedItems() { if (DEBUG) Log.d(TAG, "Selected positions after restore " + getSelectedPositions()); } - //Call listener to update EmptyView + // Call listener to update EmptyView multiRange = false; if (mUpdateListener != null && initialCount == 0 && getItemCount() > 0) - mUpdateListener.onUpdateEmptyView(getItemCount()); + mUpdateListener.onUpdateEmptyView(getMainItemCount()); emptyBin(); } /** - * Clean memory from items just removed. + * Cleans memory from items just removed. *

Note: This method is automatically called after timer is over and after a * restoration.

* @@ -2925,7 +3662,7 @@ public synchronized void emptyBin() { } /** - * Convenience method to start Undo timer with default timeout of 5'' + * Convenience method to start Undo timer with default timeout of 5''. * * @param listener the listener that will be called after timeout to commit the change * @since 3.0.0 @@ -2935,14 +3672,14 @@ public void startUndoTimer(OnDeleteCompleteListener listener) { } /** - * Start Undo timer with custom timeout + * Start Undo timer with custom timeout. * * @param timeout custom timeout * @param listener the listener that will be called after timeout to commit the change * @since 3.0.0 */ public void startUndoTimer(long timeout, OnDeleteCompleteListener listener) { - //Make longer the timer for new coming deleted items + // Make longer the timer for new coming deleted items stopUndoTimer(); mHandler.sendMessageDelayed(Message.obtain(mHandler, CONFIRM_DELETE, listener), timeout > 0 ? timeout : UNDO_TIMEOUT); } @@ -2961,7 +3698,7 @@ protected void stopUndoTimer() { * @return true if the restore list is not empty, false otherwise * @since 4.0.0 */ - public boolean isRestoreInTime() { + public final boolean isRestoreInTime() { return mRestoreList != null && !mRestoreList.isEmpty(); } @@ -2985,7 +3722,7 @@ public List getDeletedItems() { * @return the expandable(parent) of this child, or null if no parent found. * @since 5.0.0-b1 */ - public IExpandable getExpandableOfDeletedChild(T child) { + public final IExpandable getExpandableOfDeletedChild(T child) { for (RestoreInfo restoreInfo : mRestoreList) { if (restoreInfo.item.equals(child) && isExpandable(restoreInfo.refItem)) return (IExpandable) restoreInfo.refItem; @@ -3001,7 +3738,7 @@ public IExpandable getExpandableOfDeletedChild(T child) { * @since 5.0.0-b1 */ @NonNull - public List getDeletedChildren(IExpandable expandable) { + public final List getDeletedChildren(IExpandable expandable) { List deletedChild = new ArrayList<>(); for (RestoreInfo restoreInfo : mRestoreList) { if (restoreInfo.refItem != null && restoreInfo.refItem.equals(expandable) && restoreInfo.relativePosition >= 0) @@ -3020,14 +3757,14 @@ public List getDeletedChildren(IExpandable expandable) { * @since 5.0.0-b1 */ @NonNull - public List getCurrentChildren(@NonNull IExpandable expandable) { - //Check item and subItems existence + public final List getCurrentChildren(@NonNull IExpandable expandable) { + // Check item and subItems existence if (expandable == null || !hasSubItems(expandable)) return new ArrayList<>(); - //Take a copy of the subItems list + // Take a copy of the subItems list List subItems = new ArrayList<>(expandable.getSubItems()); - //Remove all children pending removal + // Remove all children pending removal if (!mRestoreList.isEmpty()) { subItems.removeAll(getDeletedChildren(expandable)); } @@ -3067,6 +3804,7 @@ public String getSearchText() { /** * Sets the new search text. + *

Note: text is always trimmed and to lowercase.

* * @param searchText the new text to filter the items * @since 3.1.0 @@ -3083,7 +3821,7 @@ public void setSearchText(String searchText) { *

If the items have highlighted text, those items must be refreshed in order to change the * text back to normal. This happens systematically when searchText is reduced in length by * the user.

- * The notification is triggered in {@link #animateTo(List)} when new items are not added. + * The notification is triggered in {@link #animateTo(List, Payload)} when new items are not added. *

Default value is {@code false}.

* * @param notifyChange true to trigger {@link #notifyItemChanged(int)} while filtering, @@ -3092,6 +3830,7 @@ public void setSearchText(String searchText) { * @since 5.0.0-b1 */ public final FlexibleAdapter setNotifyChangeOfUnfilteredItems(boolean notifyChange) { + if (DEBUG) Log.i(TAG, "Set notifyChangeOfUnfilteredItems=" + notifyChange); this.notifyChangeOfUnfilteredItems = notifyChange; return this; } @@ -3110,6 +3849,7 @@ public final FlexibleAdapter setNotifyChangeOfUnfilteredItems(boolean notifyChan * @since 5.0.0-b8 */ public final FlexibleAdapter setNotifyMoveOfFilteredItems(boolean notifyMove) { + if (DEBUG) Log.i(TAG, "Set notifyMoveOfFilteredItems=" + notifyMove); this.notifyMoveOfFilteredItems = notifyMove; return this; } @@ -3123,6 +3863,7 @@ public final FlexibleAdapter setNotifyMoveOfFilteredItems(boolean notifyMove) { * @param unfilteredItems the list to filter * @param delay any non-negative delay * @see #filterObject(IFlexible, String) + * @see #onPostFilter() * @see #setAnimateToLimit(int) * @since 5.0.0-b1 *
5.0.0-b8 Synchronization animations limit + AsyncFilter @@ -3136,28 +3877,33 @@ public void filterItems(@NonNull List unfilteredItems, @IntRange(from = 0) lo /** * WATCH OUT! PASS ALWAYS A COPY OF THE ORIGINAL LIST: due to internal * mechanism, items are removed and/or added in order to animate items in the final list. - *

This method filters the provided list with the search text previously set with + *

This method filters the provided list with the searchText previously set with * {@link #setSearchText(String)}.

* Important notes: *
    + *
  1. The Filter is always executed in background, asynchronously. + * The method {@link #onPostFilter()} is called after the filter task is completed.
  2. *
  3. This method calls {@link #filterObject(IFlexible, String)}.
  4. - *
  5. If search text is empty or null, the provided list is the current list.
  6. + *
  7. If searchText is empty or {@code null}, the provided list is the new list plus any + * Scrollable Headers and Footers if existent.
  8. *
  9. Any pending deleted items are always filtered out, but if restored, they will be * displayed according to the current filter and at the right positions.
  10. - *
  11. NEW! Expandable items are picked up and displayed if at least a child is - * collected by the current filter.
  12. - *
  13. NEW! Items are animated thanks to {@link #animateTo(List)} BUT a limit of - * {@value mAnimateToLimit} (default) items is set. NOTE: you can change this limit - * by calling {@link #setAnimateToLimit(int)}. Above this limit {@link #notifyDataSetChanged()} - * will be called to improve performance.
  14. + *
  15. Expandable items are picked up and displayed if at least a child is collected by + * the current filter.
  16. + *
  17. Items are animated thanks to {@link #animateTo(List, Payload)} BUT a limit of + * {@value ANIMATE_TO_LIMIT} (default) items is set. Note: Above this limit, + * {@link #notifyDataSetChanged()} will be called to improve performance. you can change + * this limit by calling {@link #setAnimateToLimit(int)}.
  18. *
* * @param unfilteredItems the list to filter * @see #filterObject(IFlexible, String) + * @see #onPostFilter() * @see #setAnimateToLimit(int) * @since 4.1.0 Created *
5.0.0-b1 Expandable + Child filtering *
5.0.0-b8 Synchronization animations limit + AsyncFilter + *
5.0.0-rc1 Scrollable Headers and Footers adaptation */ public void filterItems(@NonNull List unfilteredItems) { mHandler.removeMessages(FILTER); @@ -3165,19 +3911,19 @@ public void filterItems(@NonNull List unfilteredItems) { } private synchronized void filterItemsAsync(@NonNull List unfilteredItems) { - // NOTE: In case user has deleted some items and he changes or applies a filter while + // Note: In case user has deleted some items and he changes or applies a filter while // deletion is pending (Undo started), in order to be consistent, we need to recalculate // the new position in the new list and finally skip those items to avoid they are shown! - if (DEBUG) Log.i(TAG, "filterItems with searchText=" + mSearchText); + if (DEBUG) Log.i(TAG, "filterItems with searchText=\"" + mSearchText + "\""); List filteredItems = new ArrayList<>(); - filtering = true;//Enable flag: skip adjustPositions! + filtering = true; //Enable flag: skip adjustPositions! - if (hasSearchText()) { + if (hasSearchText() && hasNewSearchText(mSearchText)) { //skip when text is unchanged int newOriginalPosition = -1; for (T item : unfilteredItems) { if (mFilterAsyncTask != null && mFilterAsyncTask.isCancelled()) return; - //Filter header first + // Filter header first T header = (T) getHeaderOf(item); if (headersShown) { if (header != null && filterObject(header, getSearchText()) @@ -3188,7 +3934,7 @@ private synchronized void filterItemsAsync(@NonNull List unfilteredItems) { if (filterExpandableObject(item)) { RestoreInfo restoreInfo = getPendingRemovedItem(item); if (restoreInfo != null) { - //If found, point to the new reference while filtering + // If found, point to the new reference while filtering restoreInfo.filterRefItem = ++newOriginalPosition < filteredItems.size() ? filteredItems.get(newOriginalPosition) : null; } else { @@ -3202,29 +3948,31 @@ private synchronized void filterItemsAsync(@NonNull List unfilteredItems) { item.setHidden(true); } } - } else if (hasNewSearchText(mSearchText)) {//this is better than checking emptiness + } else if (hasNewSearchText(mSearchText)) { //this is better than checking emptiness filteredItems = unfilteredItems; //with no filter if (!mRestoreList.isEmpty()) { for (RestoreInfo restoreInfo : mRestoreList) { - //Clear the refItem generated by the filter + // Clear the refItem generated by the filter restoreInfo.clearFilterRef(); - //Find the real reference + // Find the real reference restoreInfo.refItem = filteredItems.get( Math.max(0, filteredItems.indexOf(restoreInfo.item) - 1)); } - //Deleted items not yet committed should not appear + // Deleted items not yet committed should not appear filteredItems.removeAll(getDeletedItems()); } resetFilterFlags(filteredItems); + restoreScrollableHeadersAndFooters(filteredItems); } - //Reset flags + // Reset flags filtering = false; - //Animate search results only in case of new SearchText + // Animate search results only in case of new SearchText if (hasNewSearchText(mSearchText)) { mOldSearchText = mSearchText; - animateTo(filteredItems); + animateDiff(filteredItems, Payload.FILTER); + //animateTo(filteredItems, Payload.FILTER); } } @@ -3240,36 +3988,36 @@ private synchronized void filterItemsAsync(@NonNull List unfilteredItems) { * @since 5.0.0-b1 */ private boolean filterExpandableObject(T item) { - //Reset expansion flag + // Reset expansion flag boolean filtered = false; if (isExpandable(item)) { IExpandable expandable = (IExpandable) item; - //Save which expandable was originally expanded before filtering it out + // Save which expandable was originally expanded before filtering it out if (expandable.isExpanded()) { if (mExpandedFilterFlags == null) mExpandedFilterFlags = new HashSet<>(); mExpandedFilterFlags.add(expandable); } expandable.setExpanded(false); - //Children scan filter + // Children scan filter for (T subItem : getCurrentChildren(expandable)) { - //Reuse normal filter for Children + // Reuse normal filter for Children subItem.setHidden(!filterObject(subItem, getSearchText())); if (!filtered && !subItem.isHidden()) { filtered = true; } } - //Expand if filter found text in subItems + // Expand if filter found text in subItems expandable.setExpanded(filtered); } - //if not filtered already, fallback to Normal filter + // if not filtered already, fallback to Normal filter return filtered || filterObject(item, getSearchText()); } /** * This method checks if the provided object is a type of {@link IFilterable} interface, * if yes, performs the filter on the implemented method {@link IFilterable#filter(String)}. - *

NOTE: + *

Note: *
- The item will be collected if the implemented method returns true. *
- {@code IExpandable} items are automatically picked up and displayed if at least a * child is collected by the current filter. You DON'T NEED to implement the scan for the @@ -3285,11 +4033,7 @@ private boolean filterExpandableObject(T item) { *
5.0.0-b1 Expandable + Child filtering */ protected boolean filterObject(T item, String constraint) { - if (item instanceof IFilterable) { - IFilterable filterable = (IFilterable) item; - return filterable.filter(constraint); - } - return false; + return item instanceof IFilterable && ((IFilterable) item).filter(constraint); } /** @@ -3299,7 +4043,7 @@ private int addFilteredSubItems(List values, T item) { if (isExpandable(item)) { IExpandable expandable = (IExpandable) item; if (hasSubItems(expandable)) { - //Add subItems if not hidden by filterObject() + // Add subItems if not hidden by filterObject() List filteredSubItems = new ArrayList<>(); List subItems = expandable.getSubItems(); for (T subItem : subItems) { @@ -3316,21 +4060,21 @@ private int addFilteredSubItems(List values, T item) { * Clears flags after searchText is cleared out for Expandable items and sub items. */ private void resetFilterFlags(List items) { - //Reset flags for all items! + // Reset flags for all items! for (int i = 0; i < items.size(); i++) { T item = items.get(i); item.setHidden(false); if (isExpandable(item)) { IExpandable expandable = (IExpandable) item; - //Reset expanded flag + // Reset expanded flag if (mExpandedFilterFlags != null) expandable.setExpanded(mExpandedFilterFlags.contains(expandable)); if (hasSubItems(expandable)) { List subItems = expandable.getSubItems(); for (T subItem : subItems) { - //Reset subItem hidden flag + // Reset subItem hidden flag subItem.setHidden(false); - //Show subItems for expanded items + // Show subItems for expanded items if (expandable.isExpanded()) { i++; if (i < items.size()) items.add(i, subItem); @@ -3347,24 +4091,90 @@ private void resetFilterFlags(List items) { * Tunes the limit after the which the synchronization animations, occurred during * updateDataSet and filter operations, are skipped and {@link #notifyDataSetChanged()} * will be called instead. - *

Default value is {@value mAnimateToLimit} items, number new items.

+ *

Default value is {@value ANIMATE_TO_LIMIT} items, number new items.

* * @param limit the number of new items that, when reached, will skip synchronization animations * @return this Adapter, so the call can be chained * @since 5.0.0-b8 */ public FlexibleAdapter setAnimateToLimit(int limit) { + if (DEBUG) Log.i(TAG, "Set animateToLimit=" + limit); mAnimateToLimit = limit; return this; } + /*-------------------------*/ + /* ANIMATE CHANGES METHODS */ + /*-------------------------*/ + + /** + * @return true to calculate animation changes with DiffUtil, false to use default calculation. + * @see #setAnimateChangesWithDiffUtil(boolean) + * @deprecated DiffUtil is slower than the internal implementation, you can use it until + * final release. + */ + @Deprecated + public boolean isAnimateChangesWithDiffUtil() { + return useDiffUtil; + } + + /** + * Whether use {@link DiffUtil} to calculate the changes between 2 lists after Update or + * Filter operations. If disabled, the advanced default calculation will be used instead. + *

A time, to compare the 2 different approaches, is calculated and displayed in the log. + * To see the logs call {@link #enableLogs(boolean)} before creating the Adapter instance!

+ * Default value is {@code false} (default calculation is used). + * + * @param useDiffUtil true to switch the calculation and use DiffUtil, false to use the default + * calculation. + * @return this Adapter, so the call can be chained + * @see #setDiffUtilCallback(DiffUtilCallback) + * @deprecated DiffUtil is slower than the internal implementation, you can use it until + * final release. + */ + @Deprecated + public FlexibleAdapter setAnimateChangesWithDiffUtil(boolean useDiffUtil) { + this.useDiffUtil = useDiffUtil; + return this; + } + + /** + * Sets a custom implementation of {@link DiffUtilCallback} for the DiffUtil. Extend to + * implement the comparing methods. + * + * @param diffUtilCallback the custom callback that DiffUtil will call + * @return this Adapter, so the call can be chained + * @see #setAnimateChangesWithDiffUtil(boolean) + * @deprecated DiffUtil is slower than the internal implementation, you can use it until + * final release. + */ + @Deprecated + public FlexibleAdapter setDiffUtilCallback(DiffUtilCallback diffUtilCallback) { + this.diffUtilCallback = diffUtilCallback; + return this; + } + + @Deprecated //TODO: Call animateTo instead. + private synchronized void animateDiff(@Nullable List newItems, Payload payloadChange) { + if (useDiffUtil) { + Log.v(TAG, "Animate changes with DiffUtils! oldSize=" + getItemCount() + " newSize=" + newItems.size()); + if (diffUtilCallback == null) { + diffUtilCallback = new DiffUtilCallback(); + } + diffUtilCallback.setItems(mItems, newItems); + diffResult = DiffUtil.calculateDiff(diffUtilCallback, notifyMoveOfFilteredItems); + } else { + animateTo(newItems, payloadChange); + } + } + /** * Animate the synchronization between the old list and the new list. *

Used by filter and updateDataSet.

- * Note: The animations are skipped in favor of {@code notifyDataSetChanged} + * Note: The animations are skipped in favor of {@link #notifyDataSetChanged()} * when the number of items reaches the limit. See {@link #setAnimateToLimit(int)}. *

Note: In case the animations are performed, unchanged items will be notified if - * {@code notifyChangeOfUnfilteredItems} is set true, and payload will be set as a Boolean.

+ * {@code notifyChangeOfUnfilteredItems} is set true, and CHANGE payload will be set.

* * @param newItems the new list containing the new items * @see #setNotifyChangeOfUnfilteredItems(boolean) @@ -3373,9 +4183,8 @@ public FlexibleAdapter setAnimateToLimit(int limit) { * @since 5.0.0-b1 Created *
5.0.0-b8 Synchronization animation limit */ - private synchronized void animateTo(@Nullable List newItems) { - if (newItems == null) newItems = new ArrayList<>(); - notifications = new ArrayList<>(); + private synchronized void animateTo(@Nullable List newItems, Payload payloadChange) { + mNotifications = new ArrayList<>(); if (newItems.size() <= mAnimateToLimit) { if (DEBUG) Log.v(TAG, "Animate changes! oldSize=" + getItemCount() + " newSize=" + newItems.size() + " limit=" + mAnimateToLimit); @@ -3388,10 +4197,10 @@ private synchronized void animateTo(@Nullable List newItems) { if (DEBUG) Log.v(TAG, "NotifyDataSetChanged! oldSize=" + getItemCount() + " newSize=" + newItems.size() + " limit=" + mAnimateToLimit); mTempItems = newItems; - notifications.add(new Notification(-1, 0)); + mNotifications.add(new Notification(-1, 0)); } //Execute All notifications if filter was Synchronous! - if (mFilterAsyncTask == null) executeNotifications(); + if (mFilterAsyncTask == null) executeNotifications(payloadChange); } /** @@ -3400,25 +4209,43 @@ private synchronized void animateTo(@Nullable List newItems) { * @since 5.0.0-b1 */ private void applyAndAnimateRemovals(List from, List newItems) { - //Using Hash for performance - hashItems = new HashSet<>(newItems); - int out = 0; + // This avoids the call indexOf() later on: newItems.get(newItems.indexOf(item))); + // and use the hash for the get + Map existingItems = null; + if (notifyChangeOfUnfilteredItems) { + // Using Hash for performance + mHashItems = new HashSet<>(from); + existingItems = new HashMap<>(); + for (int i = 0; i < newItems.size(); i++) { + if (mFilterAsyncTask != null && mFilterAsyncTask.isCancelled()) return; + final T item = newItems.get(i); + // Save the index of this new item + if (mHashItems.contains(item)) existingItems.put(item, i); + } + } + // Using Hash for performance + mHashItems = new HashSet<>(newItems); + int out = 0, mod = 0; for (int i = from.size() - 1; i >= 0; i--) { if (mFilterAsyncTask != null && mFilterAsyncTask.isCancelled()) return; final T item = from.get(i); - if (!hashItems.contains(item) && (!isHeader(item) || (isHeader(item) && headersShown))) { + if (!mHashItems.contains(item)) { //if (DEBUG) Log.v(TAG, "calculateRemovals remove position=" + i + " item=" + item + " searchText=" + mSearchText); from.remove(i); - notifications.add(new Notification(i, Notification.REMOVE)); + mNotifications.add(new Notification(i, Notification.REMOVE)); out++; } else if (notifyChangeOfUnfilteredItems) { - from.set(i, item); - notifications.add(new Notification(i, Notification.CHANGE)); - //if (DEBUG) Log.v(TAG, "calculateRemovals keep position=" + i + " item=" + item + " searchText=" + mSearchText); + from.set(i, newItems.get(existingItems.get(item))); + mNotifications.add(new Notification(i, Notification.CHANGE)); + mod++; + //if (DEBUG) Log.v(TAG, "calculateAdditions keep position=" + i + " item=" + item + " searchText=" + mSearchText); } } - hashItems = null; - if (DEBUG) Log.v(TAG, "calculateRemovals total out=" + out); + mHashItems = null; + if (DEBUG) { + Log.v(TAG, "calculateRemovals total out=" + out); + Log.v(TAG, "calculateModifications total mod=" + mod); + } } /** @@ -3427,26 +4254,26 @@ private void applyAndAnimateRemovals(List from, List newItems) { * @since 5.0.0-b1 */ private void applyAndAnimateAdditions(List from, List newItems) { - //Using Hash for performance - hashItems = new HashSet<>(from); + // Using Hash for performance + mHashItems = new HashSet<>(from); int in = 0; for (int i = 0; i < newItems.size(); i++) { if (mFilterAsyncTask != null && mFilterAsyncTask.isCancelled()) return; final T item = newItems.get(i); - if (!hashItems.contains(item)) { - //if (DEBUG) Log.v(TAG, "calculateAdditions add position=" + i + " item=" + item + " searchText=" + mSearchText); + if (!mHashItems.contains(item)) { + // if (DEBUG) Log.v(TAG, "calculateAdditions add position=" + i + " item=" + item + " searchText=" + mSearchText); if (notifyMoveOfFilteredItems) { - //We add always at the end to animate moved items at the missing position + // We add always at the end to animate moved items at the missing position from.add(item); - notifications.add(new Notification(from.size(), Notification.ADD)); + mNotifications.add(new Notification(from.size(), Notification.ADD)); } else { from.add(i, item); - notifications.add(new Notification(i, Notification.ADD)); + mNotifications.add(new Notification(i, Notification.ADD)); } in++; } } - hashItems = null; + mHashItems = null; if (DEBUG) Log.v(TAG, "calculateAdditions total new=" + in); } @@ -3463,49 +4290,80 @@ private void applyAndAnimateMovedItems(List from, List newItems) { final T item = newItems.get(toPosition); final int fromPosition = from.indexOf(item); if (fromPosition >= 0 && fromPosition != toPosition) { - //if (DEBUG) Log.v(TAG, "calculateMovedItems fromPosition=" + fromPosition + " toPosition=" + toPosition + " searchText=" + mSearchText); + // if (DEBUG) Log.v(TAG, "calculateMovedItems fromPosition=" + fromPosition + " toPosition=" + toPosition + " searchText=" + mSearchText); T movedItem = from.remove(fromPosition); if (toPosition < from.size()) from.add(toPosition, movedItem); else from.add(movedItem); - notifications.add(new Notification(fromPosition, toPosition, Notification.MOVE)); + mNotifications.add(new Notification(fromPosition, toPosition, Notification.MOVE)); move++; } } if (DEBUG) Log.v(TAG, "calculateMovedItems total move=" + move); } - private synchronized void executeNotifications() { - if (DEBUG) Log.i(TAG, "Performing " + notifications.size() + " notifications"); - mItems = mTempItems;// Update mItems in the UI Thread - setAnimate(false);//Disable scroll animation - for (Notification notification : notifications) { - switch (notification.operation) { - case Notification.ADD: - notifyItemInserted(notification.position); - break; - case Notification.CHANGE: - notifyItemChanged(notification.position, Payload.FILTER); - break; - case Notification.REMOVE: - notifyItemRemoved(notification.position); - break; - case Notification.MOVE: - notifyItemMoved(notification.fromPosition, notification.position); - break; - default: - if (DEBUG) Log.w(TAG, "notifyDataSetChanged!"); - notifyDataSetChanged(); - break; + private synchronized void executeNotifications(Payload payloadChange) { + if (diffResult != null) { + if (DEBUG) Log.i(TAG, "Dispatching notifications"); + mItems = diffUtilCallback.getNewItems(); //Update mItems in the UI Thread + diffResult.dispatchUpdatesTo(this); + diffResult = null; + } else { + if (DEBUG) Log.i(TAG, "Performing " + mNotifications.size() + " notifications"); + mItems = mTempItems; //Update mItems in the UI Thread + setScrollAnimate(false); //Disable scroll animation + for (Notification notification : mNotifications) { + switch (notification.operation) { + case Notification.ADD: + notifyItemInserted(notification.position); + break; + case Notification.CHANGE: + notifyItemChanged(notification.position, payloadChange); + break; + case Notification.REMOVE: + notifyItemRemoved(notification.position); + break; + case Notification.MOVE: + notifyItemMoved(notification.fromPosition, notification.position); + break; + default: + if (DEBUG) Log.w(TAG, "notifyDataSetChanged!"); + notifyDataSetChanged(); + break; + } } + mTempItems = null; + mNotifications = null; } - mTempItems = null; - notifications = null; + time = System.currentTimeMillis(); + time = time - start; + if (DEBUG) Log.i(TAG, "Animate changes DONE in " + time + "ms"); + } + + /** + * @return the time (in ms) of the last update or filter operation. + */ + public long getTime() { + return time; } /*---------------*/ /* TOUCH METHODS */ /*---------------*/ + private void initializeItemTouchHelper() { + if (mItemTouchHelper == null) { + if (mRecyclerView == null) { + throw new IllegalStateException("RecyclerView cannot be null. Enabling LongPressDrag or Swipe must be done after the Adapter is added to the RecyclerView."); + } + if (mItemTouchHelperCallback == null) { + mItemTouchHelperCallback = new ItemTouchHelperCallback(this); + if (DEBUG) Log.i(TAG, "Initialized default ItemTouchHelperCallback"); + } + mItemTouchHelper = new ItemTouchHelper(mItemTouchHelperCallback); + mItemTouchHelper.attachToRecyclerView(mRecyclerView); + } + } + /** * Used by {@link FlexibleViewHolder#onTouch(View, MotionEvent)} * to start Drag or Swipe when HandleView is touched. @@ -3519,9 +4377,11 @@ public final ItemTouchHelper getItemTouchHelper() { } /** - * Returns the customization of the ItemTouchHelperCallback. + * Returns the customization of the ItemTouchHelperCallback or the default if it wasn't set + * before. * * @return the ItemTouchHelperCallback instance already initialized + * @see #setItemTouchHelperCallback(ItemTouchHelperCallback) * @since 5.0.0-b7 */ public final ItemTouchHelperCallback getItemTouchHelperCallback() { @@ -3529,6 +4389,23 @@ public final ItemTouchHelperCallback getItemTouchHelperCallback() { return mItemTouchHelperCallback; } + /** + * Sets a custom callback implementation for item touch. + *

If called, Helper will be reinitialized.

+ * If not called, the default Helper will be used. + * + * @param itemTouchHelperCallback the custom callback implementation for item touch + * @return this Adapter, so the call can be chained + * @since 5.0.0-rc1 + */ + public final FlexibleAdapter setItemTouchHelperCallback(ItemTouchHelperCallback itemTouchHelperCallback) { + mItemTouchHelperCallback = itemTouchHelperCallback; + mItemTouchHelper = null; + initializeItemTouchHelper(); + if (DEBUG) Log.i(TAG, "Initialized custom ItemTouchHelperCallback"); + return this; + } + /** * Returns whether ItemTouchHelper should start a drag and drop operation if an item is * long pressed.

@@ -3536,6 +4413,7 @@ public final ItemTouchHelperCallback getItemTouchHelperCallback() { * * @return true if ItemTouchHelper should start dragging an item when it is long pressed, * false otherwise. Default value is {@code false}. + * @see #setLongPressDragEnabled(boolean) * @since 5.0.0-b1 */ public final boolean isLongPressDragEnabled() { @@ -3543,8 +4421,8 @@ public final boolean isLongPressDragEnabled() { } /** - * Enable the Drag on LongPress on the entire ViewHolder. - *

NOTE: This will skip LongClick on the view in order to handle the LongPress, + * Enable / Disable the Drag on LongPress on the entire ViewHolder. + *

Note: This will skip LongClick on the view in order to handle the LongPress, * however the LongClick listener will be called if necessary in the new * {@link FlexibleViewHolder#onActionStateChanged(int, int)}.

* Default value is {@code false}. @@ -3555,24 +4433,28 @@ public final boolean isLongPressDragEnabled() { */ public final FlexibleAdapter setLongPressDragEnabled(boolean longPressDragEnabled) { initializeItemTouchHelper(); + if (DEBUG) Log.i(TAG, "Set longPressDragEnabled=" + longPressDragEnabled); mItemTouchHelperCallback.setLongPressDragEnabled(longPressDragEnabled); return this; } /** - * Enabled by default. - *

To use, it is sufficient to set the HandleView by calling - * {@link FlexibleViewHolder#setDragHandleView(View)}.

+ * Returns whether ItemTouchHelper should start a drag and drop operation by touching its + * handle. + *

Default value is {@code false}.

+ * To use, it is sufficient to set the HandleView by calling + * {@link FlexibleViewHolder#setDragHandleView(View)}. * * @return true if active, false otherwise + * @see #setHandleDragEnabled(boolean) * @since 5.0.0-b1 */ public final boolean isHandleDragEnabled() { - return handleDragEnabled; + return mItemTouchHelperCallback != null && mItemTouchHelperCallback.isHandleDragEnabled(); } /** - * Enable/Disable the drag with handle. + * Enable / Disable the drag of the itemView with a handle view. *

Default value is {@code false}.

* * @param handleDragEnabled true to activate, false otherwise @@ -3580,7 +4462,9 @@ public final boolean isHandleDragEnabled() { * @since 5.0.0-b1 */ public final FlexibleAdapter setHandleDragEnabled(boolean handleDragEnabled) { - this.handleDragEnabled = handleDragEnabled; + initializeItemTouchHelper(); + if (DEBUG) Log.i(TAG, "Set handleDragEnabled=" + handleDragEnabled); + this.mItemTouchHelperCallback.setHandleDragEnabled(handleDragEnabled); return this; } @@ -3591,6 +4475,7 @@ public final FlexibleAdapter setHandleDragEnabled(boolean handleDragEnabled) { * * @return true if ItemTouchHelper should start swiping an item when user swipes a pointer * over the View, false otherwise. Default value is {@code false}. + * @see #setSwipeEnabled(boolean) * @since 5.0.0-b1 */ public final boolean isSwipeEnabled() { @@ -3606,6 +4491,7 @@ public final boolean isSwipeEnabled() { * @since 5.0.0-b1 */ public final FlexibleAdapter setSwipeEnabled(boolean swipeEnabled) { + if (DEBUG) Log.i(TAG, "Set swipeEnabled=" + swipeEnabled); initializeItemTouchHelper(); mItemTouchHelperCallback.setSwipeEnabled(swipeEnabled); return this; @@ -3640,26 +4526,25 @@ public void moveItem(int fromPosition, int toPosition) { public void moveItem(int fromPosition, int toPosition, @Nullable Object payload) { if (DEBUG) Log.v(TAG, "moveItem fromPosition=" + fromPosition + " toPosition=" + toPosition); - //Preserve selection + // Preserve selection if ((isSelected(fromPosition))) { removeSelection(fromPosition); addSelection(toPosition); } T item = mItems.get(fromPosition); - //Preserve expanded status and Collapse expandable + // Preserve expanded status and Collapse expandable boolean expanded = isExpanded(item); if (expanded) collapse(toPosition); - //Move item! + // Move item! mItems.remove(fromPosition); - if (toPosition < getItemCount()) mItems.add(toPosition, item); - else mItems.add(item); + performInsert(toPosition, Collections.singletonList(item), false); notifyItemMoved(fromPosition, toPosition); if (payload != null) notifyItemChanged(toPosition, payload); - //Eventually display the new Header + // Eventually display the new Header if (headersShown) { showHeaderOf(toPosition, item, false); } - //Restore original expanded status + // Restore original expanded status if (expanded) expand(toPosition); } @@ -3682,12 +4567,12 @@ public void swapItems(List list, int fromPosition, int toPosition) { toPosition + " [selected? " + isSelected(toPosition) + "]"); } - //Collapse expandable before swapping (otherwise items are mixed badly) -// if (fromPosition < toPosition && isExpandable(getItem(fromPosition)) && isExpanded(toPosition)) { -// collapse(toPosition); -// } + // Collapse expandable before swapping (otherwise items are mixed badly) + if (fromPosition < toPosition && isExpandable(getItem(fromPosition)) && isExpanded(toPosition)) { + collapse(toPosition); + } - //Perform item swap (for all LayoutManagers) + // Perform item swap (for all LayoutManagers) if (fromPosition < toPosition) { for (int i = fromPosition; i < toPosition; i++) { if (DEBUG) Log.v(TAG, "swapItems from=" + i + " to=" + (i + 1)); @@ -3703,24 +4588,24 @@ public void swapItems(List list, int fromPosition, int toPosition) { } notifyItemMoved(fromPosition, toPosition); - //Header swap linkage + // Header swap linkage if (headersShown) { - //Situation AFTER items have been swapped, items are inverted! + // Situation AFTER items have been swapped, items are inverted! T fromItem = getItem(toPosition); T toItem = getItem(fromPosition); int oldPosition, newPosition; if (toItem instanceof IHeader && fromItem instanceof IHeader) { if (fromPosition < toPosition) { - //Dragging down fromHeader - //Auto-linkage all section-items with new header + // Dragging down fromHeader + // Auto-linkage all section-items with new header IHeader header = (IHeader) fromItem; List items = getSectionItems(header); for (ISectionable sectionable : items) { linkHeaderTo((T) sectionable, header, Payload.LINK); } } else { - //Dragging up fromHeader - //Auto-linkage all section-items with new header + // Dragging up fromHeader + // Auto-linkage all section-items with new header IHeader header = (IHeader) toItem; List items = getSectionItems(header); for (ISectionable sectionable : items) { @@ -3728,27 +4613,27 @@ public void swapItems(List list, int fromPosition, int toPosition) { } } } else if (toItem instanceof IHeader) { - //A Header is being swapped up - //Else a Header is being swapped down + // A Header is being swapped up + // Else a Header is being swapped down oldPosition = fromPosition < toPosition ? toPosition + 1 : toPosition; newPosition = fromPosition < toPosition ? toPosition : fromPosition + 1; - //Swap header linkage + // Swap header linkage linkHeaderTo(getItem(oldPosition), getSectionHeader(oldPosition), Payload.LINK); linkHeaderTo(getItem(newPosition), (IHeader) toItem, Payload.LINK); } else if (fromItem instanceof IHeader) { - //A Header is being dragged down - //Else a Header is being dragged up + // A Header is being dragged down + // Else a Header is being dragged up oldPosition = fromPosition < toPosition ? fromPosition : fromPosition + 1; newPosition = fromPosition < toPosition ? toPosition + 1 : fromPosition; - //Swap header linkage + // Swap header linkage linkHeaderTo(getItem(oldPosition), getSectionHeader(oldPosition), Payload.LINK); linkHeaderTo(getItem(newPosition), (IHeader) fromItem, Payload.LINK); } else { - //A Header receives the toItem - //Else a Header receives the fromItem + // A Header receives the toItem + // Else a Header receives the fromItem oldPosition = fromPosition < toPosition ? toPosition : fromPosition; newPosition = fromPosition < toPosition ? fromPosition : toPosition; - //Swap header linkage + // Swap header linkage T oldItem = getItem(oldPosition); IHeader header = getHeaderOf(oldItem); if (header != null) { @@ -3783,8 +4668,9 @@ else if (mItemSwipeListener != null) { */ @Override public boolean shouldMove(int fromPosition, int toPosition) { - return (mItemMoveListener == null || mItemMoveListener.shouldMoveItem(fromPosition, toPosition));// && - //!(isExpandable(getItem(fromPosition)) && getExpandableOf(toPosition) != null); + T toItem = getItem(toPosition); + return !(mScrollableHeaders.contains(toItem) || mScrollableFooters.contains(toItem)) && + (mItemMoveListener == null || mItemMoveListener.shouldMoveItem(fromPosition, toPosition)); } /** @@ -3811,23 +4697,12 @@ public boolean onItemMove(int fromPosition, int toPosition) { @Override @CallSuper public void onItemSwiped(int position, int direction) { - //Delegate actions to the user + // Delegate actions to the user if (mItemSwipeListener != null) { mItemSwipeListener.onItemSwipe(position, direction); } } - private void initializeItemTouchHelper() { - if (mItemTouchHelper == null) { - if (mRecyclerView == null) { - throw new IllegalStateException("RecyclerView cannot be null. Enabling LongPressDrag or Swipe must be done after the Adapter is added to the RecyclerView."); - } - mItemTouchHelperCallback = new ItemTouchHelperCallback(this); - mItemTouchHelper = new ItemTouchHelper(mItemTouchHelperCallback); - mItemTouchHelper.attachToRecyclerView(mRecyclerView); - } - } - /*------------------------*/ /* OTHERS PRIVATE METHODS */ /*------------------------*/ @@ -3842,7 +4717,7 @@ private void mapViewTypeFrom(T item) { if (item != null && !mTypeInstances.containsKey(item.getLayoutRes())) { mTypeInstances.put(item.getLayoutRes(), item); if (DEBUG) - Log.i(TAG, "Mapped viewType " + item.getLayoutRes() + " from " + item.getClass().getSimpleName()); + Log.i(TAG, "Mapped viewType " + item.getLayoutRes() + " from " + getClassName(item)); } } @@ -3863,7 +4738,7 @@ private T getViewTypeInstance(int viewType) { */ private RestoreInfo getPendingRemovedItem(T item) { for (RestoreInfo restoreInfo : mRestoreList) { - //refPosition >= 0 means that position has been calculated and restore is ongoing + // refPosition >= 0 means that position has been calculated and restore is ongoing if (restoreInfo.item.equals(item) && restoreInfo.refPosition < 0) return restoreInfo; } return null; @@ -3893,15 +4768,15 @@ private int createRestoreSubItemInfo(IExpandable expandable, T item, @Nullable O * @since 5.0.0-b1 */ private void createRestoreItemInfo(int position, T item, @Nullable Object payload) { - //Collapse Parent before removal if it is expanded! + // Collapse Parent before removal if it is expanded! if (isExpanded(item)) collapse(position); item.setHidden(true); - //Get the reference of the previous item (getItem returns null if outOfBounds) - //If null, it will be restored at position = 0 + // Get the reference of the previous item (getItem returns null if outOfBounds) + // If null, it will be restored at position = 0 T refItem = getItem(position - 1); if (refItem != null) { - //Check if the refItem is a child of an Expanded parent, take the parent! + // Check if the refItem is a child of an Expanded parent, take the parent! IExpandable expandable = getExpandableOf(refItem); if (expandable != null) refItem = (T) expandable; } @@ -3921,7 +4796,7 @@ private List getExpandableList(IExpandable expandable) { if (expandable != null && hasSubItems(expandable)) { List allSubItems = expandable.getSubItems(); for (T subItem : allSubItems) { - //Pick up only no hidden items (doesn't get into account the filtered items) + // Pick up only no hidden items (doesn't get into account the filtered items) if (!subItem.isHidden()) subItems.add(subItem); } } @@ -3938,15 +4813,21 @@ private List getExpandableList(IExpandable expandable) { */ private boolean hasSubItemsSelected(int startPosition, List subItems) { for (T subItem : subItems) { - if (isSelected(startPosition + 1) || - (isExpandable(subItem) && hasSubItemsSelected(startPosition + 1, getExpandableList((IExpandable) subItem)))) + if (isSelected(++startPosition) || + (isExpanded(subItem) && hasSubItemsSelected(startPosition, getExpandableList((IExpandable) subItem)))) return true; } return false; } + private void performScroll(final int position) { + if (mRecyclerView != null) { + mRecyclerView.smoothScrollToPosition(Math.min(Math.max(0, position), getItemCount() - 1)); + } + } + private void autoScrollWithDelay(final int position, final int subItemsCount, final long delay) { - //Must be delayed to give time at RecyclerView to recalculate positions after an automatic collapse + // Must be delayed to give time at RecyclerView to recalculate positions after an automatic collapse new Handler(Looper.getMainLooper(), new Handler.Callback() { public boolean handleMessage(Message message) { int firstVisibleItem = Utils.findFirstCompletelyVisibleItemPosition(mRecyclerView.getLayoutManager()); @@ -3958,16 +4839,16 @@ public boolean handleMessage(Message message) { int scrollMax = position - firstVisibleItem; int scrollMin = Math.max(0, position + subItemsCount - lastVisibleItem); int scrollBy = Math.min(scrollMax, scrollMin); - int spanCount = getSpanCount(mRecyclerView.getLayoutManager()); + int spanCount = Utils.getSpanCount(mRecyclerView.getLayoutManager()); if (spanCount > 1) { scrollBy = scrollBy % spanCount + spanCount; } int scrollTo = firstVisibleItem + scrollBy; // if (DEBUG) // Log.v(TAG, "autoScroll scrollMin=" + scrollMin + " scrollMax=" + scrollMax + " scrollBy=" + scrollBy + " scrollTo=" + scrollTo); - mRecyclerView.smoothScrollToPosition(scrollTo); + performScroll(scrollTo); } else if (position < firstVisibleItem) { - mRecyclerView.smoothScrollToPosition(position); + performScroll(position); } return true; } @@ -3977,16 +4858,29 @@ public boolean handleMessage(Message message) { private void adjustSelected(int startPosition, int itemCount) { List selectedPositions = getSelectedPositions(); boolean adjusted = false; + String diff = ""; + if (itemCount > 0) { + // Reverse sorting is necessary because using Set removes duplicates + // during adjusting, so we scan backward. + Collections.sort(selectedPositions, new Comparator() { + @Override + public int compare(Integer lhs, Integer rhs) { + return rhs - lhs; + } + }); + diff = "+"; + } for (Integer position : selectedPositions) { if (position >= startPosition) { - if (DEBUG) - Log.v(TAG, "Adjust Selected position " + position + " to " + Math.max(position + itemCount, startPosition)); +// if (DEBUG) +// Log.v(TAG, "Adjust Selected position " + position + " to " + Math.max(position + itemCount, startPosition)); removeSelection(position); - addSelection(Math.max(position + itemCount, startPosition)); + addAdjustedSelection(Math.max(position + itemCount, startPosition)); adjusted = true; } } - if (DEBUG && adjusted) Log.v(TAG, "AdjustedSelected=" + getSelectedPositions()); + if (DEBUG && adjusted) + Log.v(TAG, "AdjustedSelected(" + diff + itemCount + ")=" + getSelectedPositions()); } /*----------------*/ @@ -4001,16 +4895,21 @@ private void adjustSelected(int startPosition, int itemCount) { */ public void onSaveInstanceState(Bundle outState) { if (outState != null) { - //Save selection state + // Save selection state + if (mScrollableHeaders.size() > 0) { + // We need to rollback the added item positions if headers were added lately + adjustSelected(0, -mScrollableHeaders.size()); + } super.onSaveInstanceState(outState); - //Save selection coherence + // Save selection coherence outState.putBoolean(EXTRA_CHILD, this.childSelected); outState.putBoolean(EXTRA_PARENT, this.parentSelected); - outState.putInt(EXTRA_LEVEL, this.selectedLevel); - //Current filter. Old text is not saved otherwise animateTo() cannot be called + outState.putInt(EXTRA_LEVEL, this.mSelectedLevel); + // Current filter. Old text is not saved otherwise animateTo() cannot be called outState.putString(EXTRA_SEARCH, this.mSearchText); - //Save headers shown status + // Save headers shown status outState.putBoolean(EXTRA_HEADERS, this.headersShown); + outState.putBoolean(EXTRA_STICKY, areHeadersSticky()); } } @@ -4022,7 +4921,7 @@ public void onSaveInstanceState(Bundle outState) { */ public void onRestoreInstanceState(Bundle savedInstanceState) { if (savedInstanceState != null) { - //Restore headers shown status + // Restore headers shown status boolean headersShown = savedInstanceState.getBoolean(EXTRA_HEADERS); if (!headersShown) { hideAllHeaders(); @@ -4030,13 +4929,20 @@ public void onRestoreInstanceState(Bundle savedInstanceState) { showAllHeadersWithReset(true); } this.headersShown = headersShown; - //Restore selection state + if (savedInstanceState.getBoolean(EXTRA_STICKY) && !areHeadersSticky()) { + setStickyHeaders(true); + } + // Restore selection state super.onRestoreInstanceState(savedInstanceState); - //Restore selection coherence + if (mScrollableHeaders.size() > 0) { + // We need to restore the added item positions if headers were added early + adjustSelected(0, mScrollableHeaders.size()); + } + // Restore selection coherence this.parentSelected = savedInstanceState.getBoolean(EXTRA_PARENT); this.childSelected = savedInstanceState.getBoolean(EXTRA_CHILD); - this.selectedLevel = savedInstanceState.getInt(EXTRA_LEVEL); - //Current filter (old text must not be saved) + this.mSelectedLevel = savedInstanceState.getInt(EXTRA_LEVEL); + // Current filter (old text must not be saved) this.mSearchText = savedInstanceState.getString(EXTRA_SEARCH); } } @@ -4050,9 +4956,12 @@ public void onRestoreInstanceState(Bundle savedInstanceState) { */ public interface OnUpdateListener { /** - * Called at startup and every time an item is inserted, removed or filtered. + * Called at startup and every time a main item is inserted, removed or filtered. + *

Note: Having any Scrollable Headers/Footers visible, the {@code size} + * will represents only the main items.

* - * @param size the current number of items in the adapter, result of {@link #getItemCount()} + * @param size the current number of main items in the adapter, result of + * {@link FlexibleAdapter#getMainItemCount()} * @since 5.0.0-b1 */ void onUpdateEmptyView(int size); @@ -4065,8 +4974,8 @@ public interface OnDeleteCompleteListener { /** * Called when Undo timeout is over and removal must be committed in the user Database. *

Due to Java Generic, it's too complicated and not - * well manageable if we pass the List<T> object.
- * To get deleted items, use {@link #getDeletedItems()} from the + * well manageable if we pass the List<T> object.
+ * To get deleted items, use {@link FlexibleAdapter#getDeletedItems()} from the * implementation of this method.

* * @since 5.0.0-b1 @@ -4080,12 +4989,14 @@ public interface OnDeleteCompleteListener { public interface OnItemClickListener { /** * Called when single tap occurs. - *

Delegates the click event to the listener and checks if selection MODE if - * SINGLE or MULTI is enabled in order to activate the ItemView.

+ *

This method receives the click event generated from the itemView to check if one + * of the selection mode ({@code SINGLE or MULTI}) is enabled in order to activate the + * itemView.

* For Expandable Views it will toggle the Expansion if configured so. * * @param position the adapter position of the item clicked - * @return true if the click should activate the ItemView, false for no change. + * @return true if the click should activate the itemView according to the selection mode, + * false for no change to the itemView. * @since 5.0.0-b1 */ boolean onItemClick(int position); @@ -4097,9 +5008,8 @@ public interface OnItemClickListener { public interface OnItemLongClickListener { /** * Called when long tap occurs. - *

This method always calls - * {@link FlexibleViewHolder#toggleActivation} - * after listener event is consumed in order to activate the ItemView.

+ *

This method always calls {@link FlexibleViewHolder#toggleActivation()} + * after the listener event is consumed in order to activate the itemView.

* For Expandable Views it will collapse the View if configured so. * * @param position the adapter position of the item clicked @@ -4113,8 +5023,8 @@ public interface OnItemLongClickListener { */ public interface OnActionStateListener { /** - * Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped - * or when has been released. + * Called when the {@link ItemTouchHelper} first registers an item as being moved + * or swiped or when has been released. *

Override this method to receive touch events with its state.

* * @param viewHolder the viewHolder touched @@ -4138,7 +5048,7 @@ public interface OnItemMoveListener extends OnActionStateListener { * @param toPosition the potential resolved position of the swapped item * @return return true if the items can swap ({@code onItemMove()} will be called), * false otherwise (nothing happens) - * @see #onItemMove(int, int) + * @see FlexibleAdapter#onItemMove(int, int) * @since 5.0.0-b8 */ boolean shouldMoveItem(int fromPosition, int toPosition); @@ -4151,7 +5061,7 @@ public interface OnItemMoveListener extends OnActionStateListener { * * @param fromPosition the start position of the moved item * @param toPosition the resolved position of the moved item - * @see #shouldMoveItem(int, int) + * @see FlexibleAdapter#shouldMove(int, int) * @since 5.0.0-b1 */ void onItemMove(int fromPosition, int toPosition); @@ -4162,7 +5072,7 @@ public interface OnItemMoveListener extends OnActionStateListener { */ public interface OnItemSwipeListener extends OnActionStateListener { /** - * Called when swiping ended its animation and Item is not visible anymore. + * Called when swiping ended its animation and item is not visible anymore. * * @param position the position of the item swiped * @param direction the direction to which the ViewHolder is swiped, one of: @@ -4191,13 +5101,33 @@ public interface OnStickyHeaderChangeListener { /** * @since 22/04/2016 */ - public interface EndlessScrollListener { + public interface EndlessScrollListener { + + /** + * No more data to load. + *

This method is called if any limit is reached (targetCount or pageSize + * must be set) AND if new data is temporary unavailable (ex. no connection or no + * new updates remotely). If no new data, a {@link FlexibleAdapter#notifyItemChanged(int, Object)} + * with a payload {@link Payload#NO_MORE_LOAD} is triggered on the progressItem.

+ * + * @param newItemsSize the last size of the new items loaded + * @see FlexibleAdapter#setEndlessTargetCount(int) + * @see FlexibleAdapter#setEndlessPageSize(int) + * @since 5.0.0-rc1 + */ + void noMoreLoad(int newItemsSize); + /** * Loads more data. + *

Use {@code lastPosition} and {@code currentPage} to know what to load next.

+ * {@code lastPosition} is the count of the main items without Scrollable Headers. * + * @param lastPosition the position of the last main item in the adapter + * @param currentPage the current page * @since 5.0.0-b6 + *
5.0.0-rc1 added {@code lastPosition} and {@code currentPage} as parameters */ - void onLoadMore(); + void onLoadMore(int lastPosition, int currentPage); } /** @@ -4206,8 +5136,8 @@ public interface EndlessScrollListener { private class AdapterDataObserver extends RecyclerView.AdapterDataObserver { private void adjustPositions(int positionStart, int itemCount) { - if (!filtering) {//Filtering has multiple insert and removal, we skip this process - if (adjustSelected)//Don't, if remove range / restore + if (!filtering) { //Filtering has multiple insert and removal, we skip this process + if (adjustSelected) //Don't, if remove range / restore adjustSelected(positionStart, itemCount); adjustSelected = true; } @@ -4263,7 +5193,7 @@ public RestoreInfo(T refItem, T item, Object payload) { } public RestoreInfo(T refItem, T item, int relativePosition, Object payload) { - this.refItem = refItem;//This can be an Expandable or a Header + this.refItem = refItem; //This can be an Expandable or a Header this.item = item; this.relativePosition = relativePosition; this.payload = payload; @@ -4278,7 +5208,7 @@ public int getRestorePosition(boolean isChild) { } T item = getItem(refPosition); if (isChild && isExpandable(item)) { - //Assert the expandable children are collapsed + // Assert the expandable children are collapsed recursiveCollapse(refPosition, getCurrentChildren((IExpandable) item), 0); } else if (isExpanded(item) && !isChild) { refPosition += getExpandableList((IExpandable) item).size() + 1; @@ -4344,10 +5274,12 @@ protected void onPreExecute() { @Override protected Void doInBackground(Void... params) { + start = System.currentTimeMillis(); switch (what) { case UPDATE: if (DEBUG) Log.d(TAG, "doInBackground - started UPDATE"); - animateTo(newItems); + animateDiff(newItems, Payload.CHANGE); + //animateTo(newItems, Payload.CHANGE); if (DEBUG) Log.d(TAG, "doInBackground - ended UPDATE"); break; case FILTER: @@ -4361,51 +5293,211 @@ protected Void doInBackground(Void... params) { @Override protected void onPostExecute(Void result) { - //Notify all the changes - executeNotifications(); - //Execute post data - switch (what) { - case UPDATE: - postUpdate(false); - break; - case FILTER: - postFilter(); - break; + if (diffResult != null || mNotifications != null) { + //Execute post data + switch (what) { + case UPDATE: + // Notify all the changes + executeNotifications(Payload.CHANGE); + postUpdate(false); + break; + case FILTER: + // Notify all the changes + executeNotifications(Payload.FILTER); + postFilter(); + break; + } } mFilterAsyncTask = null; } } /** - * @param init true to skip all notifications and instant reset by calling notifyDataSetChanged + * @param init true to skip all notifications and instant refresh the list by calling + * {@link #notifyDataSetChanged()} */ private void postUpdate(boolean init) { - //Show headers and expanded items if Data Set not empty + // Show headers and expanded items if Data Set not empty if (getItemCount() > 0) { expandItemsAtStartUp(); if (headersShown) { showAllHeadersWithReset(init); } } - //Execute instant reset on init + // Execute instant reset on init if (init) { - if (DEBUG) Log.w(TAG, "notifyDataSetChanged!"); + if (DEBUG) Log.w(TAG, "updateDataSet with notifyDataSetChanged!"); notifyDataSetChanged(); } - //Update empty view + // Perform user code + onPostUpdate(); + // Update empty view if (mUpdateListener != null) { - mUpdateListener.onUpdateEmptyView(getItemCount()); + mUpdateListener.onUpdateEmptyView(getMainItemCount()); } } + /** + * This method is called after the execution of Async Update and before the call to the + * {@link OnUpdateListener#onUpdateEmptyView(int)}. + * + * @see #updateDataSet(List, boolean) + */ + protected void onPostUpdate() { + // Dedicated for user implementation + } + private void postFilter() { - //Restore headers if necessary + // Restore headers if necessary if (headersShown && !hasSearchText()) { showAllHeadersWithReset(false); } - //Call listener to update EmptyView, assuming the filter always made a change + // Perform user code + onPostFilter(); + // Call listener to update EmptyView, assuming the filter always made a change if (mUpdateListener != null) - mUpdateListener.onUpdateEmptyView(getItemCount()); + mUpdateListener.onUpdateEmptyView(getMainItemCount()); + } + + /** + * This method is called after the execution of Async Filter and before the call to the + * {@link OnUpdateListener#onUpdateEmptyView(int)}. + * + * @see #filterItems(List) + */ + protected void onPostFilter() { + // Dedicated for user implementation + } + + /** + * Handler callback for delayed actions. + *

You can use and override this Callback, current values used by the Adapter:

+ * 0 = async call for updateDataSet. + *
1 = async call for filterItems, optionally delayed. + *
2 = deleteConfirmed when Undo timeout is over. + *
8 = hide the progress item from the list, optionally delayed. + *

Note: numbers 0-9 are reserved for the Adapter, use others.

+ * + * @since 5.0.0-rc1 + */ + public class HandlerCallback implements Handler.Callback { + + @CallSuper + @Override + public boolean handleMessage(Message message) { + switch (message.what) { + case UPDATE: //updateDataSet OR + case FILTER: //filterItems + if (mFilterAsyncTask != null) mFilterAsyncTask.cancel(true); + mFilterAsyncTask = new FilterAsyncTask(message.what, (List) message.obj); + mFilterAsyncTask.execute(); + return true; + case CONFIRM_DELETE: //confirm delete + OnDeleteCompleteListener listener = (OnDeleteCompleteListener) message.obj; + if (listener != null) listener.onDeleteConfirmed(); + emptyBin(); + return true; + case LOAD_MORE_COMPLETE: //hide progress item + hideProgressItem(); + return true; + } + return false; + } + } + + /** + * The old and new lists are available as: + *

- {@code protected List oldItems;} + *
- {@code protected List newItems;} + * + * @deprecated DiffUtil is slower than the internal implementation, you can use it until + * final release. + */ + @Deprecated + public static class DiffUtilCallback extends DiffUtil.Callback { + + protected List oldItems; + protected List newItems; + + public final void setItems(List oldItems, List newItems) { + this.oldItems = oldItems; + this.newItems = newItems; + } + + public final List getNewItems() { + return newItems; + } + + @Override + public final int getOldListSize() { + return oldItems.size(); + } + + @Override + public final int getNewListSize() { + return newItems.size(); + } + + /** + * Called by the DiffUtil to decide whether two object represent the same item. + *

+ * For example, if your items have unique ids, this method should check their id equality. + * + * @param oldItemPosition The position of the item in the old list + * @param newItemPosition The position of the item in the new list + * @return True if the two items represent the same object or false if they are different. + */ + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + T oldItem = oldItems.get(oldItemPosition); + T newItem = newItems.get(newItemPosition); + return oldItem.equals(newItem); + } + + /** + * Called by the DiffUtil when it wants to check whether two items have the same data. + * DiffUtil uses this information to detect if the contents of an item has changed. + *

+ * DiffUtil uses this method to check equality instead of {@link Object#equals(Object)} + * so that you can change its behavior depending on your UI. + * For example, if you are using DiffUtil with a + * {@link RecyclerView.Adapter RecyclerView.Adapter}, you should + * return whether the items' visual representations are the same. + *

+ * This method is called only if {@link #areItemsTheSame(int, int)} returns + * {@code true} for these items. + * + * @param oldItemPosition The position of the item in the old list + * @param newItemPosition The position of the item in the new list which replaces the + * oldItem + * @return True if the contents of the items are the same or false if they are different. + */ + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return false; + } + + /** + * When {@link #areItemsTheSame(int, int)} returns {@code true} for two items and + * {@link #areContentsTheSame(int, int)} returns false for them, DiffUtil + * calls this method to get a payload about the change. + *

+ * For example, if you are using DiffUtil with {@link RecyclerView}, you can return the + * particular field that changed in the item and your + * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that + * information to run the correct animation. + *

+ * Default implementation returns {@code null}. + * + * @param oldItemPosition The position of the item in the old list + * @param newItemPosition The position of the item in the new list + * @return A payload object that represents the change between the two items. + */ + @Nullable + @Override + public Object getChangePayload(int oldItemPosition, int newItemPosition) { + return Payload.CHANGE; + } } } \ No newline at end of file diff --git a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/Payload.java b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/Payload.java index 058d5e6e..feef4913 100644 --- a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/Payload.java +++ b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/Payload.java @@ -17,8 +17,8 @@ /** * Payload occurs only for {@code notifyItemChanged()}. - *

The value of this enumeration will be passed to the bind method to optimize the view binding - * in order to update only the inner views interested for the change.

+ *

The value of this enumeration will be provided to the bind method to optimize the view + * binding in order to update only the inner views interested for the change.

* You can still pass your own Object instead of one of these values. * * @author Davide Steduto @@ -46,5 +46,9 @@ public enum Payload { /** when items are notified due to Selecting All items / Clearing Selection) */ SELECTION, /** when items are notified after a split */ - SPLIT + SPLIT, + /** when an item is expanded by the user */ + EXPANDED, + /** when an item is collapsed by the user */ + COLLAPSED } \ No newline at end of file diff --git a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/SelectableAdapter.java b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/SelectableAdapter.java index b28a5691..f1c018c3 100644 --- a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/SelectableAdapter.java +++ b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/SelectableAdapter.java @@ -18,9 +18,8 @@ import android.os.Bundle; import android.support.annotation.IntDef; import android.support.annotation.NonNull; -import android.support.v7.widget.GridLayoutManager; +import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.StaggeredGridLayoutManager; import android.util.Log; import android.view.View; @@ -28,6 +27,8 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; @@ -64,7 +65,8 @@ public abstract class SelectableAdapter extends RecyclerView.Adapter public static final int MODE_IDLE = 0, MODE_SINGLE = 1, MODE_MULTI = 2; /** - * Annotation interface for selection modes. + * Annotation interface for selection modes: + * {@link #MODE_IDLE}, {@link #MODE_SINGLE}, {@link #MODE_MULTI} */ @IntDef({MODE_IDLE, MODE_SINGLE, MODE_MULTI}) @Retention(RetentionPolicy.SOURCE) @@ -72,10 +74,17 @@ public abstract class SelectableAdapter extends RecyclerView.Adapter } private Set mSelectedPositions; + private Set mBoundViewHolders; private int mMode; protected RecyclerView mRecyclerView; protected FastScroller mFastScroller; + /** + * Flag when fast scrolling is active. + *

Used to know if user is fast scrolling.

+ */ + protected boolean isFastScroll = false; + /** * ActionMode selection flag SelectAll. *

Used when user click on selectAll action button in ActionMode.

@@ -96,7 +105,9 @@ public abstract class SelectableAdapter extends RecyclerView.Adapter * @since 1.0.0 */ public SelectableAdapter() { + Log.i("FlexibleAdapter", "Running version " + BuildConfig.VERSION_NAME); mSelectedPositions = new TreeSet<>(); + mBoundViewHolders = new HashSet<>(); mMode = MODE_IDLE; } @@ -149,23 +160,6 @@ public RecyclerView getRecyclerView() { return mRecyclerView; } - /** - * Helper method to return the number of the columns (span count) of the given LayoutManager. - *

All Layouts are supported.

- * - * @param layoutManager the layout manager to check - * @return the span count - * @since 5.0.0-b7 - */ - public static int getSpanCount(RecyclerView.LayoutManager layoutManager) { - if (layoutManager instanceof GridLayoutManager) { - return ((GridLayoutManager) layoutManager).getSpanCount(); - } else if (layoutManager instanceof StaggeredGridLayoutManager) { - return ((StaggeredGridLayoutManager) layoutManager).getSpanCount(); - } - return 1; - } - /** * Sets the mode of the selection: *
    @@ -180,10 +174,11 @@ public static int getSpanCount(RecyclerView.LayoutManager layoutManager) { * @since 2.0.0 */ public void setMode(@Mode int mode) { + if (DEBUG) Log.i(TAG, Utils.getModeName(mode) + " enabled"); if (mMode == MODE_SINGLE && mode == MODE_IDLE) clearSelection(); this.mMode = mode; - mLastItemInActionMode = (mode == MODE_IDLE); + this.mLastItemInActionMode = (mode != MODE_MULTI); } /** @@ -205,26 +200,37 @@ public int getMode() { * @since 5.0.0-b1 */ public boolean isSelectAll() { + // Reset the flags with delay + resetActionModeFlags(); return mSelectAll; } /** - * @return true if user returns to {@link #MODE_IDLE} and no selection is active, false otherwise + * @return true if user returns to {@link #MODE_IDLE} or {@link #MODE_SINGLE} and no + * selection is active, false otherwise * @since 5.0.0-b1 */ public boolean isLastItemInActionMode() { + // Reset the flags with delay + resetActionModeFlags(); return mLastItemInActionMode; } /** - * Reset to false the ActionMode flags: {@code SelectAll} and {@code LastItemInActionMode}. - *

    IMPORTANT: To be called with delay in {@code holder.itemView.postDelayed()}.

    + * Resets to false the ActionMode flags: {@code SelectAll} and {@code LastItemInActionMode}. * * @since 5.0.0-b1 */ - public void resetActionModeFlags() { - this.mSelectAll = false; - this.mLastItemInActionMode = false; + private void resetActionModeFlags() { + if (mSelectAll || mLastItemInActionMode) { + mRecyclerView.postDelayed(new Runnable() { + @Override + public void run() { + mSelectAll = false; + mLastItemInActionMode = false; + } + }, 200L); + } } /** @@ -289,10 +295,21 @@ public void toggleSelection(int position) { * @see #isSelectable(int) * @since 5.0.0-b7 */ - public boolean addSelection(int position) { + public final boolean addSelection(int position) { return isSelectable(position) && mSelectedPositions.add(position); } + /** + * This method is used only internally to force adjust selection. + * + * @param position Position of the item to add the selection status for. + * @return true if the set is modified, false otherwise + * @since 5.0.0-rc1 + */ + final boolean addAdjustedSelection(int position) { + return mSelectedPositions.add(position); + } + /** * Removes the selection status for the given position without notifying the change. * @@ -300,7 +317,7 @@ public boolean addSelection(int position) { * @return true if the set is modified, false otherwise * @since 5.0.0-b7 */ - public boolean removeSelection(int position) { + public final boolean removeSelection(int position) { return mSelectedPositions.remove(position); } @@ -339,7 +356,7 @@ public void selectAll(Integer... viewTypes) { mSelectedPositions.add(i); itemCount++; } else { - //Optimization for ItemRangeChanged + // Optimization for ItemRangeChanged if (positionStart + itemCount == i) { notifySelectionChanged(positionStart, itemCount); itemCount = 0; @@ -364,26 +381,66 @@ public void clearSelection() { if (DEBUG) Log.d(TAG, "clearSelection " + mSelectedPositions); Iterator iterator = mSelectedPositions.iterator(); int positionStart = 0, itemCount = 0; - //The notification is done only on items that are currently selected. + // The notification is done only on items that are currently selected. while (iterator.hasNext()) { int position = iterator.next(); iterator.remove(); - //Optimization for ItemRangeChanged + // Optimization for ItemRangeChanged if (positionStart + itemCount == position) { itemCount++; } else { - //Notify previous items in range + // Notify previous items in range notifySelectionChanged(positionStart, itemCount); positionStart = position; itemCount = 1; } } - //Notify remaining items in range + // Notify remaining items in range notifySelectionChanged(positionStart, itemCount); } private void notifySelectionChanged(int positionStart, int itemCount) { - if (itemCount > 0) notifyItemRangeChanged(positionStart, itemCount, Payload.SELECTION); + if (itemCount > 0) { + // Avoid to rebind the VH, direct call to the itemView activation + for (FlexibleViewHolder holder : mBoundViewHolders) { + if (isSelectable(holder.getAdapterPosition())) + holder.toggleActivation(); + } + // Use classic notification, in case FlexibleViewHolder is not implemented + if (mBoundViewHolders.isEmpty()) + notifyItemRangeChanged(positionStart, itemCount, Payload.SELECTION); + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) { + // When user scrolls, this line binds the correct selection status + holder.itemView.setActivated(isSelected(position)); + // Bind the correct view elevation + if (holder instanceof FlexibleViewHolder) { + FlexibleViewHolder flexHolder = (FlexibleViewHolder) holder; + if (holder.itemView.isActivated() && flexHolder.getActivationElevation() > 0) + ViewCompat.setElevation(holder.itemView, flexHolder.getActivationElevation()); + else if (flexHolder.getActivationElevation() > 0) //Leave unaltered the default elevation + ViewCompat.setElevation(holder.itemView, 0); + mBoundViewHolders.add(flexHolder); + } + } + + @Override + public void onViewRecycled(RecyclerView.ViewHolder holder) { + if (holder instanceof FlexibleViewHolder) + mBoundViewHolders.remove(holder); + } + + /** + * Usually {@code RecyclerView} binds 3 items more than the visible items. + * + * @return a Set with all bound FlexibleViewHolders + * @since 5.0.0-rc1 + */ + public Set getAllBoundViewHolders() { + return Collections.unmodifiableSet(mBoundViewHolders); } /** @@ -415,17 +472,6 @@ public List getSelectedPositions() { */ // public Set getSelectedPositionsAsSet() { // return mSelectedPositions; -// } - - /** - * Sorts and retrieves the list of selected items. - *

    To call once! Then call {@link #getSelectedPositions()}.

    - * - * @return Ordered list of selected items ids - */ -// public List getSortedSelectedPositions() { -// Collections.sort(mSelectedPositions); -// return mSelectedPositions; // } /*----------------*/ @@ -440,6 +486,8 @@ public List getSelectedPositions() { */ public void onSaveInstanceState(Bundle outState) { outState.putIntegerArrayList(TAG, new ArrayList<>(mSelectedPositions)); + if (DEBUG && getSelectedItemCount() > 0) + Log.d(TAG, "Saving selection " + mSelectedPositions); } /** @@ -450,7 +498,8 @@ public void onSaveInstanceState(Bundle outState) { */ public void onRestoreInstanceState(Bundle savedInstanceState) { mSelectedPositions.addAll(savedInstanceState.getIntegerArrayList(TAG)); - Log.d(TAG, "Restore selection " + mSelectedPositions); + if (DEBUG && getSelectedItemCount() > 0) + Log.d(TAG, "Restore selection " + mSelectedPositions); } /*---------------*/ @@ -546,7 +595,7 @@ public String onCreateBubbleText(int position) { */ @Override public void onFastScrollerStateChange(boolean scrolling) { - //nothing + isFastScroll = scrolling; } } \ No newline at end of file diff --git a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/common/FlexibleItemAnimator.java b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/common/FlexibleItemAnimator.java index befe8248..aad73c32 100644 --- a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/common/FlexibleItemAnimator.java +++ b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/common/FlexibleItemAnimator.java @@ -791,14 +791,10 @@ private static void clear(View v) { * If the payload list is not empty, DefaultItemAnimator returns true. * When this is the case: *
      - *
    • If you override {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}, both - * ViewHolder arguments will be the same instance. - *
    • - *
    • - * If you are not overriding {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}, - * then DefaultItemAnimator will call {@link #animateMove(ViewHolder, int, int, int, int)} and - * run a move animation instead. - *
    • + *
    • If you override {@code animateChange()}, both ViewHolder arguments will be the same + * instance.
    • + *
    • If you are not overriding {@code animateChange()}, then DefaultItemAnimator will call + * {@code animateMove()} and run a move animation instead.
    • *
    */ @Override diff --git a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/ActionModeHelper.java b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/ActionModeHelper.java index df11e797..83a75fa0 100644 --- a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/ActionModeHelper.java +++ b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/ActionModeHelper.java @@ -25,6 +25,8 @@ import android.view.Menu; import android.view.MenuItem; +import java.util.List; + import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.SelectableAdapter; import eu.davidea.flexibleadapter.SelectableAdapter.Mode; @@ -99,6 +101,21 @@ public ActionMode getActionMode() { return mActionMode; } + /** + * Gets the activated position only when mode is {@code MODE_SINGLE}. + * + * @return the activated position when {@code MODE_SINGLE}. -1 if no item is selected + * @since 5.0.0-rc1 + */ + public int getActivatedPosition() { + List selectedPositions = mAdapter.getSelectedPositions(); + if (mAdapter.getMode() == SelectableAdapter.MODE_SINGLE && + selectedPositions.size() == 1) { + return selectedPositions.get(0); + } + return RecyclerView.NO_POSITION; + } + /** * Implements the basic behavior of a CAB and multi select behavior. * @@ -125,11 +142,11 @@ public boolean onClick(int position) { */ @NonNull public ActionMode onLongClick(AppCompatActivity activity, int position) { - //Activate ActionMode + // Activate ActionMode if (mActionMode == null) { mActionMode = activity.startSupportActionMode(this); } - //we have to select this on our own as we will consume the event + // We have to select this on our own as we will consume the event toggleSelection(position); return mActionMode; } @@ -143,11 +160,12 @@ public ActionMode onLongClick(AppCompatActivity activity, int position) { * @since 5.0.0-b6 */ public void toggleSelection(int position) { - if (position >= 0 && (mAdapter.getMode() == SelectableAdapter.MODE_SINGLE || + if (position >= 0 && ( + (mAdapter.getMode() == SelectableAdapter.MODE_SINGLE && !mAdapter.isSelected(position)) || mAdapter.getMode() == SelectableAdapter.MODE_MULTI)) { mAdapter.toggleSelection(position); } - //If MODE_SINGLE is active then ActionMode can be null + // If MODE_SINGLE is active then ActionMode can be null if (mActionMode == null) return; int count = mAdapter.getSelectedItemCount(); @@ -172,11 +190,11 @@ public void updateContextTitle(int count) { } /** - * Helper method to restart the action mode after a restoration of deleted items. The - * ActionMode will be activated only if {@link FlexibleAdapter#getSelectedItemCount()} - * has selection. - *

    To be called in the onUndo method after the restoration is done or in the - * onRestoreInstanceState.

    + * Helper method to restart the action mode after a restoration of deleted items and after + * screen rotation. The ActionMode will be activated only if + * {@link FlexibleAdapter#getSelectedItemCount()} has selections. + *

    To be called in the {@code onUndo} method after the restoration is done or at the end + * of {@code onRestoreInstanceState}.

    * * @param activity the current Activity * @since 5.0.0-b6 @@ -191,12 +209,12 @@ public void restoreSelection(AppCompatActivity activity) { @CallSuper @Override public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { - //Inflate the Context Menu + // Inflate the Context Menu actionMode.getMenuInflater().inflate(mCabMenu, menu); if (SelectableAdapter.DEBUG) Log.i(TAG, "ActionMode is active!"); - //Activate the ActionMode Multi + // Activate the ActionMode Multi mAdapter.setMode(SelectableAdapter.MODE_MULTI); - //Notify the provided callback + // Notify the provided callback return mCallback == null || mCallback.onCreateActionMode(actionMode, menu); } @@ -214,7 +232,7 @@ public boolean onActionItemClicked(ActionMode actionMode, MenuItem item) { consumed = mCallback.onActionItemClicked(actionMode, item); } if (!consumed) { - //Finish the actionMode + // Finish the actionMode actionMode.finish(); } return consumed; @@ -230,11 +248,11 @@ public boolean onActionItemClicked(ActionMode actionMode, MenuItem item) { public void onDestroyActionMode(ActionMode actionMode) { if (SelectableAdapter.DEBUG) Log.i(TAG, "ActionMode is about to be destroyed! New mode will be " + defaultMode); - //Change mode and deselect everything + // Change mode and deselect everything mAdapter.setMode(defaultMode); mAdapter.clearSelection(); mActionMode = null; - //Notify the provided callback + // Notify the provided callback if (mCallback != null) { mCallback.onDestroyActionMode(actionMode); } diff --git a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/AnimatorHelper.java b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/AnimatorHelper.java index 61967172..98ca955c 100644 --- a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/AnimatorHelper.java +++ b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/AnimatorHelper.java @@ -18,6 +18,7 @@ import android.animation.Animator; import android.animation.ObjectAnimator; import android.support.annotation.FloatRange; +import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; @@ -36,10 +37,6 @@ public class AnimatorHelper { protected static final String TAG = AnimatorHelper.class.getSimpleName(); -// private enum AnimatorEnum { -// ALPHA, SLIDE_IN_LEFT, SLIDE_IN_RIGHT, SLIDE_IN_BOTTOM, SLIDE_IN_TOP, SCALE -// } - /*-----------*/ /* ANIMATORS */ /*-----------*/ @@ -47,7 +44,7 @@ public class AnimatorHelper { /** * This is the default animator. * - * @param animators user defined list + * @param animators user defined list of animators * @param view itemView to animate * @param alphaFrom starting alpha value * @since 5.0.0-b1 @@ -61,7 +58,7 @@ public static void alphaAnimator( /** * Item will slide from Left to Right. * - * @param animators user defined list + * @param animators user defined list of animators * @param view itemView to animate * @param percent any % multiplier (between 0 and 1) of the LayoutManager Width * @since 5.0.0-b1 @@ -71,13 +68,13 @@ public static void slideInFromLeftAnimator( RecyclerView recyclerView, @FloatRange(from = 0.0, to= 1.0) float percent) { alphaAnimator(animators, view, 0f); animators.add(ObjectAnimator.ofFloat(view, "translationX", -recyclerView.getLayoutManager().getWidth() * percent, 0)); - if (FlexibleAdapter.DEBUG) Log.v(TAG, "Added LEFT Animator"); + if (FlexibleAdapter.DEBUG) Log.v(TAG, " Added LEFT Animator"); } /** * Item will slide from Right to Left. * - * @param animators user defined list + * @param animators user defined list of animators * @param view ItemView to animate * @param percent Any % multiplier (between 0 and 1) of the LayoutManager Width * @since 5.0.0-b1 @@ -87,13 +84,13 @@ public static void slideInFromRightAnimator( RecyclerView recyclerView, @FloatRange(from = 0.0, to = 1.0) float percent) { alphaAnimator(animators, view, 0f); animators.add(ObjectAnimator.ofFloat(view, "translationX", recyclerView.getLayoutManager().getWidth() * percent, 0)); - if (FlexibleAdapter.DEBUG) Log.v(TAG, "Added RIGHT Animator"); + if (FlexibleAdapter.DEBUG) Log.v(TAG, " Added RIGHT Animator"); } /** * Item will slide from Top of the screen to its natural position. * - * @param animators user defined list + * @param animators user defined list of animators * @param view itemView to animate * @since 5.0.0-b7 */ @@ -102,13 +99,13 @@ public static void slideInFromTopAnimator( RecyclerView recyclerView) { alphaAnimator(animators, view, 0f); animators.add(ObjectAnimator.ofFloat(view, "translationY", -recyclerView.getMeasuredHeight() >> 1, 0)); - if (FlexibleAdapter.DEBUG) Log.v(TAG, "Added TOP Animator"); + if (FlexibleAdapter.DEBUG) Log.v(TAG, " Added TOP Animator"); } /** * Item will slide from Bottom of the screen to its natural position. * - * @param animators user defined list + * @param animators user defined list of animators * @param view itemView to animate * @since 5.0.0-b1 */ @@ -117,13 +114,13 @@ public static void slideInFromBottomAnimator( RecyclerView recyclerView) { alphaAnimator(animators, view, 0f); animators.add(ObjectAnimator.ofFloat(view, "translationY", recyclerView.getMeasuredHeight() >> 1, 0)); - if (FlexibleAdapter.DEBUG) Log.v(TAG, "Added BOTTOM Animator"); + if (FlexibleAdapter.DEBUG) Log.v(TAG, " Added BOTTOM Animator"); } /** * Item will scale to {@code 1.0f}. * - * @param animators user defined list + * @param animators user defined list of animators * @param view itemView to animate * @param scaleFrom initial scale value * @since 5.0.0-b1 @@ -133,7 +130,33 @@ public static void scaleAnimator( alphaAnimator(animators, view, 0f); animators.add(ObjectAnimator.ofFloat(view, "scaleX", scaleFrom, 1f)); animators.add(ObjectAnimator.ofFloat(view, "scaleY", scaleFrom, 1f)); - if (FlexibleAdapter.DEBUG) Log.v(TAG, "Added SCALE Animator"); + if (FlexibleAdapter.DEBUG) Log.v(TAG, " Added SCALE Animator"); + } + + /** + * Item will flip from {@code 0.0f} to {@code 1.0f}. + * + * @param animators user defined list of animators + * @param view itemView to animate + * @since 5.0.0-rc1 + */ + public static void flipAnimator(@NonNull List animators, @NonNull View view) { + alphaAnimator(animators, view, 0f); + animators.add(ObjectAnimator.ofFloat(view, "scaleY", 0f, 1f)); + if (FlexibleAdapter.DEBUG) Log.v(TAG, " Added FLIP Animator"); + } + + /** + * Adds a custom duration to the current view. + * + * @param animators user defined list of animators + * @param duration duration in milliseconds + */ + public static void setDuration(@NonNull List animators, @IntRange(from = 0) long duration) { + if (animators.size() > 0) { + Animator animator = animators.get(animators.size() - 1); + animator.setDuration(duration); + } } } \ No newline at end of file diff --git a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/ItemTouchHelperCallback.java b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/ItemTouchHelperCallback.java index 97225023..c4e3fb7c 100644 --- a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/ItemTouchHelperCallback.java +++ b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/ItemTouchHelperCallback.java @@ -32,21 +32,22 @@ import eu.davidea.viewholders.FlexibleViewHolder; /** - * This class is an implementation of {@link Callback} that enables drag & drop - * and swipe actions. Drag and Swipe events are started depending by its configuration. + * This class is an implementation of {@link Callback} that enables Drag & Drop + * and Swipe actions. Drag and Swipe events are started depending by the configuration. * * @author Davide Steduto * @since 23/01/2016 Created */ +@SuppressWarnings({"WeakerAccess", "unused"}) public class ItemTouchHelperCallback extends Callback { - private static final float ALPHA_FULL = 1.0f; + protected static final float ALPHA_FULL = 1.0f; - private AdapterCallback mItemTouchCallback; - private boolean mIsLongPressDragEnabled = false, mIsSwipeEnabled = false; - private long mSwipeAnimationDuration = 300L, mDragAnimationDuration = 400L; - private float mSwipeThreshold = 0.5f, mMoveThreshold = 0.5f; - private int mSwipeFlags = -1; + protected AdapterCallback mItemTouchCallback; + protected boolean longPressDragEnabled = false, handleDragEnabled = false, swipeEnabled = false; + protected long mSwipeAnimationDuration = 300L, mDragAnimationDuration = 400L; + protected float mSwipeThreshold = 0.5f, mMoveThreshold = 0.5f; + protected int mSwipeFlags = -1; /*-------------*/ /* CONSTRUCTOR */ @@ -59,16 +60,17 @@ public ItemTouchHelperCallback(AdapterCallback itemTouchCallback) { /*-----------------------*/ /* CONFIGURATION SETTERS */ /*-----------------------*/ + /* DRAG */ /** - * Enable / disable the drag operation with long press on the ViewHolder. + * Enable / Disable the drag operation with long press on the ViewHolder. *

    Default value is {@code false}.

    * * @param isLongPressDragEnabled true to enable, false to disable */ public void setLongPressDragEnabled(boolean isLongPressDragEnabled) { - this.mIsLongPressDragEnabled = isLongPressDragEnabled; + this.longPressDragEnabled = isLongPressDragEnabled; } /** @@ -76,7 +78,25 @@ public void setLongPressDragEnabled(boolean isLongPressDragEnabled) { */ @Override public boolean isLongPressDragEnabled() { - return mIsLongPressDragEnabled; + return longPressDragEnabled; + } + + /** + * @return true if handle drag is enabled, false otherwise + */ + public boolean isHandleDragEnabled() { + return handleDragEnabled; + } + + /** + * Enable / Disable the drag of the itemView with a handle view. + *

    Default value is {@code false}.

    + * + * @param handleDragEnabled true to activate, false to disable + * @since 5.0.0-b1 + */ + public void setHandleDragEnabled(boolean handleDragEnabled) { + this.handleDragEnabled = handleDragEnabled; } /** @@ -84,7 +104,7 @@ public boolean isLongPressDragEnabled() { */ @Override public boolean canDropOver(RecyclerView recyclerView, RecyclerView.ViewHolder current, RecyclerView.ViewHolder target) { - return current.getItemViewType() == target.getItemViewType(); + return true; } /** @@ -116,7 +136,7 @@ public float getMoveThreshold(RecyclerView.ViewHolder viewHolder) { * @param isSwipeEnabled true to enable swipe, false to disable */ public void setSwipeEnabled(boolean isSwipeEnabled) { - this.mIsSwipeEnabled = isSwipeEnabled; + this.swipeEnabled = isSwipeEnabled; } /** @@ -124,7 +144,7 @@ public void setSwipeEnabled(boolean isSwipeEnabled) { */ @Override public boolean isItemViewSwipeEnabled() { - return mIsSwipeEnabled; + return swipeEnabled; } /** diff --git a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/StickyHeaderHelper.java b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/StickyHeaderHelper.java index 46403f05..376263f5 100644 --- a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/StickyHeaderHelper.java +++ b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/StickyHeaderHelper.java @@ -16,18 +16,22 @@ package eu.davidea.flexibleadapter.helpers; import android.animation.Animator; +import android.support.v4.view.ViewCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.OrientationHelper; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.OnScrollListener; import android.support.v7.widget.StaggeredGridLayoutManager; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.widget.FrameLayout; import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.FlexibleAdapter.OnStickyHeaderChangeListener; +import eu.davidea.flexibleadapter.R; import eu.davidea.flexibleadapter.items.IHeader; import eu.davidea.flexibleadapter.utils.Utils; import eu.davidea.viewholders.FlexibleViewHolder; @@ -38,9 +42,9 @@ * * @since 25/03/2016 Created */ -public class StickyHeaderHelper extends OnScrollListener { +public final class StickyHeaderHelper extends OnScrollListener { - private static final String TAG = FlexibleAdapter.class.getSimpleName(); + private static final String TAG = StickyHeaderHelper.class.getSimpleName(); private FlexibleAdapter mAdapter; private RecyclerView mRecyclerView; @@ -48,85 +52,76 @@ public class StickyHeaderHelper extends OnScrollListener { private FlexibleViewHolder mStickyHeaderViewHolder; private OnStickyHeaderChangeListener mStickyHeaderChangeListener; private int mHeaderPosition = RecyclerView.NO_POSITION; - + private boolean displayWithAnimation = false; + private float mElevation; public StickyHeaderHelper(FlexibleAdapter adapter, - OnStickyHeaderChangeListener stickyHeaderChangeListener) { + OnStickyHeaderChangeListener stickyHeaderChangeListener, + ViewGroup stickyHolderLayout) { mAdapter = adapter; mStickyHeaderChangeListener = stickyHeaderChangeListener; + mStickyHolderLayout = stickyHolderLayout; } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + displayWithAnimation = mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE; updateOrClearHeader(false); } - public boolean isAttachedToRecyclerView() { - return mRecyclerView != null; - } - public void attachToRecyclerView(RecyclerView parent) { if (mRecyclerView != null) { mRecyclerView.removeOnScrollListener(this); clearHeader(); } - mRecyclerView = parent; - if (mRecyclerView != null) { - mRecyclerView.addOnScrollListener(this); - mRecyclerView.post(new Runnable() { - @Override - public void run() { - initStickyHeadersHolder(); - } - }); + if (parent == null) { + throw new IllegalStateException("Adapter is not attached to RecyclerView. Enable sticky headers after setting adapter to RecyclerView."); } + mRecyclerView = parent; + mRecyclerView.addOnScrollListener(this); + initStickyHeadersHolder(); } - public void detachFromRecyclerView(RecyclerView parent) { - if (mRecyclerView == parent) { - mRecyclerView.removeOnScrollListener(this); - mRecyclerView = null; - mStickyHolderLayout.animate().setListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - } - - @Override - public void onAnimationEnd(Animator animation) { - clearHeader(); - } + public void detachFromRecyclerView() { + mRecyclerView.removeOnScrollListener(this); + mRecyclerView = null; + clearHeaderWithAnimation(); + if (FlexibleAdapter.DEBUG) Log.i(TAG, "StickyHolderLayout detached"); + } - @Override - public void onAnimationCancel(Animator animation) { - } + private FrameLayout createContainer(int width, int height) { + FrameLayout frameLayout = new FrameLayout(mRecyclerView.getContext()); + frameLayout.setLayoutParams(new ViewGroup.LayoutParams(width, height)); + return frameLayout; + } - @Override - public void onAnimationRepeat(Animator animation) { - } - }); - mStickyHolderLayout.animate().alpha(0).start(); - if (FlexibleAdapter.DEBUG) Log.i(TAG, "StickyHolderLayout detached"); - } + private ViewGroup getParent(View view) { + return (ViewGroup) view.getParent(); } private void initStickyHeadersHolder() { - //Initialize Holder Layout and show sticky header if exists already - mStickyHolderLayout = mAdapter.getStickySectionHeadersHolder(); - if (mStickyHolderLayout != null) { - if (mStickyHolderLayout.getLayoutParams() == null) { - throw new IllegalStateException("The ViewGroup provided, doesn't have LayoutParams correctly set, please initialize the ViewGroup accordingly"); - } - mStickyHolderLayout.setClipToPadding(false); - mStickyHolderLayout.setAlpha(0); - updateOrClearHeader(false); - mStickyHolderLayout.animate().alpha(1).start(); - if (FlexibleAdapter.DEBUG) Log.i(TAG, "StickyHolderLayout initialized"); - } else { - Log.w(TAG, "WARNING! ViewGroup for Sticky Headers unspecified! You must include @layout/sticky_header_layout or implement FlexibleAdapter.getStickySectionHeadersHolder() method"); + if (mStickyHolderLayout == null) { + // Create stickyContainer for shadow elevation + FrameLayout stickyContainer = createContainer( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + ViewGroup oldParentLayout = getParent(mRecyclerView); + oldParentLayout.addView(stickyContainer); + // Initialize Holder Layout + mStickyHolderLayout = (ViewGroup) LayoutInflater.from(mRecyclerView.getContext()).inflate(R.layout.sticky_header_layout, stickyContainer); + if (FlexibleAdapter.DEBUG) Log.i(TAG, "Default StickyHolderLayout initialized"); + } else if (FlexibleAdapter.DEBUG) { + Log.i(TAG, "User defined StickyHolderLayout initialized"); } + // Show sticky header if exists already + updateOrClearHeader(false); + } + + public int getStickyPosition() { + return mHeaderPosition; } - public boolean hasStickyHeaderTranslated(int position) { + private boolean hasStickyHeaderTranslated(int position) { RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(position); return vh != null && (vh.itemView.getX() < 0 || vh.itemView.getY() < 0); } @@ -138,13 +133,13 @@ private void onStickyHeaderChange(int sectionIndex) { } public void updateOrClearHeader(boolean updateHeaderContent) { - if (mStickyHolderLayout == null || mAdapter.hasSearchText() || - mRecyclerView == null || mRecyclerView.getChildCount() == 0) { - clearHeader(); + // + if (!mAdapter.areHeadersShown() || mAdapter.hasSearchText() || mAdapter.getItemCount() == 0) { + clearHeaderWithAnimation(); return; } - int firstHeaderPosition = getHeaderPosition(RecyclerView.NO_POSITION); - if (firstHeaderPosition >= 0 && firstHeaderPosition < mAdapter.getItemCount()) { + int firstHeaderPosition = getStickyPosition(RecyclerView.NO_POSITION); + if (firstHeaderPosition >= 0) { updateHeader(firstHeaderPosition, updateHeaderContent); } else { clearHeader(); @@ -154,6 +149,17 @@ public void updateOrClearHeader(boolean updateHeaderContent) { private void updateHeader(int headerPosition, boolean updateHeaderContent) { // Check if there is a new header to be sticky if (mHeaderPosition != headerPosition) { + // #244 - Don't animate if header is already visible at the first layout position + int firstVisibleItemPosition = Utils.findFirstVisibleItemPosition(mRecyclerView.getLayoutManager()); + // Animate if headers were hidden, but don't if configuration changed (rotation) + if (displayWithAnimation && mHeaderPosition == RecyclerView.NO_POSITION && + headerPosition != firstVisibleItemPosition) { + displayWithAnimation = false; + mStickyHolderLayout.setAlpha(0); + mStickyHolderLayout.animate().alpha(1).start(); + } else { + mStickyHolderLayout.setAlpha(1); + } mHeaderPosition = headerPosition; FlexibleViewHolder holder = getHeaderViewHolder(headerPosition); if (FlexibleAdapter.DEBUG) @@ -166,37 +172,57 @@ private void updateHeader(int headerPosition, boolean updateHeaderContent) { translateHeader(); } - private void translateHeader() { - if (mStickyHeaderViewHolder == null) return; + private void configureLayoutElevation() { + // 1. Take elevation from header item layout (most important) + mElevation = ViewCompat.getElevation(mStickyHeaderViewHolder.getContentView()); + if (mElevation == 0f) { + // 2. Take elevation settings + mElevation = mAdapter.getStickyHeaderElevation(); + } + if (mElevation > 0) { + // Needed to elevate the view + ViewCompat.setBackground(mStickyHolderLayout, mStickyHeaderViewHolder.getContentView().getBackground()); + } + } + private void translateHeader() { + // Sticky at zero offset (no translation) int headerOffsetX = 0, headerOffsetY = 0; + // Get calculated elevation + float elevation = mElevation; - //Search for the position where the next header item is found and take the new offset + // Search for the position where the next header item is found and translate the new offset for (int i = 0; i < mRecyclerView.getChildCount(); i++) { final View nextChild = mRecyclerView.getChildAt(i); if (nextChild != null) { int adapterPos = mRecyclerView.getChildAdapterPosition(nextChild); - int nextHeaderPosition = getHeaderPosition(adapterPos); + int nextHeaderPosition = getStickyPosition(adapterPos); if (mHeaderPosition != nextHeaderPosition) { if (Utils.getOrientation(mRecyclerView.getLayoutManager()) == OrientationHelper.HORIZONTAL) { if (nextChild.getLeft() > 0) { int headerWidth = mStickyHolderLayout.getMeasuredWidth(); - headerOffsetX = Math.min(nextChild.getLeft() - headerWidth, 0); + int nextHeaderOffsetX = nextChild.getLeft() - headerWidth; + headerOffsetX = Math.min(nextHeaderOffsetX, 0); + // Early remove the elevation/shadow to match with the next view + if (nextHeaderOffsetX < 5) elevation = 0f; if (headerOffsetX < 0) break; } } else { if (nextChild.getTop() > 0) { int headerHeight = mStickyHolderLayout.getMeasuredHeight(); - headerOffsetY = Math.min(nextChild.getTop() - headerHeight, 0); + int nextHeaderOffsetY = nextChild.getTop() - headerHeight; + headerOffsetY = Math.min(nextHeaderOffsetY, 0); + // Early remove the elevation/shadow to match with the next view + if (nextHeaderOffsetY < 5) elevation = 0f; if (headerOffsetY < 0) break; } } } } } - //Fix to remove unnecessary shadow - //ViewCompat.setElevation(mStickyHeaderViewHolder.getContentView(), 0f); - //Apply translation + // Apply the user elevation to the sticky container + ViewCompat.setElevation(mStickyHolderLayout, elevation); + // Apply translation (pushed up by another header) mStickyHolderLayout.setTranslationX(headerOffsetX); mStickyHolderLayout.setTranslationY(headerOffsetY); //Log.v(TAG, "TranslationX=" + headerOffsetX + " TranslationY=" + headerOffsetY); @@ -216,42 +242,92 @@ private void swapHeader(FlexibleViewHolder newHeader) { private void ensureHeaderParent() { final View view = mStickyHeaderViewHolder.getContentView(); - //#121 - Make sure the measured height (width for horizontal layout) is kept if + // #121 - Make sure the measured height (width for horizontal layout) is kept if // WRAP_CONTENT has been set for the Header View mStickyHeaderViewHolder.itemView.getLayoutParams().width = view.getMeasuredWidth(); mStickyHeaderViewHolder.itemView.getLayoutParams().height = view.getMeasuredHeight(); - //#139 - Copy xml params instead of Measured params + // Ensure the itemView is hidden to avoid double background + mStickyHeaderViewHolder.itemView.setVisibility(View.INVISIBLE); + // #139 - Copy xml params instead of Measured params ViewGroup.LayoutParams params = mStickyHolderLayout.getLayoutParams(); params.width = view.getLayoutParams().width; params.height = view.getLayoutParams().height; removeViewFromParent(view); - mStickyHolderLayout.setClipToPadding(false); mStickyHolderLayout.addView(view); + configureLayoutElevation(); } - public void clearHeader() { - if (mStickyHeaderViewHolder != null) { - if (FlexibleAdapter.DEBUG) Log.d(TAG, "clearHeader"); - resetHeader(mStickyHeaderViewHolder); - mStickyHolderLayout.setAlpha(1); - mStickyHeaderViewHolder = null; - mHeaderPosition = RecyclerView.NO_POSITION; - onStickyHeaderChange(mHeaderPosition); + /** + * On swing and on fast scroll some header items might still be invisible. We need + * to identify them and restore visibility. + */ + @SuppressWarnings("unchecked") + private void restoreHeaderItemVisibility() { + if (mRecyclerView == null) return; + // Restore every header item visibility + for (int i = 0; i < mRecyclerView.getChildCount(); i++) { + View oldHeader = mRecyclerView.getChildAt(i); + int headerPos = mRecyclerView.getChildAdapterPosition(oldHeader); + if (mAdapter.isHeader(mAdapter.getItem(headerPos))) { + oldHeader.setVisibility(View.VISIBLE); + } } } private void resetHeader(FlexibleViewHolder header) { + restoreHeaderItemVisibility(); + // Clean the header container final View view = header.getContentView(); removeViewFromParent(view); - //Reset transformation on removed header + // Reset translation on removed header view.setTranslationX(0); view.setTranslationY(0); - mStickyHeaderViewHolder.itemView.setVisibility(View.VISIBLE); if (!header.itemView.equals(view)) ((ViewGroup) header.itemView).addView(view); header.setIsRecyclable(true); } + private void clearHeader() { + if (mStickyHeaderViewHolder != null) { + if (FlexibleAdapter.DEBUG) Log.d(TAG, "clearHeader"); + resetHeader(mStickyHeaderViewHolder); + mStickyHolderLayout.setAlpha(0); + mStickyHolderLayout.animate().cancel(); + mStickyHolderLayout.animate().setListener(null); + mStickyHeaderViewHolder = null; + restoreHeaderItemVisibility(); + mHeaderPosition = RecyclerView.NO_POSITION; + onStickyHeaderChange(mHeaderPosition); + } + } + + public void clearHeaderWithAnimation() { + if (mStickyHeaderViewHolder != null && mHeaderPosition != RecyclerView.NO_POSITION) { + mStickyHolderLayout.animate().setListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + mHeaderPosition = RecyclerView.NO_POSITION; + } + + @Override + public void onAnimationEnd(Animator animation) { + displayWithAnimation = true; //This helps after clearing filter + mStickyHolderLayout.setAlpha(0); + clearHeader(); + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }); + mStickyHolderLayout.animate().alpha(0).start(); + } + } + private static void removeViewFromParent(final View view) { final ViewParent parent = view.getParent(); if (parent instanceof ViewGroup) { @@ -260,9 +336,9 @@ private static void removeViewFromParent(final View view) { } @SuppressWarnings("unchecked") - private int getHeaderPosition(int adapterPosHere) { + private int getStickyPosition(int adapterPosHere) { if (adapterPosHere == RecyclerView.NO_POSITION) { - //Fix to display correct sticky header (especially after the searchText is cleared out) + // Fix to display correct sticky header (especially after the searchText is cleared out) if (mRecyclerView.getLayoutManager() instanceof StaggeredGridLayoutManager) { adapterPosHere = ((StaggeredGridLayoutManager) mRecyclerView.getLayoutManager()).findFirstVisibleItemPositions(null)[0]; } else { @@ -273,7 +349,7 @@ private int getHeaderPosition(int adapterPosHere) { } } IHeader header = mAdapter.getSectionHeader(adapterPosHere); - //Header cannot be sticky if it's also an Expandable in collapsed status, RV will raise an exception + // Header cannot be sticky if it's also an Expandable in collapsed status, RV will raise an exception if (header == null || mAdapter.isExpandable(header) && !mAdapter.isExpanded(header)) { return RecyclerView.NO_POSITION; } @@ -289,17 +365,17 @@ private int getHeaderPosition(int adapterPosHere) { */ @SuppressWarnings("unchecked") private FlexibleViewHolder getHeaderViewHolder(int position) { - //Find existing ViewHolder + // Find existing ViewHolder FlexibleViewHolder holder = (FlexibleViewHolder) mRecyclerView.findViewHolderForAdapterPosition(position); if (holder == null) { - //Create and binds a new ViewHolder + // Create and binds a new ViewHolder holder = (FlexibleViewHolder) mAdapter.createViewHolder(mRecyclerView, mAdapter.getItemViewType(position)); mAdapter.bindViewHolder(holder, position); - //Restore the Adapter position + // Restore the Adapter position holder.setBackupPosition(position); - //Calculate width and height + // Calculate width and height int widthSpec; int heightSpec; if (Utils.getOrientation(mRecyclerView.getLayoutManager()) == OrientationHelper.VERTICAL) { @@ -310,7 +386,7 @@ private FlexibleViewHolder getHeaderViewHolder(int position) { heightSpec = View.MeasureSpec.makeMeasureSpec(mRecyclerView.getHeight(), View.MeasureSpec.EXACTLY); } - //Measure and Layout the stickyView + // Measure and Layout the stickyView final View headerView = holder.getContentView(); int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, mRecyclerView.getPaddingLeft() + mRecyclerView.getPaddingRight(), diff --git a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/UndoHelper.java b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/UndoHelper.java index 42439469..146e6af4 100644 --- a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/UndoHelper.java +++ b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/helpers/UndoHelper.java @@ -16,6 +16,8 @@ package eu.davidea.flexibleadapter.helpers; import android.content.Context; +import android.graphics.Color; +import android.support.annotation.ColorInt; import android.support.annotation.IntDef; import android.support.annotation.IntRange; import android.support.annotation.NonNull; @@ -35,6 +37,7 @@ * @author Davide Steduto * @since 30/04/2016 */ +@SuppressWarnings("WeakerAccess") public class UndoHelper extends Snackbar.Callback { /** @@ -65,12 +68,13 @@ public class UndoHelper extends Snackbar.Callback { private FlexibleAdapter mAdapter; private OnActionListener mActionListener; private OnUndoListener mUndoListener; + private @ColorInt int mActionTextColor = Color.TRANSPARENT; /** * Default constructor. - *

    Only from version 5.0.0-b8, by calling this constructor, - * {@link FlexibleAdapter#setPermanentDelete(boolean)} is set {@code false} automatically. + *

    By calling this constructor, {@link FlexibleAdapter#setPermanentDelete(boolean)} + * is set {@code false} automatically. * * @param adapter the instance of {@code FlexibleAdapter} * @param undoListener the callback for the Undo and Delete confirmation @@ -107,15 +111,26 @@ public UndoHelper withAction(@Action int action, @NonNull OnActionListener actio return this; } + /** + * Sets the text color of the action. + * + * @param color the color for the action button + * @return this object, so it can be chained + */ + public UndoHelper withActionTextColor(@ColorInt int color) { + this.mActionTextColor = color; + return this; + } + /** * As {@link #remove(List, View, CharSequence, CharSequence, int)} but with String * resources instead of CharSequence. */ - public Snackbar remove(List positions, @NonNull View mainView, - @StringRes int messageStringResId, @StringRes int actionStringResId, - @IntRange(from = 0) int undoTime) { + public void remove(List positions, @NonNull View mainView, + @StringRes int messageStringResId, @StringRes int actionStringResId, + @IntRange(from = -1) int undoTime) { Context context = mainView.getContext(); - return remove(positions, mainView, context.getString(messageStringResId), + remove(positions, mainView, context.getString(messageStringResId), context.getString(actionStringResId), undoTime); } @@ -131,18 +146,16 @@ public Snackbar remove(List positions, @NonNull View mainView, * @param actionText the action text to display * @param undoTime How long to display the message. Either {@link Snackbar#LENGTH_SHORT} or * {@link Snackbar#LENGTH_LONG} or any custom Integer. - * @return The SnackBar instance to be customized again * @see #remove(List, View, int, int, int) */ @SuppressWarnings("WrongConstant") - public Snackbar remove(List positions, @NonNull View mainView, - CharSequence message, CharSequence actionText, - @IntRange(from = 0) int undoTime) { + public void remove(List positions, @NonNull View mainView, + CharSequence message, CharSequence actionText, + @IntRange(from = -1) int undoTime) { this.mPositions = positions; Snackbar snackbar; if (!mAdapter.isPermanentDelete()) { - snackbar = Snackbar.make(mainView, message, undoTime + 400)//More time due to the animation - .setCallback(this) + snackbar = Snackbar.make(mainView, message, undoTime > 0 ? undoTime + 400 : undoTime) .setAction(actionText, new View.OnClickListener() { @Override public void onClick(View v) { @@ -150,14 +163,14 @@ public void onClick(View v) { mUndoListener.onUndoConfirmed(mAction); } }); - snackbar.show(); - return snackbar; } else { - snackbar = Snackbar.make(mainView, message, undoTime) - .setCallback(this); - snackbar.show(); - return snackbar; + snackbar = Snackbar.make(mainView, message, undoTime); + } + if (mActionTextColor != Color.TRANSPARENT) { + snackbar.setActionTextColor(mActionTextColor); } + snackbar.addCallback(this); + snackbar.show(); } /** @@ -167,15 +180,15 @@ public void onClick(View v) { public void onDismissed(Snackbar snackbar, int event) { if (mAdapter.isPermanentDelete()) return; switch (event) { - case DISMISS_EVENT_ACTION: - //We ignore it, action is performed already - break; case DISMISS_EVENT_SWIPE: case DISMISS_EVENT_MANUAL: case DISMISS_EVENT_TIMEOUT: if (mUndoListener != null) mUndoListener.onDeleteConfirmed(mAction); mAdapter.emptyBin(); + case DISMISS_EVENT_CONSECUTIVE: + case DISMISS_EVENT_ACTION: + default: break; } } @@ -186,13 +199,13 @@ public void onDismissed(Snackbar snackbar, int event) { @Override public void onShown(Snackbar snackbar) { boolean consumed = false; - //Perform the action before deletion + // Perform the action before deletion if (mActionListener != null) consumed = mActionListener.onPreAction(); - //Remove selected items from Adapter list after SnackBar is shown + // Remove selected items from Adapter list after SnackBar is shown if (!consumed) mAdapter.removeItems(mPositions, mPayload); - //Perform the action after the deletion + // Perform the action after the deletion if (mActionListener != null) mActionListener.onPostAction(); - //Here, we can notify the callback only in case of permanent deletion + // Here, we can notify the callback only in case of permanent deletion if (mAdapter.isPermanentDelete() && mUndoListener != null) mUndoListener.onDeleteConfirmed(mAction); } @@ -246,7 +259,7 @@ public interface OnUndoListener { * Called when Undo timeout is over and action must be committed in the user Database. *

    Due to Java Generic, it's too complicated and not well manageable if we pass the * List<T> object.
    - * To get deleted items, use {@link FlexibleAdapter#getDeletedItems()} from the + * So, to get deleted items, use {@link FlexibleAdapter#getDeletedItems()} from the * implementation of this method.

    * * @param action one of {@link UndoHelper#ACTION_REMOVE}, {@link UndoHelper#ACTION_UPDATE} diff --git a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/items/AbstractFlexibleItem.java b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/items/AbstractFlexibleItem.java index 46d54fe8..44b2c637 100644 --- a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/items/AbstractFlexibleItem.java +++ b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/items/AbstractFlexibleItem.java @@ -47,7 +47,7 @@ public abstract class AbstractFlexibleItem /** * You must implement this method to compare items identifiers. *

    Adapter needs this method to distinguish them and pick up correct items.

    - * See + * See * Writing a correct {@code equals} method to implement your own {@code equals} method. *

    Basic Java implementation: *

    diff --git a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/items/IFlexible.java b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/items/IFlexible.java
    index ebe9561d..4ebe0616 100644
    --- a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/items/IFlexible.java
    +++ b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/items/IFlexible.java
    @@ -107,11 +107,11 @@ public interface IFlexible {
     	/*---------------------*/
     
     	/**
    -	 * Returns the layout resource Id to auto-map a specific ViewType on this Item.
    +	 * Returns the layout resource ID to AutoMap a specific ViewType on this Item.
     	 * 

    NOTE: Should identify a resource Layout reference {@link android.R.layout} used - * by FlexibleAdapter to auto-map the ViewTypes.

    - * HELP: To know how to implement auto-map for ViewTypes please refer to the - * FlexibleAdapter WikiPage + * by FlexibleAdapter to AutoMap the ViewTypes.

    + * HELP: To know how to implement AutoMap for ViewTypes please refer to the + * FlexibleAdapter Wiki Page * on GitHub. * * @return Layout identifier @@ -120,9 +120,9 @@ public interface IFlexible { int getLayoutRes(); /** - * Delegates the creation of the ViewHolder to the user, if auto-map has been implemented. - *

    HELP: To know how to implement auto-map for ViewTypes please refer to the - * FlexibleAdapter WikiPage + * Delegates the creation of the ViewHolder to the user if AutoMap has been implemented. + *

    HELP: To know how to implement AutoMap for ViewTypes please refer to the + * FlexibleAdapter Wiki Page * on GitHub.

    * * @param adapter the Adapter instance extending {@link FlexibleAdapter} @@ -134,9 +134,9 @@ public interface IFlexible { VH createViewHolder(FlexibleAdapter adapter, LayoutInflater inflater, ViewGroup parent); /** - * Binds the data of this item to the given Layout, if auto-map has been implemented. - *

    HELP: To know how to implement auto-map for ViewTypes please refer to the - * FlexibleAdapter WikiPage + * Binds the data of this item to the given Layout if AutoMap has been implemented. + *

    HELP: To know how to implement AutoMap for ViewTypes please refer to the + * FlexibleAdapter Wiki Page * on GitHub.

    * How to use Payload, please refer to * {@link android.support.v7.widget.RecyclerView.Adapter#onBindViewHolder(RecyclerView.ViewHolder, int, List)}. diff --git a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/utils/DrawableUtils.java b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/utils/DrawableUtils.java index fcd40f01..5c25e92d 100644 --- a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/utils/DrawableUtils.java +++ b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/utils/DrawableUtils.java @@ -15,9 +15,9 @@ */ package eu.davidea.flexibleadapter.utils; -import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; +import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; @@ -26,6 +26,7 @@ import android.graphics.drawable.shapes.RoundRectShape; import android.support.annotation.ColorInt; import android.support.annotation.DrawableRes; +import android.support.v4.view.ViewCompat; import android.util.TypedValue; import android.view.View; @@ -37,6 +38,7 @@ * @author Davide Steduto * @since 14/06/2016 Created */ +@SuppressWarnings("deprecation") public final class DrawableUtils { /** @@ -45,15 +47,11 @@ public final class DrawableUtils { * @param view the view to apply the drawable * @param drawable drawable object * @since 5.0.0-b7 + * @deprecated Use {@link #setBackgroundCompat(View, Drawable)} instead. */ - @SuppressWarnings("deprecation") - @SuppressLint("NewApi") + @Deprecated public static void setBackground(View view, Drawable drawable) { - if (Utils.hasJellyBean()) { - view.setBackground(drawable); - } else { - view.setBackgroundDrawable(drawable); - } + setBackgroundCompat(view, drawable); } /** @@ -62,9 +60,33 @@ public static void setBackground(View view, Drawable drawable) { * @param view the view to apply the drawable * @param drawableRes drawable resource id * @since 5.0.0-b7 + * @deprecated Use {@link #setBackgroundCompat(View, int)} instead. */ + @Deprecated public static void setBackground(View view, @DrawableRes int drawableRes) { - setBackground(view, getDrawableCompat(view.getContext(), drawableRes)); + setBackgroundCompat(view, getDrawableCompat(view.getContext(), drawableRes)); + } + + /** + * Helper method to set the background depending on the android version. + * + * @param view the view to apply the drawable + * @param drawable drawable object + * @since 5.0.0-rc1 + */ + public static void setBackgroundCompat(View view, Drawable drawable) { + ViewCompat.setBackground(view, drawable); + } + + /** + * Helper method to set the background depending on the android version + * + * @param view the view to apply the drawable + * @param drawableRes drawable resource id + * @since 5.0.0-rc1 + */ + public static void setBackgroundCompat(View view, @DrawableRes int drawableRes) { + setBackgroundCompat(view, getDrawableCompat(view.getContext(), drawableRes)); } /** @@ -75,7 +97,6 @@ public static void setBackground(View view, @DrawableRes int drawableRes) { * @return the drawable object * @since 5.0.0-b7 */ - @SuppressWarnings("deprecation") public static Drawable getDrawableCompat(Context context, @DrawableRes int drawableRes) { try { if (Utils.hasLollipop()) { @@ -89,58 +110,99 @@ public static Drawable getDrawableCompat(Context context, @DrawableRes int drawa } /** - * Helper to get the system default Selectable Background. + * Helper to get the default Selectable Background. Returns the resourceId of the + * {@code R.attr.selectableItemBackground} attribute of the overridden style. * * @param context the context * @return Default selectable background resId * @since 5.0.0-b7 + * @deprecated Use {@link #getSelectableItemBackground(Context)} instead. */ + @Deprecated public static int getSelectableBackground(Context context) { TypedValue outValue = new TypedValue(); - //it is important here to not use the android.R because this wouldn't add the latest drawable + // It's important to not use the android.R because this wouldn't add the overridden drawable context.getTheme().resolveAttribute(R.attr.selectableItemBackground, outValue, true); return outValue.resourceId; } /** - * Helper to get the system default Color Control Highlight. + * Helper to get the default selectableItemBackground drawable of the + * {@code R.attr.selectableItemBackground} attribute of the overridden style. * * @param context the context - * @return Default Color Control Highlight resId - * @since 5.0.0-b7 + * @return Default selectable item background drawable + * @since 5.0.0-rc1 */ + public static Drawable getSelectableItemBackground(Context context) { + TypedValue outValue = new TypedValue(); + // It's important to not use the android.R because this wouldn't add the overridden drawable + context.getTheme().resolveAttribute(R.attr.selectableItemBackground, outValue, true); + return getDrawableCompat(context, outValue.resourceId); + } + + /** + * Helper to get the system default Color Control Highlight. Returns the color of the + * {@code R.attr.colorControlHighlight} attribute in the overridden style. + * + * @param context the context + * @return Default Color Control Highlight + * @since 5.0.0-b7 Created, returns the resourceId + *
    5.0.0-rc1 Now returns the real color (not the resourceId) + */ + @ColorInt public static int getColorControlHighlight(Context context) { TypedValue outValue = new TypedValue(); - //it is important here to not use the android.R because this wouldn't add the latest drawable + // It's important to not use the android.R because this wouldn't add the overridden drawable context.getTheme().resolveAttribute(R.attr.colorControlHighlight, outValue, true); - return outValue.resourceId; + if (Utils.hasMarshmallow()) return context.getColor(outValue.resourceId); + else return context.getResources().getColor(outValue.resourceId); } /** * Helper to get a custom selectable background with Ripple if device has at least Lollipop. * - * @param rippleColor the color of the ripple * @param normalColor the color in normal state * @param pressedColor the pressed color + * @param rippleColor the color of the ripple * @return the RippleDrawable with StateListDrawable if at least Lollipop, the normal * StateListDrawable otherwise - * @since 5.0.0-b7 + * @since 5.0.0-b7 Created + *
    5.0.0-rc1 RippleColor becomes the 3rd parameter */ - public static Drawable getSelectableBackgroundCompat(@ColorInt int rippleColor, - @ColorInt int normalColor, - @ColorInt int pressedColor) { + public static Drawable getSelectableBackgroundCompat(@ColorInt int normalColor, + @ColorInt int pressedColor, + @ColorInt int rippleColor) { if (Utils.hasLollipop()) { return new RippleDrawable(ColorStateList.valueOf(rippleColor), - getStateListDrawable(normalColor, pressedColor, true), + getStateListDrawable(normalColor, pressedColor), getRippleMask(normalColor)); } else { - return getStateListDrawable(normalColor, pressedColor, false); + return getStateListDrawable(normalColor, pressedColor); + } + } + + /** + * Adds a ripple effect to any background. + * + * @param drawable any background drawable + * @param rippleColor the color of the ripple + * @return the RippleDrawable with the chosen background drawable if at least Lollipop, + * the provided drawable otherwise + * @since 5.0.0-rc1 + */ + public static Drawable getRippleDrawable(Drawable drawable, @ColorInt int rippleColor) { + if (Utils.hasLollipop()) { + return new RippleDrawable(ColorStateList.valueOf(rippleColor), + drawable, getRippleMask(Color.BLACK)); + } else { + return drawable; } } private static Drawable getRippleMask(@ColorInt int color) { float[] outerRadii = new float[8]; - //3 is the radius of final ripple, instead of 3 we can give required final radius + // 3 is the radius of final ripple, instead of 3 we can give required final radius Arrays.fill(outerRadii, 3); RoundRectShape r = new RoundRectShape(outerRadii, null, null); ShapeDrawable shapeDrawable = new ShapeDrawable(r); @@ -149,15 +211,13 @@ private static Drawable getRippleMask(@ColorInt int color) { } private static StateListDrawable getStateListDrawable(@ColorInt int normalColor, - @ColorInt int pressedColor, - boolean withRipple) { + @ColorInt int pressedColor) { StateListDrawable states = new StateListDrawable(); - if (!withRipple) - states.addState(new int[]{android.R.attr.state_pressed}, getColorDrawable(pressedColor)); states.addState(new int[]{android.R.attr.state_activated}, getColorDrawable(pressedColor)); states.addState(new int[]{}, getColorDrawable(normalColor)); - //if possible we enable animating across states - if (!withRipple) { + // Animating across states. + // It seems item background is lost on scrolling out of the screen, 21 <= API <= 23 + if (!Utils.hasLollipop() || Utils.hasNougat()) { int duration = 200; //android.R.integer.config_shortAnimTime states.setEnterFadeDuration(duration); states.setExitFadeDuration(duration); @@ -165,7 +225,14 @@ private static StateListDrawable getStateListDrawable(@ColorInt int normalColor, return states; } - private static ColorDrawable getColorDrawable(@ColorInt int color) { + /** + * Generate the {@code ColorDrawable} object from the provided Color. + * + * @param color the color + * @return the {@code ColorDrawable} object + * @since 5.0.0-rc1 + */ + public static ColorDrawable getColorDrawable(@ColorInt int color) { return new ColorDrawable(color); } diff --git a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/utils/Utils.java b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/utils/Utils.java index 5af4c249..05f12c51 100644 --- a/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/utils/Utils.java +++ b/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/utils/Utils.java @@ -15,7 +15,7 @@ */ package eu.davidea.flexibleadapter.utils; -import android.annotation.TargetApi; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; @@ -25,6 +25,8 @@ import android.os.Build.VERSION_CODES; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.OrientationHelper; import android.support.v7.widget.RecyclerView; @@ -36,6 +38,9 @@ import java.util.Locale; +import eu.davidea.flexibleadapter.R; +import eu.davidea.flexibleadapter.SelectableAdapter; + /** * @author Davide Steduto * @since 27/01/2016 Created @@ -45,6 +50,22 @@ public final class Utils { public static final int INVALID_COLOR = -1; public static int colorAccent = INVALID_COLOR; + /** + * API 24 + * @see VERSION_CODES#N + */ + public static boolean hasNougat() { + return Build.VERSION.SDK_INT >= VERSION_CODES.N; + } + + /** + * API 23 + * @see VERSION_CODES#M + */ + public static boolean hasMarshmallow() { + return Build.VERSION.SDK_INT >= VERSION_CODES.M; + } + /** * API 21 * @@ -63,6 +84,30 @@ public static boolean hasJellyBean() { return Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; } + /** + * @return the string representation of the provided {@link eu.davidea.flexibleadapter.SelectableAdapter.Mode} + * @since 5.0.0-rc1 + */ + @SuppressLint("SwitchIntDef") + public static String getModeName(@SelectableAdapter.Mode int mode) { + switch (mode) { + case SelectableAdapter.MODE_SINGLE: + return "MODE_SINGLE"; + case SelectableAdapter.MODE_MULTI: + return "MODE_MULTI"; + default: + return "MODE_IDLE"; + } + } + + /** + * @return the SimpleClassName of the provided object + * @since 5.0.0-rc1 + */ + public static String getClassName(@NonNull Object o) { + return o.getClass().getSimpleName(); + } + /** * Sets a spannable text with the accent color (if available) into the provided TextView. *

    Internally calls {@link #fetchAccentColor(Context, int)}.

    @@ -73,8 +118,11 @@ public static boolean hasJellyBean() { * @param constraint the text to highlight * @param defColor the default color in case accentColor is not found * @see #fetchAccentColor(Context, int) + * @deprecated Use + * {@link #highlightText(TextView, String, String, int)} OR + * {@link #highlightText(TextView, String, String)} */ - //TODO: Deprecate defColor? + @Deprecated public static void highlightText(@NonNull Context context, @NonNull TextView textView, String originalText, String constraint, @ColorInt int defColor) { if (originalText == null) originalText = ""; @@ -92,6 +140,68 @@ public static void highlightText(@NonNull Context context, @NonNull TextView tex } } + /** + * Sets a spannable text with the accent color (if available) into the provided TextView. + *

    Internally calls {@link #fetchAccentColor(Context, int)}.

    + * + * @param textView the TextView to transform + * @param originalText the original text which the transformation is applied to + * @param constraint the text to highlight + * @see #highlightText(TextView, String, String, int) + * @since 5.0.0-rc1 + */ + public static void highlightText(@NonNull TextView textView, + @Nullable String originalText, @Nullable String constraint) { + int accentColor = fetchAccentColor(textView.getContext(), 1); + highlightText(textView, originalText, constraint, accentColor); + } + + /** + * Sets a spannable text with any highlight color into the provided TextView. + * + * @param textView the TextView to transform + * @param originalText the original text which the transformation is applied to + * @param constraint the text to highlight + * @param color the highlight color + * @see #fetchAccentColor(Context, int) + * @see #highlightText(TextView, String, String) + * @since 5.0.0-rc1 + */ + public static void highlightText(@NonNull TextView textView, @Nullable String originalText, + @Nullable String constraint, @ColorInt int color) { + if (originalText == null) originalText = ""; + if (constraint == null) constraint = ""; + int i = originalText.toLowerCase(Locale.getDefault()).indexOf(constraint.toLowerCase(Locale.getDefault())); + if (i != -1) { + Spannable spanText = Spannable.Factory.getInstance().newSpannable(originalText); + spanText.setSpan(new ForegroundColorSpan(color), i, + i + constraint.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + spanText.setSpan(new StyleSpan(Typeface.BOLD), i, + i + constraint.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + textView.setText(spanText, TextView.BufferType.SPANNABLE); + } else { + textView.setText(originalText, TextView.BufferType.NORMAL); + } + } + + /** + * Resolves bug #161. Necessary when {@code theme} attribute is used in the layout. + * Used by {@code FlexibleAdapter.getStickyHeaderContainer()} method. + */ + //TODO: review comment + public static Activity scanForActivity(Context context) { + if (context instanceof Activity) + return (Activity) context; + else if (context instanceof ContextWrapper) + return scanForActivity(((ContextWrapper) context).getBaseContext()); + + return null; + } + + /*------------------------------*/ + /* ACCENT COLOR UTILITY METHODS */ + /*------------------------------*/ + /** * Reset the internal accent color to {@link #INVALID_COLOR}, to give the possibility * to re-fetch it at runtime, since once it is fetched it cannot be changed. @@ -107,21 +217,21 @@ public static void resetAccentColor() { * @param context context * @param defColor value to return if the accentColor cannot be found */ - //TODO: Deprecate defColor and use R.attr.colorAccent? - @TargetApi(VERSION_CODES.LOLLIPOP) public static int fetchAccentColor(Context context, @ColorInt int defColor) { if (colorAccent == INVALID_COLOR) { - if (hasLollipop()) { - TypedArray androidAttr = context.getTheme().obtainStyledAttributes(new int[]{android.R.attr.colorAccent}); - colorAccent = androidAttr.getColor(0, defColor); - androidAttr.recycle(); - } else { - colorAccent = defColor; - } + int attr = R.attr.colorAccent; + if (hasLollipop()) attr = android.R.attr.colorAccent; + TypedArray androidAttr = context.getTheme().obtainStyledAttributes(new int[]{attr}); + colorAccent = androidAttr.getColor(0, defColor); + androidAttr.recycle(); } return colorAccent; } + /*-------------------------------*/ + /* RECYCLER-VIEW UTILITY METHODS */ + /*-------------------------------*/ + /** * Finds the layout orientation of the RecyclerView. * @@ -150,13 +260,30 @@ public static int getOrientation(RecyclerView.LayoutManager layoutManager) { } /** - * Helper method to find the adapter position of the First completely visible view [for each - * span], no matter which Layout is. + * Helper method to retrieve the number of the columns (span count) of the given LayoutManager. + *

    All Layouts are supported.

    + * + * @param layoutManager the layout manager to check + * @return the span count + * @since 5.0.0-b7 + */ + public static int getSpanCount(RecyclerView.LayoutManager layoutManager) { + if (layoutManager instanceof GridLayoutManager) { + return ((GridLayoutManager) layoutManager).getSpanCount(); + } else if (layoutManager instanceof StaggeredGridLayoutManager) { + return ((StaggeredGridLayoutManager) layoutManager).getSpanCount(); + } + return 1; + } + + /** + * Helper method to find the adapter position of the first completely visible view + * [for each span], no matter which Layout is. * * @param layoutManager the layout manager in use - * @return the adapter position of the first fully visible item or {@code RecyclerView.NO_POSITION} + * @return the adapter position of the first fully visible item or {@code RecyclerView.NO_POSITION} * if there aren't any visible items. - * @see #findLastCompletelyVisibleItemPosition(RecyclerView.LayoutManager) + * @see #findFirstVisibleItemPosition(RecyclerView.LayoutManager) * @since 5.0.0-b8 */ public static int findFirstCompletelyVisibleItemPosition(RecyclerView.LayoutManager layoutManager) { @@ -168,13 +295,31 @@ public static int findFirstCompletelyVisibleItemPosition(RecyclerView.LayoutMana } /** - * Helper method to find the adapter position of the Last completely visible view [for each - * span], no matter which Layout is. + * Helper method to find the adapter position of the first partially visible view + * [for each span], no matter which Layout is. * * @param layoutManager the layout manager in use - * @return the adapter position of the last fully visible item or {@code RecyclerView.NO_POSITION} + * @return the adapter position of the first partially visible item or {@code RecyclerView.NO_POSITION} * if there aren't any visible items. * @see #findFirstCompletelyVisibleItemPosition(RecyclerView.LayoutManager) + * @since 5.0.0-rc1 + */ + public static int findFirstVisibleItemPosition(RecyclerView.LayoutManager layoutManager) { + if (layoutManager instanceof StaggeredGridLayoutManager) { + return ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(null)[0]; + } else { + return ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); + } + } + + /** + * Helper method to find the adapter position of the last completely visible view + * [for each span], no matter which Layout is. + * + * @param layoutManager the layout manager in use + * @return the adapter position of the last fully visible item or {@code RecyclerView.NO_POSITION} + * if there aren't any visible items. + * @see #findLastVisibleItemPosition(RecyclerView.LayoutManager) * @since 5.0.0-b8 */ public static int findLastCompletelyVisibleItemPosition(RecyclerView.LayoutManager layoutManager) { @@ -186,16 +331,21 @@ public static int findLastCompletelyVisibleItemPosition(RecyclerView.LayoutManag } /** - * Resolves bug #161. Necessary when {@code theme} attribute is used in the layout. - * Used by {@code FlexibleAdapter.getStickySectionHeadersHolder()} method. + * Helper method to find the adapter position of the last partially visible view + * [for each span], no matter which Layout is. + * + * @param layoutManager the layout manager in use + * @return the adapter position of the last partially visible item or {@code RecyclerView.NO_POSITION} + * if there aren't any visible items. + * @see #findLastCompletelyVisibleItemPosition(RecyclerView.LayoutManager) + * @since 5.0.0-rc1 */ - public static Activity scanForActivity(Context context) { - if (context instanceof Activity) - return (Activity) context; - else if (context instanceof ContextWrapper) - return scanForActivity(((ContextWrapper) context).getBaseContext()); - - return null; + public static int findLastVisibleItemPosition(RecyclerView.LayoutManager layoutManager) { + if (layoutManager instanceof StaggeredGridLayoutManager) { + return ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(null)[0]; + } else { + return ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); + } } } \ No newline at end of file diff --git a/flexible-adapter/src/main/java/eu/davidea/viewholders/ContentViewHolder.java b/flexible-adapter/src/main/java/eu/davidea/viewholders/ContentViewHolder.java index 6df523a0..09aae99b 100644 --- a/flexible-adapter/src/main/java/eu/davidea/viewholders/ContentViewHolder.java +++ b/flexible-adapter/src/main/java/eu/davidea/viewholders/ContentViewHolder.java @@ -15,6 +15,7 @@ */ package eu.davidea.viewholders; +import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.FrameLayout; @@ -41,14 +42,19 @@ abstract class ContentViewHolder extends RecyclerView.ViewHolder { * @param stickyHeader true if the ViewHolder is a header to be sticky * @since 5.0.0-b7 */ - public ContentViewHolder(View view, FlexibleAdapter adapter, boolean stickyHeader) { - //Since itemView is declared "final", the split is done before the View is initialized + ContentViewHolder(View view, FlexibleAdapter adapter, boolean stickyHeader) { + // Since itemView is declared "final", the split is done before the View is initialized super(stickyHeader ? new FrameLayout(view.getContext()) : view); if (stickyHeader) { itemView.setLayoutParams(adapter.getRecyclerView().getLayoutManager() .generateLayoutParams(view.getLayoutParams())); - ((FrameLayout) itemView).addView(view);//Add View after setLayoutParams + ((FrameLayout) itemView).addView(view); //Add View after setLayoutParams + float elevation = ViewCompat.getElevation(view); + if (elevation > 0) { + ViewCompat.setBackground(itemView, view.getBackground()); + ViewCompat.setElevation(itemView, elevation); + } contentView = view; } } @@ -64,7 +70,7 @@ public ContentViewHolder(View view, FlexibleAdapter adapter, boolean stickyHeade * @return the real contentView * @since 5.0.0-b7 */ - public View getContentView() { + public final View getContentView() { return contentView != null ? contentView : itemView; } @@ -79,7 +85,7 @@ public View getContentView() { * @see #setBackupPosition(int) * @since 5.0.0-b6 */ - public int getFlexibleAdapterPosition() { + public final int getFlexibleAdapterPosition() { int position = getAdapterPosition(); if (position == RecyclerView.NO_POSITION) { position = mBackupPosition; @@ -94,7 +100,7 @@ public int getFlexibleAdapterPosition() { * @param backupPosition the known position of this ViewHolder * @since 5.0.0-b6 */ - public void setBackupPosition(int backupPosition) { + public final void setBackupPosition(int backupPosition) { mBackupPosition = backupPosition; } diff --git a/flexible-adapter/src/main/java/eu/davidea/viewholders/ExpandableViewHolder.java b/flexible-adapter/src/main/java/eu/davidea/viewholders/ExpandableViewHolder.java index 9f6cfdc1..490f5f45 100644 --- a/flexible-adapter/src/main/java/eu/davidea/viewholders/ExpandableViewHolder.java +++ b/flexible-adapter/src/main/java/eu/davidea/viewholders/ExpandableViewHolder.java @@ -21,11 +21,13 @@ import android.view.View.OnLongClickListener; import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.Payload; /** - * ViewHolder for a Expandable Items. Holds callbacks which can be used to trigger expansion events. - *

    This class extends {@link FlexibleViewHolder}, which means it will benefit of all implemented - * methods the super class holds.

    + * ViewHolder for a Expandable Items. It holds methods which can be used to trigger expansion + * or collapsing events. + *

    This class extends {@link FlexibleViewHolder}, which means it will benefit of all + * implemented methods the super class holds.

    * * @author Davide Steduto * @since 16/01/2016 Created @@ -66,7 +68,7 @@ public ExpandableViewHolder(View view, FlexibleAdapter adapter, boolean stickyHe /*--------------*/ /** - * Allows to expand or collapse child views of this ItemView when {@link OnClickListener} + * Allows to expand or collapse child views of this itemView when {@link OnClickListener} * event occurs on the entire view. *

    This method returns always true; Extend with "return false" to Not expand or collapse * this ItemView onClick events.

    @@ -91,9 +93,28 @@ protected boolean isViewCollapsibleOnLongClick() { return true; } + /** + * Allows to notify change and rebound this itemView on expanding and collapsing events, + * in order to update the content (so, user can decide to display the current expanding status). + *

    This method returns always false; Override with {@code "return true"} to trigger the + * notification.

    + * + * @return true to rebound the content of this itemView on expanding and collapsing events, + * false to ignore the events + * @see #expandView(int) + * @see #collapseView(int) + * @since 5.0.0-rc1 + */ + protected boolean shouldNotifyParentOnClick() { + return false; + } + /** * Expands or Collapses based on the current state. * + * @see #shouldNotifyParentOnClick() + * @see #expandView(int) + * @see #collapseView(int) * @since 5.0.0-b1 */ @CallSuper @@ -107,23 +128,31 @@ protected void toggleExpansion() { } /** - * Triggers expansion of the Item. + * Triggers expansion of this itemView. + *

    If {@link #shouldNotifyParentOnClick()} returns {@code true}, this view is rebound + * with payload {@link Payload#EXPANDED}.

    * + * @see #shouldNotifyParentOnClick() * @since 5.0.0-b1 */ @CallSuper protected void expandView(int position) { mAdapter.expand(position); + if (shouldNotifyParentOnClick()) mAdapter.notifyItemChanged(position, Payload.EXPANDED); } /** - * Triggers collapse of the Item. + * Triggers collapse of this itemView. + *

    If {@link #shouldNotifyParentOnClick()} returns {@code true}, this view is rebound + * with payload {@link Payload#COLLAPSED}.

    * + * @see #shouldNotifyParentOnClick() * @since 5.0.0-b1 */ @CallSuper protected void collapseView(int position) { mAdapter.collapse(position); + if (shouldNotifyParentOnClick()) mAdapter.notifyItemChanged(position, Payload.COLLAPSED); } /*---------------------------------*/ @@ -135,7 +164,7 @@ protected void collapseView(int position) { *

    Note: In Expandable version, it tries to expand, but before, * it checks if the view {@link #isViewExpandableOnClick()}.

    * - * @param view the View that is the trigger for expansion + * @param view the view that receives the event * @since 5.0.0-b1 */ @Override @@ -147,11 +176,11 @@ public void onClick(View view) { } /** - * Called when user long taps on the ItemView. + * Called when user long taps on this itemView. *

    Note: In Expandable version, it tries to collapse, but before, * it checks if the view {@link #isViewCollapsibleOnLongClick()}.

    * - * @param view the View that is the trigger for collapsing + * @param view the view that receives the event * @since 5.0.0-b1 */ @Override @@ -166,6 +195,7 @@ public boolean onLongClick(View view) { /** * {@inheritDoc} *

    Note: In the Expandable version, expanded items are forced to collapse.

    + * * @since 5.0.0-b1 */ @Override diff --git a/flexible-adapter/src/main/java/eu/davidea/viewholders/FlexibleViewHolder.java b/flexible-adapter/src/main/java/eu/davidea/viewholders/FlexibleViewHolder.java index b18cbf57..bdec9c7f 100644 --- a/flexible-adapter/src/main/java/eu/davidea/viewholders/FlexibleViewHolder.java +++ b/flexible-adapter/src/main/java/eu/davidea/viewholders/FlexibleViewHolder.java @@ -29,6 +29,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.SelectableAdapter; +import eu.davidea.flexibleadapter.helpers.AnimatorHelper; import eu.davidea.flexibleadapter.helpers.ItemTouchHelperCallback; import eu.davidea.flexibleadapter.items.IFlexible; @@ -51,16 +52,16 @@ public abstract class FlexibleViewHolder extends ContentViewHolder private static final String TAG = FlexibleViewHolder.class.getSimpleName(); - //FlexibleAdapter is needed to retrieve listeners and item status + // FlexibleAdapter is needed to retrieve listeners and item status protected final FlexibleAdapter mAdapter; - //These 2 fields avoid double tactile feedback triggered by Android during the touch event + // These 2 fields avoid double tactile feedback triggered by Android during the touch event // (Drag or Swipe), also assure the LongClick event is correctly fired for ActionMode if that // was the user intention. private boolean mLongClickSkipped = false; private boolean alreadySelected = false; - //State for Dragging & Swiping actions + // State for Dragging & Swiping actions protected int mActionState = ItemTouchHelper.ACTION_STATE_IDLE; /*--------------*/ @@ -103,6 +104,7 @@ public FlexibleViewHolder(View view, FlexibleAdapter adapter, boolean stickyHead /** * {@inheritDoc} * + * @see #toggleActivation() * @since 5.0.0-b1 */ @Override @@ -110,18 +112,15 @@ public FlexibleViewHolder(View view, FlexibleAdapter adapter, boolean stickyHead public void onClick(View view) { int position = getFlexibleAdapterPosition(); if (!mAdapter.isEnabled(position)) return; - //Experimented that, if LongClick is not consumed, onClick is fired. We skip the - //call to the listener in this case, which is allowed only in ACTION_STATE_IDLE. + // Experimented that, if LongClick is not consumed, onClick is fired. We skip the + // call to the listener in this case, which is allowed only in ACTION_STATE_IDLE. if (mAdapter.mItemClickListener != null && mActionState == ItemTouchHelper.ACTION_STATE_IDLE) { if (FlexibleAdapter.DEBUG) Log.v(TAG, "onClick on position " + position + " mode=" + mAdapter.getMode()); - //Get the permission to activate the View from user + // Get the permission to activate the View from user if (mAdapter.mItemClickListener.onItemClick(position)) { - //Now toggle the activation - if (!mAdapter.isSelected(position) && itemView.isActivated() || - mAdapter.isSelected(position) && !itemView.isActivated()) { - toggleActivation(); - } + // Now toggle the activation + toggleActivation(); } } } @@ -129,6 +128,7 @@ public void onClick(View view) { /** * {@inheritDoc} * + * @see #toggleActivation() * @since 5.0.0-b1 */ @Override @@ -138,7 +138,7 @@ public boolean onLongClick(View view) { if (!mAdapter.isEnabled(position)) return false; if (FlexibleAdapter.DEBUG) Log.v(TAG, "onLongClick on position " + position + " mode=" + mAdapter.getMode()); - //If DragLongPress is enabled, then LongClick must be skipped and the listener will + // If DragLongPress is enabled, then LongClick must be skipped and the listener will // be called in onActionStateChanged in Drag mode. if (mAdapter.mItemLongClickListener != null && !mAdapter.isLongPressDragEnabled()) { mAdapter.mItemLongClickListener.onItemLongClick(position); @@ -159,7 +159,10 @@ public boolean onLongClick(View view) { @Override public boolean onTouch(View view, MotionEvent event) { int position = getFlexibleAdapterPosition(); - if (!mAdapter.isEnabled(position)) return false; + if (!mAdapter.isEnabled(position) || !isDraggable()) { + Log.w(TAG, "Can't start drag: Item is not enabled or draggable!"); + return false; + } if (FlexibleAdapter.DEBUG) Log.v(TAG, "onTouch with DragHandleView on position " + position + " mode=" + mAdapter.getMode()); if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN && @@ -175,7 +178,7 @@ public boolean onTouch(View view, MotionEvent event) { /*--------------*/ /** - * Sets the inner view which will be used to drag the Item ViewHolder. + * Sets the inner view which will be used to drag this itemView. * * @param view handle view * @see #onTouch(View, MotionEvent) @@ -188,35 +191,49 @@ protected void setDragHandleView(@NonNull View view) { } /** - * Allows to change and see the activation status on the ItemView and to perform object - * animation in it. - *

    IMPORTANT NOTE! the change of the background is visible if you added - * android:background="?attr/selectableItemBackground" on the item layout AND - * in the style.xml.
    - * Adapter must have a reference to its instance to check selection state.

    - *

    This must be called every time we want the activation state visible on the ItemView, - * for instance, after a Click (to add the item to the selection list) or after a LongClick - * (to activate the ActionMode) or during a Drag (to show that we enabled the Drag).

    - * If you do this, it's not necessary to invalidate the row (with notifyItemChanged): - * In this way bindViewHolder is NOT called and inner Views can animate without - * interruption, so you can see the animation running still having the selection activated. + * Allows to change and see the activation status on the itemView and to perform animation + * on inner views. + *

    IMPORTANT NOTE! the selected background is visible if you added + * {@code android:background="?attr/selectableItemBackground"} on the item layout AND + * customized the file {@code style.xml}.

    + * Alternatively, to set a background at runtime, you can use the new + * {@link eu.davidea.flexibleadapter.utils.DrawableUtils}. + *

    Note: This method must be called every time we want the activation state visible + * on the itemView, for instance: after a Click (to add the item to the selection list) or + * after a LongClick (to activate the ActionMode) or during dragging (to show that we enabled + * the Drag).

    + * If you follow the above instructions, it's not necessary to invalidate this view with + * {@code notifyItemChanged}: In this way {@code bindViewHolder} won't be called and inner + * views can animate without interruptions, eventually you will see the animation running + * on those inner views at the same time of selection activation. * + * @see #getActivationElevation() * @since 5.0.0-b1 */ @CallSuper - protected void toggleActivation() { - itemView.setActivated(mAdapter.isSelected(getFlexibleAdapterPosition())); - if (itemView.isActivated() && getActivationElevation() > 0) - ViewCompat.setElevation(itemView, getActivationElevation()); - else if (getActivationElevation() > 0)//Leave unaltered the default elevation - ViewCompat.setElevation(itemView, 0); + public void toggleActivation() { + // Only for selectable items + int position = getFlexibleAdapterPosition(); + if (!mAdapter.isSelectable(position)) return; + // [De]Activate the view + boolean selected = mAdapter.isSelected(position); + if (itemView.isActivated() && !selected || !itemView.isActivated() && selected) { + itemView.setActivated(selected); + // Apply elevation + if (itemView.isActivated() && getActivationElevation() > 0) + ViewCompat.setElevation(itemView, getActivationElevation()); + else if (getActivationElevation() > 0) //Leave unaltered the default elevation + ViewCompat.setElevation(itemView, 0); + } } /** * Allows to set elevation while the view is activated. *

    Override to return desired value of elevation on this itemView.

    + * Note: returned value must be in Pixel. * - * @return never elevate, returns 0dp if not overridden + * @return {@code 0px} (never elevate) if not overridden + * @see #toggleActivation() * @since 5.0.0-b2 */ public float getActivationElevation() { @@ -225,10 +242,11 @@ public float getActivationElevation() { /** * Allows to activate the itemView when Swipe event occurs. - *

    This method returns always false; Extend with "return true" to Not expand or collapse - * this ItemView onClick events.

    + *

    This method returns always false; Override with {@code "return true"} to Not expand or + * collapse this itemView onClick events.

    * * @return always false, if not overridden + * @see #toggleActivation() * @since 5.0.0-b2 */ protected boolean shouldActivateViewWhileSwiping() { @@ -237,10 +255,11 @@ protected boolean shouldActivateViewWhileSwiping() { /** * Allows to add and keep item selection if ActionMode is active. - *

    This method returns always false; Extend with "return true" to add item to the ActionMode - * count.

    + *

    This method returns always false;Override with {@code "return true"} to add the item + * to the ActionMode count.

    * * @return always false, if not overridden + * @see #toggleActivation() * @since 5.0.0-b2 */ protected boolean shouldAddSelectionInActionMode() { @@ -256,15 +275,17 @@ protected boolean shouldAddSelectionInActionMode() { * actively scrolls the list (forward or backward). *

    Implement your logic for different animators based on position, selection and/or * direction.

    - * Create your {@link Animator}(s), then add it to the list of animators. + * Use can take one of the predefined Animator from {@link AnimatorHelper} or create your own + * {@link Animator}(s), then add it to the list of animators. * + * @param animators NonNull list of animators, which you should add new animators * @param position can be used to differentiate the Animators based on positions * @param isForward can be used to separate animation from top/bottom or from left/right scrolling + * @see AnimatorHelper * @since 5.0.0-b8 - * @see eu.davidea.flexibleadapter.helpers.AnimatorHelper */ public void scrollAnimators(@NonNull List animators, int position, boolean isForward) { - //Free to implement + // Free to implement } /*--------------------------------*/ @@ -272,14 +293,16 @@ public void scrollAnimators(@NonNull List animators, int position, boo /*--------------------------------*/ /** - * Here we handle the event of when the ItemTouchHelper first registers an item as being - * moved or swiped. - *

    In this implementations, View activation is automatically handled in case of Drag: - * The Item will be added to the selection list if not selected yet and mode MULTI is activated.

    + * Here we handle the event of when the {@code ItemTouchHelper} first registers an item + * as being moved or swiped. + *

    In this implementation, View activation is automatically handled if dragged: The Item + * will be added to the selection list if not selected yet and mode MULTI is activated.

    * * @param position the position of the item touched * @param actionState one of {@link ItemTouchHelper#ACTION_STATE_SWIPE} or * {@link ItemTouchHelper#ACTION_STATE_DRAG}. + * @see #shouldActivateViewWhileSwiping() + * @see #shouldAddSelectionInActionMode() * @since 5.0.0-b1 */ @Override @@ -292,23 +315,23 @@ public void onActionStateChanged(int position, int actionState) { " actionState=" + (actionState == ItemTouchHelper.ACTION_STATE_SWIPE ? "Swipe(1)" : "Drag(2)")); if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { if (!alreadySelected) { - //Be sure, if MODE_MULTI is active, to add this item to the selection list (call listener!) - //Also be sure user consumes the long click event if not done in onLongClick. - //Drag by LongPress or Drag by handleView + // Be sure, if MODE_MULTI is active, to add this item to the selection list (call listener!) + // Also be sure user consumes the long click event if not done in onLongClick. + // Drag by LongPress or Drag by handleView if (mLongClickSkipped || mAdapter.getMode() == SelectableAdapter.MODE_MULTI) { - //Next check, allows to initiate the ActionMode and to add selection if configured + // Next check, allows to initiate the ActionMode and to add selection if configured if ((shouldAddSelectionInActionMode() || mAdapter.getMode() != SelectableAdapter.MODE_MULTI) && mAdapter.mItemLongClickListener != null && mAdapter.isSelectable(position)) { mAdapter.mItemLongClickListener.onItemLongClick(position); alreadySelected = true; //Keep selection on release! } } - //If still not selected, be sure current item appears selected for the Drag transition + // If still not selected, be sure current item appears selected for the Drag transition if (!alreadySelected) { mAdapter.toggleSelection(position); } } - //Now toggle the activation, Activate view and make selection visible only if necessary + // Now toggle the activation, Activate view and make selection visible only if necessary if (!itemView.isActivated()) { toggleActivation(); } @@ -325,6 +348,8 @@ public void onActionStateChanged(int position, int actionState) { * In case of Drag, the state will be cleared depends by current selection mode! * * @param position the position of the item released + * @see #shouldActivateViewWhileSwiping() + * @see #shouldAddSelectionInActionMode() * @since 5.0.0-b1 */ @Override @@ -333,7 +358,7 @@ public void onItemReleased(int position) { if (FlexibleAdapter.DEBUG) Log.v(TAG, "onItemReleased position=" + position + " mode=" + mAdapter.getMode() + " actionState=" + (mActionState == ItemTouchHelper.ACTION_STATE_SWIPE ? "Swipe(1)" : "Drag(2)")); - //Be sure to keep selection if MODE_MULTI and shouldAddSelectionInActionMode is active + // Be sure to keep selection if MODE_MULTI and shouldAddSelectionInActionMode is active if (!alreadySelected) { if (shouldAddSelectionInActionMode() && mAdapter.getMode() == SelectableAdapter.MODE_MULTI) { @@ -351,25 +376,27 @@ public void onItemReleased(int position) { } } } - //Reset internal action state ready for next action + // Reset internal action state ready for next action mLongClickSkipped = false; mActionState = ItemTouchHelper.ACTION_STATE_IDLE; } /** + * @return the boolean value from the item flag, true to allow dragging * @since 5.0.0-b7 */ @Override - public boolean isDraggable() { + public final boolean isDraggable() { IFlexible item = mAdapter.getItem(getFlexibleAdapterPosition()); return item != null && item.isDraggable(); } /** + * @return the boolean value from the item flag, true to allow swiping * @since 5.0.0-b7 */ @Override - public boolean isSwipeable() { + public final boolean isSwipeable() { IFlexible item = mAdapter.getItem(getFlexibleAdapterPosition()); return item != null && item.isSwipeable(); } diff --git a/flexible-adapter/src/main/res/layout/sticky_header_layout.xml b/flexible-adapter/src/main/res/layout/sticky_header_layout.xml index f446d759..0412752e 100644 --- a/flexible-adapter/src/main/res/layout/sticky_header_layout.xml +++ b/flexible-adapter/src/main/res/layout/sticky_header_layout.xml @@ -2,5 +2,4 @@ \ No newline at end of file + android:layout_height="wrap_content"/> \ No newline at end of file diff --git a/jfrog-bintray-publish.gradle b/jfrog-bintray-publish.gradle index 421e5d48..05f801d0 100644 --- a/jfrog-bintray-publish.gradle +++ b/jfrog-bintray-publish.gradle @@ -2,30 +2,6 @@ apply plugin: 'com.jfrog.bintray' version = libraryVersion -task clean(type: Delete) { - delete rootProject.buildDir -} - -task sourcesJar(type: Jar) { - from android.sourceSets.main.java.srcDirs - classifier = 'sources' -} - -task javadoc(type: Javadoc) { - source = android.sourceSets.main.java.srcDirs - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) -} - -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.getDestinationDir() -} - -artifacts { - archives javadocJar - archives sourcesJar -} - def getRepositoryUsername() { return getProperties().get('bintray.user') } @@ -50,7 +26,7 @@ bintray { websiteUrl = siteUrl licenses = allLicenses vcsUrl = gitUrl - labels = ['android', 'recyclerview', 'list', 'adapter', 'selection', 'actionmode', 'undo', 'viewholder', 'ripple', 'search', 'filter', 'filtering', 'async', 'asynchrnous', 'item', 'item animator', 'itemanimator', 'animator' ,'animation', 'expand', 'expandable', 'collapse', 'collapsible', 'section', 'sticky', 'header', 'category', 'drag', 'draggable', 'swipe', 'swipeable', 'leave behind', 'fastscroll', 'fastscroller', 'endless scroll', 'load more'] + labels = ['android', 'recyclerview', 'list', 'adapter', 'selection', 'actionmode', 'undo', 'viewholder', 'ripple', 'search', 'filter', 'filtering', 'async', 'asynchrnous', 'item', 'item animator', 'itemanimator', 'animator' ,'animation', 'expand', 'expandable', 'collapse', 'collapsible', 'section', 'sections', 'sticky', 'header', 'headers', 'category', 'footer', 'footers', 'drag', 'draggable', 'swipe', 'swipeable', 'leave behind', 'fastscroll', 'fastscroller', 'endless', 'endless scroll', 'load more'] publish = true publicDownloadNumbers = true //noinspection GroovyAssignabilityCheck diff --git a/maven-install.gradle b/maven-install.gradle index be81a1c9..c5d463db 100644 --- a/maven-install.gradle +++ b/maven-install.gradle @@ -2,6 +2,30 @@ apply plugin: 'com.github.dcendents.android-maven' group = publishedGroupId // Maven Group ID for the artifact +task clean(type: Delete) { + delete rootProject.buildDir +} + +task sourcesJar(type: Jar) { + from android.sourceSets.main.java.srcDirs + classifier = 'sources' +} + +task javadoc(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.getDestinationDir() +} + +artifacts { + archives javadocJar + archives sourcesJar +} + install { repositories.mavenInstaller { // This generates POM.xml with proper parameters diff --git a/screenshots/shf.png b/screenshots/shf.png new file mode 100644 index 00000000..5460a69b Binary files /dev/null and b/screenshots/shf.png differ