├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── styles.xml │ │ │ │ └── arrays.xml │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── image.gif │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ ├── activity_main.xml │ │ │ │ ├── view_item.xml │ │ │ │ ├── view_footer.xml │ │ │ │ └── view_header.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── wujf │ │ │ │ └── stickyheaderfooter │ │ │ │ ├── headerfooterutil │ │ │ │ ├── StickyRecyclerAdapter.java │ │ │ │ ├── util │ │ │ │ │ ├── OrientationProvider.java │ │ │ │ │ └── LinearLayoutOrientationProvider.java │ │ │ │ ├── caching │ │ │ │ │ ├── FooterProvider.java │ │ │ │ │ ├── HeaderProvider.java │ │ │ │ │ ├── HeaderViewCache.java │ │ │ │ │ └── FooterViewCache.java │ │ │ │ ├── ItemVisibilityAdapter.java │ │ │ │ ├── StickyRecyclerFootersAdapter.java │ │ │ │ ├── StickyRecyclerHeadersAdapter.java │ │ │ │ ├── calculation │ │ │ │ │ └── DimensionCalculator.java │ │ │ │ ├── rendering │ │ │ │ │ └── HeaderRenderer.java │ │ │ │ ├── StickyRecyclerTouchListener.java │ │ │ │ ├── StickyRecyclerDecoration.java │ │ │ │ └── HeaderPositionCalculator.java │ │ │ │ └── MainActivity.java │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── wujf │ │ │ └── stickyheaderfooter │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── wujf │ │ └── stickyheaderfooter │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── README.md ├── gradle.properties ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | StickerHeaderFooterRecyclerView 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanglaohushiwo/StickyHeaderFooterRecyclerView/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanglaohushiwo/StickyHeaderFooterRecyclerView/HEAD/app/src/main/res/mipmap-xhdpi/image.gif -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanglaohushiwo/StickyHeaderFooterRecyclerView/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanglaohushiwo/StickyHeaderFooterRecyclerView/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanglaohushiwo/StickyHeaderFooterRecyclerView/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanglaohushiwo/StickyHeaderFooterRecyclerView/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanglaohushiwo/StickyHeaderFooterRecyclerView/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanglaohushiwo/StickyHeaderFooterRecyclerView/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanglaohushiwo/StickyHeaderFooterRecyclerView/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanglaohushiwo/StickyHeaderFooterRecyclerView/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanglaohushiwo/StickyHeaderFooterRecyclerView/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanglaohushiwo/StickyHeaderFooterRecyclerView/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Aug 09 17:53:40 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | "# StickyHeaderFooterRecyclerView" 2 | 我这个项目主要是从另一个项目改的,项目地址为https://github.com/timehop/sticky-headers-recyclerview 3 | 在原来的基础上添加了: 4 | 1.底部悬停效果 5 | 2.支持头部子view和底部子view的点击效果 6 | 但是还存在一些问题: 7 | 1.不能再调用其他的addItemDecoration方法来添加分隔线,只能在itemview最底部添加分隔线。 8 | 2.如果相同种类的子view总的高度小于headerview或者footerview时,会出现问题。 9 | 3.暂时只支持竖直方向不支持水平方向的,因为我感觉水平方向可能用不到。 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/StickyRecyclerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | 5 | public interface StickyRecyclerAdapter extends StickyRecyclerHeadersAdapter, StickyRecyclerFootersAdapter { 6 | int getItemCount(); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/util/OrientationProvider.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil.util; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | 5 | /** 6 | * Interface for getting the orientation of a RecyclerView from its LayoutManager 7 | */ 8 | public interface OrientationProvider { 9 | 10 | public int getOrientation(RecyclerView recyclerView); 11 | 12 | public boolean isReverseLayout(RecyclerView recyclerView); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/test/java/com/wujf/stickyheaderfooter/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/view_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 14 | 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/wujf/stickyheaderfooter/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.wujf.stickyheaderfooter", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_footer.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 18 | 19 | 20 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/caching/FooterProvider.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil.caching; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.View; 5 | 6 | /** 7 | * Implemented by objects that provide header views for decoration 8 | */ 9 | public interface FooterProvider { 10 | 11 | /** 12 | * Will provide a header view for a given position in the RecyclerView 13 | * 14 | * @param recyclerView that will display the header 15 | * @param position that will be headed by the header 16 | * @return a header view for the given position and list 17 | */ 18 | public View getFooter(RecyclerView recyclerView, int position); 19 | 20 | public RecyclerView.ViewHolder getFooterViewHolder(int position); 21 | 22 | /** 23 | * TODO: describe this functionality and its necessity 24 | */ 25 | void invalidate(); 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/caching/HeaderProvider.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil.caching; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.View; 5 | 6 | /** 7 | * Implemented by objects that provide header views for decoration 8 | */ 9 | public interface HeaderProvider { 10 | 11 | /** 12 | * Will provide a header view for a given position in the RecyclerView 13 | * 14 | * @param recyclerView that will display the header 15 | * @param position that will be headed by the header 16 | * @return a header view for the given position and list 17 | */ 18 | public View getHeader(RecyclerView recyclerView, int position); 19 | 20 | public RecyclerView.ViewHolder getHeaderViewHolder(int position); 21 | 22 | /** 23 | * TODO: describe this functionality and its necessity 24 | */ 25 | void invalidate(); 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 19 | 20 | 21 | 25 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "com.wujf.stickyheaderfooter" 7 | minSdkVersion 15 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'com.android.support:appcompat-v7:28.0.0' 24 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 25 | testImplementation 'junit:junit:4.12' 26 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 27 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 28 | implementation 'com.android.support:recyclerview-v7:27.1.1' 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/ItemVisibilityAdapter.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil; 2 | 3 | /** 4 | * ItemVisibilityAdapter provides a way for StickyRecyclerHeadersDecoration 5 | * to know if a row is visible or not. This comes into play if the 6 | * recyclerview's layout manager is set up to provide extra layout space (by 7 | * overriding getExtraLayoutSpace). In this case rows that aren't visible (yet) 8 | * will be bound and StickyRecyclerHeadersDecoration will need to know which 9 | * are visible to correctly calculate the row to base the sticky header on 10 | *

11 | * To use it you must pass an instance of a class that implements this 12 | * interface as a second argment StickyRecyclerHeadersDecoration's constructor. 13 | */ 14 | public interface ItemVisibilityAdapter { 15 | 16 | /** 17 | * Return true the specified adapter position is visible, false otherwise 18 | *

19 | * The implementation of this method will typically return true if 20 | * the position is between the layout manager's findFirstVisibleItemPosition 21 | * and findLastVisibleItemPosition (inclusive). 22 | * 23 | * @param position the adapter position 24 | */ 25 | boolean isPositionVisible(final int position); 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/util/LinearLayoutOrientationProvider.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil.util; 2 | 3 | import android.support.v7.widget.LinearLayoutManager; 4 | import android.support.v7.widget.RecyclerView; 5 | 6 | /** 7 | * OrientationProvider for ReyclerViews who use a LinearLayoutManager 8 | */ 9 | public class LinearLayoutOrientationProvider implements OrientationProvider { 10 | 11 | @Override 12 | public int getOrientation(RecyclerView recyclerView) { 13 | RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); 14 | throwIfNotLinearLayoutManager(layoutManager); 15 | return ((LinearLayoutManager) layoutManager).getOrientation(); 16 | } 17 | 18 | @Override 19 | public boolean isReverseLayout(RecyclerView recyclerView) { 20 | RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); 21 | throwIfNotLinearLayoutManager(layoutManager); 22 | return ((LinearLayoutManager) layoutManager).getReverseLayout(); 23 | } 24 | 25 | private void throwIfNotLinearLayoutManager(RecyclerView.LayoutManager layoutManager) { 26 | if (!(layoutManager instanceof LinearLayoutManager)) { 27 | throw new IllegalStateException("StickyListHeadersDecoration can only be used with a " + 28 | "LinearLayoutManager."); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/StickyRecyclerFootersAdapter.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.ViewGroup; 5 | 6 | public interface StickyRecyclerFootersAdapter { 7 | /** 8 | * Get the ID of the header associated with this item. For example, if your headers group 9 | * items by their first letter, you could return the character representation of the first letter. 10 | * Return a value < 0 if the view should not have a header (like, a header view or footer view) 11 | * 12 | * @param position the position of the view to get the header ID of 13 | * @return the header ID 14 | */ 15 | long getFooterId(int position); 16 | 17 | /** 18 | * Creates a new ViewHolder for a header. This works the same way onCreateViewHolder in 19 | * Recycler.Adapter, ViewHolders can be reused for different views. This is usually a good place 20 | * to inflate the layout for the header. 21 | * 22 | * @param parent the view to create a header view holder for 23 | * @return the view holder 24 | */ 25 | VH onCreateFooterViewHolder(ViewGroup parent); 26 | 27 | /** 28 | * Binds an existing ViewHolder to the specified adapter position. 29 | * 30 | * @param holder the view holder 31 | * @param position the adapter position 32 | */ 33 | void onBindFooterViewHolder(VH holder, int position); 34 | 35 | VH getFooterViewHolder(int postion); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/StickyRecyclerHeadersAdapter.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.ViewGroup; 5 | 6 | public interface StickyRecyclerHeadersAdapter { 7 | /** 8 | * Get the ID of the header associated with this item. For example, if your headers group 9 | * items by their first letter, you could return the character representation of the first letter. 10 | * Return a value < 0 if the view should not have a header (like, a header view or footer view) 11 | * 12 | * @param position the position of the view to get the header ID of 13 | * @return the header ID 14 | */ 15 | long getHeaderId(int position); 16 | 17 | /** 18 | * Creates a new ViewHolder for a header. This works the same way onCreateViewHolder in 19 | * Recycler.Adapter, ViewHolders can be reused for different views. This is usually a good place 20 | * to inflate the layout for the header. 21 | * 22 | * @param parent the view to create a header view holder for 23 | * @return the view holder 24 | */ 25 | VH onCreateHeaderViewHolder(ViewGroup parent); 26 | 27 | /** 28 | * Binds an existing ViewHolder to the specified adapter position. 29 | * 30 | * @param holder the view holder 31 | * @param position the adapter position 32 | */ 33 | void onBindHeaderViewHolder(VH holder, int position); 34 | 35 | VH getHeaderViewHolder(int position); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/calculation/DimensionCalculator.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil.calculation; 2 | 3 | import android.graphics.Rect; 4 | import android.view.View; 5 | 6 | import static android.view.ViewGroup.LayoutParams; 7 | import static android.view.ViewGroup.MarginLayoutParams; 8 | 9 | /** 10 | * Helper to calculate various view dimensions 11 | */ 12 | public class DimensionCalculator { 13 | 14 | /** 15 | * Populates {@link Rect} with margins for any view. 16 | * 17 | * @param margins rect to populate 18 | * @param view for which to get margins 19 | */ 20 | public void initMargins(Rect margins, View view) { 21 | LayoutParams layoutParams = view.getLayoutParams(); 22 | 23 | if (layoutParams instanceof MarginLayoutParams) { 24 | MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams; 25 | initMarginRect(margins, marginLayoutParams); 26 | } else { 27 | margins.set(0, 0, 0, 0); 28 | } 29 | } 30 | 31 | /** 32 | * Converts {@link MarginLayoutParams} into a representative {@link Rect}. 33 | * 34 | * @param marginRect Rect to be initialized with margins coordinates, where 35 | * {@link MarginLayoutParams#leftMargin} is equivalent to {@link Rect#left}, etc. 36 | * @param marginLayoutParams margins to populate the Rect with 37 | */ 38 | private void initMarginRect(Rect marginRect, MarginLayoutParams marginLayoutParams) { 39 | marginRect.set( 40 | marginLayoutParams.leftMargin, 41 | marginLayoutParams.topMargin, 42 | marginLayoutParams.rightMargin, 43 | marginLayoutParams.bottomMargin 44 | ); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/caching/HeaderViewCache.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil.caching; 2 | 3 | import android.support.v4.util.LongSparseArray; 4 | import android.support.v7.widget.LinearLayoutManager; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import com.wujf.stickyheaderfooter.headerfooterutil.StickyRecyclerHeadersAdapter; 10 | import com.wujf.stickyheaderfooter.headerfooterutil.util.OrientationProvider; 11 | 12 | 13 | /** 14 | * An implementation of {@link HeaderProvider} that creates and caches header views 15 | */ 16 | public class HeaderViewCache implements HeaderProvider { 17 | 18 | private final StickyRecyclerHeadersAdapter mAdapter; 19 | private final LongSparseArray mHeaderViews = new LongSparseArray<>(); 20 | private final OrientationProvider mOrientationProvider; 21 | 22 | public HeaderViewCache(StickyRecyclerHeadersAdapter adapter, 23 | OrientationProvider orientationProvider) { 24 | mAdapter = adapter; 25 | mOrientationProvider = orientationProvider; 26 | } 27 | 28 | @Override 29 | public View getHeader(RecyclerView parent, int position) { 30 | long headerId = mAdapter.getHeaderId(position); 31 | 32 | RecyclerView.ViewHolder viewHolder = mHeaderViews.get(headerId); 33 | if (viewHolder == null) { 34 | //TODO - recycle views 35 | viewHolder = mAdapter.onCreateHeaderViewHolder(parent); 36 | mAdapter.onBindHeaderViewHolder(viewHolder, position); 37 | View header = viewHolder.itemView; 38 | if (header.getLayoutParams() == null) { 39 | header.setLayoutParams(new ViewGroup.LayoutParams( 40 | ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 41 | } 42 | 43 | int widthSpec; 44 | int heightSpec; 45 | 46 | if (mOrientationProvider.getOrientation(parent) == LinearLayoutManager.VERTICAL) { 47 | widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY); 48 | heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); 49 | } else { 50 | widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.UNSPECIFIED); 51 | heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.EXACTLY); 52 | } 53 | 54 | int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, 55 | parent.getPaddingLeft() + parent.getPaddingRight(), header.getLayoutParams().width); 56 | int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, 57 | parent.getPaddingTop() + parent.getPaddingBottom(), header.getLayoutParams().height); 58 | header.measure(childWidth, childHeight); 59 | header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight()); 60 | mHeaderViews.put(headerId, viewHolder); 61 | } 62 | return viewHolder.itemView; 63 | } 64 | 65 | @Override 66 | public RecyclerView.ViewHolder getHeaderViewHolder(int position) { 67 | long headerId = mAdapter.getHeaderId(position); 68 | return mHeaderViews.get(headerId); 69 | } 70 | 71 | @Override 72 | public void invalidate() { 73 | mHeaderViews.clear(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/caching/FooterViewCache.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil.caching; 2 | 3 | import android.support.v4.util.LongSparseArray; 4 | import android.support.v7.widget.LinearLayoutManager; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import com.wujf.stickyheaderfooter.headerfooterutil.StickyRecyclerFootersAdapter; 10 | import com.wujf.stickyheaderfooter.headerfooterutil.util.OrientationProvider; 11 | 12 | 13 | /** 14 | * An implementation of {@link HeaderProvider} that creates and caches header views 15 | */ 16 | public class FooterViewCache implements FooterProvider { 17 | 18 | private final StickyRecyclerFootersAdapter mAdapter; 19 | private final LongSparseArray mFooterViews = new LongSparseArray<>(); 20 | private final OrientationProvider mOrientationProvider; 21 | 22 | public FooterViewCache(StickyRecyclerFootersAdapter adapter, 23 | OrientationProvider orientationProvider) { 24 | mAdapter = adapter; 25 | mOrientationProvider = orientationProvider; 26 | } 27 | 28 | @Override 29 | public View getFooter(RecyclerView parent, int position) { 30 | long headerId = mAdapter.getFooterId(position); 31 | RecyclerView.ViewHolder viewHolder = getFooterViewHolder(position); 32 | if (viewHolder == null) { 33 | //TODO - recycle views 34 | viewHolder = mAdapter.onCreateFooterViewHolder(parent); 35 | mAdapter.onBindFooterViewHolder(viewHolder, position); 36 | View footer = viewHolder.itemView; 37 | if (footer.getLayoutParams() == null) { 38 | footer.setLayoutParams(new ViewGroup.LayoutParams( 39 | ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 40 | } 41 | 42 | int widthSpec; 43 | int heightSpec; 44 | 45 | if (mOrientationProvider.getOrientation(parent) == LinearLayoutManager.VERTICAL) { 46 | widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY); 47 | heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); 48 | } else { 49 | widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.UNSPECIFIED); 50 | heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.EXACTLY); 51 | } 52 | 53 | int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, 54 | parent.getPaddingLeft() + parent.getPaddingRight(), footer.getLayoutParams().width); 55 | int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, 56 | parent.getPaddingTop() + parent.getPaddingBottom(), footer.getLayoutParams().height); 57 | footer.measure(childWidth, childHeight); 58 | footer.layout(0, 0, footer.getMeasuredWidth(), footer.getMeasuredHeight()); 59 | mFooterViews.put(headerId, viewHolder); 60 | footer.setTag(headerId); 61 | } 62 | return viewHolder.itemView; 63 | } 64 | 65 | public RecyclerView.ViewHolder getFooterViewHolder(int position) { 66 | long footerId = mAdapter.getFooterId(position); 67 | return mFooterViews.get(footerId); 68 | } 69 | 70 | @Override 71 | public void invalidate() { 72 | mFooterViews.clear(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/rendering/HeaderRenderer.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil.rendering; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Rect; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.View; 7 | import android.widget.LinearLayout; 8 | 9 | import com.wujf.stickyheaderfooter.headerfooterutil.calculation.DimensionCalculator; 10 | import com.wujf.stickyheaderfooter.headerfooterutil.util.OrientationProvider; 11 | 12 | 13 | /** 14 | * Responsible for drawing headers to the canvas provided by the item decoration 15 | */ 16 | public class HeaderRenderer { 17 | 18 | private final DimensionCalculator mDimensionCalculator; 19 | private final OrientationProvider mOrientationProvider; 20 | 21 | /** 22 | * The following field is used as a buffer for internal calculations. Its sole purpose is to avoid 23 | * allocating new Rect every time we need one. 24 | */ 25 | private final Rect mTempRect = new Rect(); 26 | 27 | public HeaderRenderer(OrientationProvider orientationProvider) { 28 | this(orientationProvider, new DimensionCalculator()); 29 | } 30 | 31 | private HeaderRenderer(OrientationProvider orientationProvider, 32 | DimensionCalculator dimensionCalculator) { 33 | mOrientationProvider = orientationProvider; 34 | mDimensionCalculator = dimensionCalculator; 35 | } 36 | 37 | /** 38 | * Draws a header to a canvas, offsetting by some x and y amount 39 | * 40 | * @param recyclerView the parent recycler view for drawing the header into 41 | * @param canvas the canvas on which to draw the header 42 | * @param header the view to draw as the header 43 | * @param offset a Rect used to define the x/y offset of the header. Specify x/y offset by setting 44 | * the {@link Rect#left} and {@link Rect#top} properties, respectively. 45 | */ 46 | public void drawHeader(RecyclerView recyclerView, Canvas canvas, View header, Rect offset) { 47 | canvas.save(); 48 | 49 | if (recyclerView.getLayoutManager().getClipToPadding()) { 50 | // Clip drawing of headers to the padding of the RecyclerView. Avoids drawing in the padding 51 | initClipRectForHeader(mTempRect, recyclerView, header); 52 | canvas.clipRect(mTempRect); 53 | } 54 | 55 | canvas.translate(offset.left, offset.top); 56 | 57 | header.draw(canvas); 58 | canvas.restore(); 59 | } 60 | 61 | /** 62 | * Initializes a clipping rect for the header based on the margins of the header and the padding of the 63 | * recycler. 64 | * FIXME: Currently right margin in VERTICAL orientation and bottom margin in HORIZONTAL 65 | * orientation are clipped so they look accurate, but the headers are not being drawn at the 66 | * correctly smaller width and height respectively. 67 | * 68 | * @param clipRect {@link Rect} for clipping a provided header to the padding of a recycler view 69 | * @param recyclerView for which to provide a header 70 | * @param header for clipping 71 | */ 72 | private void initClipRectForHeader(Rect clipRect, RecyclerView recyclerView, View header) { 73 | mDimensionCalculator.initMargins(clipRect, header); 74 | if (mOrientationProvider.getOrientation(recyclerView) == LinearLayout.VERTICAL) { 75 | clipRect.set( 76 | recyclerView.getPaddingLeft(), 77 | recyclerView.getPaddingTop(), 78 | recyclerView.getWidth() - recyclerView.getPaddingRight() - clipRect.right, 79 | recyclerView.getHeight() - recyclerView.getPaddingBottom()); 80 | } else { 81 | clipRect.set( 82 | recyclerView.getPaddingLeft(), 83 | recyclerView.getPaddingTop(), 84 | recyclerView.getWidth() - recyclerView.getPaddingRight(), 85 | recyclerView.getHeight() - recyclerView.getPaddingBottom() - clipRect.bottom); 86 | } 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/StickyRecyclerTouchListener.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil; 2 | 3 | import android.graphics.Rect; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.util.Log; 6 | import android.view.GestureDetector; 7 | import android.view.MotionEvent; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | import java.nio.file.attribute.PosixFileAttributes; 12 | 13 | public class StickyRecyclerTouchListener implements RecyclerView.OnItemTouchListener { 14 | private final GestureDetector mTapDetector; 15 | private final RecyclerView mRecyclerView; 16 | private final StickyRecyclerDecoration mDecor; 17 | private OnHeaderClickListener mOnHeaderClickListener; 18 | 19 | public interface OnHeaderClickListener { 20 | void onHeaderClick(View header, int position, long headerId); 21 | } 22 | 23 | public StickyRecyclerTouchListener(final RecyclerView recyclerView, 24 | final StickyRecyclerDecoration decor) { 25 | mTapDetector = new GestureDetector(recyclerView.getContext(), new SingleTapDetector()); 26 | mRecyclerView = recyclerView; 27 | mDecor = decor; 28 | } 29 | 30 | public StickyRecyclerAdapter getAdapter() { 31 | if (mRecyclerView.getAdapter() instanceof StickyRecyclerHeadersAdapter) { 32 | return (StickyRecyclerAdapter) mRecyclerView.getAdapter(); 33 | } else { 34 | throw new IllegalStateException("A RecyclerView with " + 35 | StickyRecyclerTouchListener.class.getSimpleName() + 36 | " requires a " + StickyRecyclerHeadersAdapter.class.getSimpleName()); 37 | } 38 | } 39 | 40 | 41 | public void setOnHeaderClickListener(OnHeaderClickListener listener) { 42 | mOnHeaderClickListener = listener; 43 | } 44 | 45 | @Override 46 | public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) { 47 | boolean tapDetectorResponse = this.mTapDetector.onTouchEvent(e); 48 | if (tapDetectorResponse) { 49 | // Don't return false if a single tap is detected 50 | return true; 51 | } 52 | if (e.getAction() == MotionEvent.ACTION_DOWN) { 53 | int position = mDecor.findHeaderPositionUnder((int) e.getX(), (int) e.getY()); 54 | return position != -1; 55 | } 56 | return false; 57 | } 58 | 59 | @Override 60 | public void onTouchEvent(RecyclerView view, MotionEvent e) { /* do nothing? */ } 61 | 62 | @Override 63 | public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { 64 | // do nothing 65 | } 66 | 67 | private class SingleTapDetector extends GestureDetector.SimpleOnGestureListener { 68 | @Override 69 | public boolean onSingleTapUp(MotionEvent e) { 70 | int position = mDecor.findHeaderPositionUnder((int) e.getX(), (int) e.getY()); 71 | if (position != -1) { 72 | View headerView = mDecor.getHeaderView(mRecyclerView, position); 73 | performClick(headerView, e ,position); 74 | return true; 75 | } 76 | position = mDecor.findFooterPositionUnder((int) e.getX(), (int) e.getY()); 77 | if (position != -1) { 78 | View footerView = mDecor.getFooterView(mRecyclerView, position); 79 | performClick(footerView, e,position); 80 | return true; 81 | } 82 | return false; 83 | } 84 | 85 | private void performClick(View view, MotionEvent e,int position) { 86 | if (view instanceof ViewGroup) { 87 | ViewGroup viewGroup = (ViewGroup) view; 88 | for (int i = 0; i < viewGroup.getChildCount(); i++) { 89 | View child = viewGroup.getChildAt(i); 90 | performClick(child, e ,position); 91 | } 92 | } 93 | 94 | containsBounds(view, e ,position); 95 | } 96 | 97 | private View containsBounds(View view, MotionEvent e ,int position) { 98 | int x = (int) e.getX(); 99 | int y = (int) e.getY(); 100 | Rect rect = new Rect(); 101 | view.getHitRect(rect); 102 | if (view.getVisibility() == View.VISIBLE 103 | && view.dispatchTouchEvent(e) 104 | && rect.left < rect.right && rect.top < rect.bottom && x >= rect.left && x < rect.right && y >= rect.top) { 105 | view.setTag(position); 106 | view.performClick(); 107 | return view; 108 | } 109 | return null; 110 | } 111 | 112 | @Override 113 | public boolean onDoubleTap(MotionEvent e) { 114 | return true; 115 | } 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter; 2 | 3 | import android.content.pm.ActivityInfo; 4 | import android.graphics.Color; 5 | import android.os.Bundle; 6 | import android.os.Handler; 7 | import android.os.Looper; 8 | import android.support.annotation.NonNull; 9 | import android.support.v7.app.AppCompatActivity; 10 | import android.support.v7.widget.LinearLayoutManager; 11 | import android.support.v7.widget.RecyclerView; 12 | import android.util.Log; 13 | import android.view.LayoutInflater; 14 | import android.view.View; 15 | import android.view.ViewGroup; 16 | import android.widget.BaseAdapter; 17 | import android.widget.Button; 18 | import android.widget.CheckBox; 19 | import android.widget.TextView; 20 | import android.widget.Toast; 21 | import android.widget.ToggleButton; 22 | 23 | import com.wujf.stickyheaderfooter.R; 24 | import com.wujf.stickyheaderfooter.headerfooterutil.StickyRecyclerAdapter; 25 | import com.wujf.stickyheaderfooter.headerfooterutil.StickyRecyclerDecoration; 26 | import com.wujf.stickyheaderfooter.headerfooterutil.StickyRecyclerTouchListener; 27 | 28 | import java.security.SecureRandom; 29 | 30 | public class MainActivity extends AppCompatActivity { 31 | private String[] mDatas; 32 | private RecyclerView recyclerView; 33 | 34 | @Override 35 | protected void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | setContentView(R.layout.activity_main); 38 | 39 | recyclerView = (RecyclerView) findViewById(R.id.recyclerView); 40 | // Set adapter populated with example dummy data 41 | final AnimalsHeaderFooterAdapter adapter = new AnimalsHeaderFooterAdapter(); 42 | mDatas = getDummyDataSet(); 43 | recyclerView.setAdapter(adapter); 44 | // Set layout manager 45 | int orientation = getLayoutManagerOrientation(getResources().getConfiguration().orientation); 46 | final LinearLayoutManager layoutManager = new LinearLayoutManager(this, orientation, false); 47 | recyclerView.setLayoutManager(layoutManager); 48 | 49 | // Add the sticky headers decoration 50 | final StickyRecyclerDecoration decor = new StickyRecyclerDecoration(adapter); 51 | recyclerView.addItemDecoration(decor); 52 | // Add other decoration for dividers between list items view cause errors; 53 | // Add touch listeners 54 | StickyRecyclerTouchListener touchListener = 55 | new StickyRecyclerTouchListener(recyclerView, decor); 56 | recyclerView.addOnItemTouchListener(touchListener); 57 | adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { 58 | @Override 59 | public void onChanged() { 60 | decor.invalidateHeaders(); 61 | decor.invalidateFooters(); 62 | } 63 | }); 64 | } 65 | 66 | private String[] getDummyDataSet() { 67 | return getResources().getStringArray(R.array.animals); 68 | } 69 | 70 | private int getLayoutManagerOrientation(int activityOrientation) { 71 | if (activityOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) { 72 | return LinearLayoutManager.VERTICAL; 73 | } else { 74 | return LinearLayoutManager.HORIZONTAL; 75 | } 76 | } 77 | 78 | private class AnimalsHeaderFooterAdapter extends RecyclerView.Adapter 79 | implements StickyRecyclerAdapter { 80 | @NonNull 81 | @Override 82 | public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 83 | View view = LayoutInflater.from(parent.getContext()) 84 | .inflate(R.layout.view_item, parent, false); 85 | return new ItemViewHolder(view); 86 | } 87 | 88 | @Override 89 | public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { 90 | holder.tvTitle.setText(position+"---->"+mDatas[position]); 91 | 92 | } 93 | 94 | @Override 95 | public int getItemCount() { 96 | return mDatas.length; 97 | } 98 | 99 | @Override 100 | public long getFooterId(int position) { 101 | return mDatas[position].charAt(0); 102 | } 103 | 104 | @Override 105 | public FooterViewHolder onCreateFooterViewHolder(ViewGroup parent) { 106 | View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.view_footer, parent, false); 107 | return new FooterViewHolder(view); 108 | } 109 | 110 | @Override 111 | public void onBindFooterViewHolder(FooterViewHolder holder, final int position) { 112 | holder.tvTitle.setText("footer "+String.valueOf(mDatas[position].charAt(0))); 113 | holder.tvTitle.setOnClickListener(new View.OnClickListener() { 114 | @Override 115 | public void onClick(View v) { 116 | final int itemPostion = (int)v.getTag(); 117 | Toast.makeText(MainActivity.this,"footer textview below item "+itemPostion+" be clicked",Toast.LENGTH_SHORT).show(); 118 | } 119 | }); 120 | 121 | } 122 | 123 | @Override 124 | public FooterViewHolder getFooterViewHolder(int position) { 125 | RecyclerView.ItemDecoration itemDecoration = recyclerView.getItemDecorationAt(0); 126 | if (itemDecoration instanceof StickyRecyclerDecoration) { 127 | return (FooterViewHolder) ((StickyRecyclerDecoration) itemDecoration).getFooterViewHolder(position); 128 | } 129 | return null; 130 | } 131 | 132 | @Override 133 | public long getHeaderId(int position) { 134 | return mDatas[position].charAt(0); 135 | } 136 | 137 | @Override 138 | public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) { 139 | View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.view_header, parent, false); 140 | return new HeaderViewHolder(view); 141 | } 142 | 143 | @Override 144 | public void onBindHeaderViewHolder(HeaderViewHolder holder, final int position) { 145 | holder.tvTitle.setText("header "+String.valueOf(mDatas[position].charAt(0))); 146 | holder.tvTitle.setOnClickListener(new View.OnClickListener() { 147 | @Override 148 | public void onClick(View v) { 149 | final int itemPosition = (int)v.getTag(); 150 | Toast.makeText(MainActivity.this,"header textview above item "+itemPosition+" be clicked",Toast.LENGTH_SHORT).show(); 151 | 152 | } 153 | }); 154 | } 155 | 156 | @Override 157 | public HeaderViewHolder getHeaderViewHolder(int position) { 158 | RecyclerView.ItemDecoration itemDecoration = recyclerView.getItemDecorationAt(0); 159 | if (itemDecoration instanceof StickyRecyclerDecoration) { 160 | return (HeaderViewHolder) ((StickyRecyclerDecoration) itemDecoration).getHeaderViewHolder(position); 161 | } 162 | return null; 163 | } 164 | } 165 | 166 | public static class HeaderViewHolder extends RecyclerView.ViewHolder { 167 | private TextView tvTitle; 168 | 169 | public HeaderViewHolder(View itemView) { 170 | super(itemView); 171 | tvTitle = itemView.findViewById(R.id.tv_title); 172 | } 173 | } 174 | 175 | public static class FooterViewHolder extends RecyclerView.ViewHolder { 176 | private TextView tvTitle; 177 | 178 | public FooterViewHolder(View itemView) { 179 | super(itemView); 180 | tvTitle = itemView.findViewById(R.id.tv_title); 181 | } 182 | } 183 | 184 | public static class ItemViewHolder extends RecyclerView.ViewHolder { 185 | TextView tvTitle; 186 | 187 | public ItemViewHolder(View itemView) { 188 | super(itemView); 189 | tvTitle = itemView.findViewById(R.id.tv_title); 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/StickyRecyclerDecoration.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Rect; 5 | import android.support.v7.widget.LinearLayoutManager; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.util.Log; 8 | import android.util.SparseArray; 9 | import android.view.View; 10 | 11 | import com.wujf.stickyheaderfooter.headerfooterutil.caching.FooterProvider; 12 | import com.wujf.stickyheaderfooter.headerfooterutil.caching.FooterViewCache; 13 | import com.wujf.stickyheaderfooter.headerfooterutil.caching.HeaderProvider; 14 | import com.wujf.stickyheaderfooter.headerfooterutil.caching.HeaderViewCache; 15 | import com.wujf.stickyheaderfooter.headerfooterutil.calculation.DimensionCalculator; 16 | import com.wujf.stickyheaderfooter.headerfooterutil.rendering.HeaderRenderer; 17 | import com.wujf.stickyheaderfooter.headerfooterutil.util.LinearLayoutOrientationProvider; 18 | import com.wujf.stickyheaderfooter.headerfooterutil.util.OrientationProvider; 19 | 20 | public class StickyRecyclerDecoration extends RecyclerView.ItemDecoration { 21 | 22 | private final StickyRecyclerAdapter mAdapter; 23 | private final ItemVisibilityAdapter mVisibilityAdapter; 24 | private final SparseArray mHeaderRects = new SparseArray<>(); 25 | private final SparseArray mFooterRects = new SparseArray<>(); 26 | private final HeaderProvider mHeaderProvider; 27 | private final FooterProvider mFooterProvider; 28 | private final OrientationProvider mOrientationProvider; 29 | private final HeaderPositionCalculator mHeaderPositionCalculator; 30 | private final HeaderRenderer mRenderer; 31 | private final DimensionCalculator mDimensionCalculator; 32 | 33 | /** 34 | * The following field is used as a buffer for internal calculations. Its sole purpose is to avoid 35 | * allocating new Rect every time we need one. 36 | */ 37 | private final Rect mTempRect = new Rect(); 38 | 39 | // TODO: Consider passing in orientation to simplify orientation accounting within calculation 40 | public StickyRecyclerDecoration(StickyRecyclerAdapter adapter) { 41 | this(adapter, new LinearLayoutOrientationProvider(), new DimensionCalculator(), null); 42 | } 43 | 44 | public StickyRecyclerDecoration(StickyRecyclerAdapter adapter, ItemVisibilityAdapter visibilityAdapter) { 45 | this(adapter, new LinearLayoutOrientationProvider(), new DimensionCalculator(), visibilityAdapter); 46 | } 47 | 48 | private StickyRecyclerDecoration(StickyRecyclerAdapter adapter, OrientationProvider orientationProvider, 49 | DimensionCalculator dimensionCalculator, ItemVisibilityAdapter visibilityAdapter) { 50 | this(adapter, orientationProvider, dimensionCalculator, new HeaderRenderer(orientationProvider), 51 | new HeaderViewCache(adapter, orientationProvider), new FooterViewCache(adapter, orientationProvider), visibilityAdapter); 52 | } 53 | 54 | private StickyRecyclerDecoration(StickyRecyclerAdapter adapter, OrientationProvider orientationProvider, 55 | DimensionCalculator dimensionCalculator, HeaderRenderer headerRenderer, HeaderProvider headerProvider, FooterProvider footerProvider, ItemVisibilityAdapter visibilityAdapter) { 56 | this(adapter, headerRenderer, orientationProvider, dimensionCalculator, headerProvider, footerProvider, 57 | new HeaderPositionCalculator(adapter, headerProvider, footerProvider, orientationProvider, 58 | dimensionCalculator), visibilityAdapter); 59 | } 60 | 61 | private StickyRecyclerDecoration(StickyRecyclerAdapter adapter, HeaderRenderer headerRenderer, 62 | OrientationProvider orientationProvider, DimensionCalculator dimensionCalculator, HeaderProvider headerProvider, FooterProvider footerProvider, 63 | HeaderPositionCalculator headerPositionCalculator, ItemVisibilityAdapter visibilityAdapter) { 64 | mAdapter = adapter; 65 | mHeaderProvider = headerProvider; 66 | mFooterProvider = footerProvider; 67 | mOrientationProvider = orientationProvider; 68 | mRenderer = headerRenderer; 69 | mDimensionCalculator = dimensionCalculator; 70 | mHeaderPositionCalculator = headerPositionCalculator; 71 | mVisibilityAdapter = visibilityAdapter; 72 | } 73 | 74 | @Override 75 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 76 | super.getItemOffsets(outRect, view, parent, state); 77 | int itemPosition = parent.getChildAdapterPosition(view); 78 | if (itemPosition == RecyclerView.NO_POSITION) { 79 | return; 80 | } 81 | if (mHeaderPositionCalculator.hasNewHeader(itemPosition, mOrientationProvider.isReverseLayout(parent))) { 82 | View header = getHeaderView(parent, itemPosition); 83 | setItemOffsetsForHeader(outRect, header, mOrientationProvider.getOrientation(parent)); 84 | } 85 | if (mHeaderPositionCalculator.hasNewFooter(itemPosition, mOrientationProvider.isReverseLayout(parent))) { 86 | View footer = getFooterView(parent, itemPosition); 87 | setItemOffsetsForFooter(outRect, footer, mOrientationProvider.getOrientation(parent)); 88 | } 89 | } 90 | 91 | /** 92 | * Sets the offsets for the first item in a section to make room for the header view 93 | * 94 | * @param itemOffsets rectangle to define offsets for the item 95 | * @param header view used to calculate offset for the item 96 | * @param orientation used to calculate offset for the item 97 | */ 98 | private void setItemOffsetsForHeader(Rect itemOffsets, View header, int orientation) { 99 | mDimensionCalculator.initMargins(mTempRect, header); 100 | if (orientation == LinearLayoutManager.VERTICAL) { 101 | itemOffsets.top = itemOffsets.top + header.getHeight() + mTempRect.top + mTempRect.bottom; 102 | } else { 103 | itemOffsets.left = itemOffsets.left + header.getWidth() + mTempRect.left + mTempRect.right; 104 | } 105 | } 106 | 107 | private void setItemOffsetsForFooter(Rect itemOffsets, View header, int orientation) { 108 | mDimensionCalculator.initMargins(mTempRect, header); 109 | if (orientation == LinearLayoutManager.VERTICAL) { 110 | itemOffsets.bottom = header.getHeight() + mTempRect.top + mTempRect.bottom; 111 | } else { 112 | itemOffsets.left = header.getWidth() + mTempRect.left + mTempRect.right; 113 | } 114 | } 115 | 116 | @Override 117 | public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) { 118 | super.onDrawOver(canvas, parent, state); 119 | 120 | final int childCount = parent.getChildCount(); 121 | if (childCount <= 0 || mAdapter.getItemCount() <= 0) { 122 | return; 123 | } 124 | 125 | for (int i = 0; i < childCount; i++) { 126 | View itemView = parent.getChildAt(i); 127 | int position = parent.getChildAdapterPosition(itemView); 128 | if (position == RecyclerView.NO_POSITION) { 129 | continue; 130 | } 131 | 132 | boolean hasStickyHeader = mHeaderPositionCalculator.hasStickyHeader(itemView, mOrientationProvider.getOrientation(parent), position); 133 | if (hasStickyHeader || mHeaderPositionCalculator.hasNewHeader(position, mOrientationProvider.isReverseLayout(parent))) { 134 | View header = mHeaderProvider.getHeader(parent, position); 135 | //re-use existing Rect, if any. 136 | Rect headerOffset = mHeaderRects.get(position); 137 | if (headerOffset == null) { 138 | headerOffset = new Rect(); 139 | mHeaderRects.put(position, headerOffset); 140 | } 141 | mHeaderPositionCalculator.initHeaderBounds(headerOffset, parent, header, itemView, hasStickyHeader); 142 | mRenderer.drawHeader(parent, canvas, header, headerOffset); 143 | } 144 | boolean hasStickyFooter = mHeaderPositionCalculator.hasStickyFooter(parent, itemView, mOrientationProvider.getOrientation(parent), position); 145 | if (hasStickyFooter || mHeaderPositionCalculator.hasNewFooter(position, mOrientationProvider.isReverseLayout(parent))) { 146 | View footer = mFooterProvider.getFooter(parent, position); 147 | Rect footerOffset = mFooterRects.get(position); 148 | if (footerOffset == null) { 149 | footerOffset = new Rect(); 150 | mFooterRects.put(position, footerOffset); 151 | } 152 | mHeaderPositionCalculator.initFooterBounds(footerOffset, parent, footer, itemView, hasStickyFooter); 153 | mRenderer.drawHeader(parent, canvas, footer, footerOffset); 154 | } 155 | } 156 | } 157 | 158 | /** 159 | * Gets the position of the header under the specified (x, y) coordinates. 160 | * 161 | * @param x x-coordinate 162 | * @param y y-coordinate 163 | * @return position of header, or -1 if not found 164 | */ 165 | public int findHeaderPositionUnder(int x, int y) { 166 | int tempPosition = -1; // Added 167 | for (int i = 0; i < mHeaderRects.size(); i++) { 168 | Rect rect = mHeaderRects.get(mHeaderRects.keyAt(i)); 169 | if (rect.contains(x, y)) { 170 | int position = mHeaderRects.keyAt(i); 171 | if (mVisibilityAdapter == null || mVisibilityAdapter.isPositionVisible(position)) { 172 | tempPosition = position; // Added 173 | } else { 174 | if (tempPosition != -1) { 175 | break;//my Added 176 | } 177 | } 178 | } 179 | } 180 | return tempPosition; // Added 181 | } 182 | 183 | /** 184 | * Gets the position of the footer under the specified (x, y) coordinates. 185 | * 186 | * @param x x-coordinate 187 | * @param y y-coordinate 188 | * @return position of header, or -1 if not found 189 | */ 190 | public int findFooterPositionUnder(int x, int y) { 191 | int tempPosition = -1; // Added 192 | for (int i = 0; i < mFooterRects.size(); i++) { 193 | Rect rect = mFooterRects.get(mFooterRects.keyAt(i)); 194 | if (rect.contains(x, y)) { 195 | int position = mFooterRects.keyAt(i); 196 | if (mVisibilityAdapter == null || mVisibilityAdapter.isPositionVisible(position)) { 197 | tempPosition = position; // Added 198 | } else { 199 | if (tempPosition != -1) { 200 | break;//my Added 201 | } 202 | } 203 | } 204 | } 205 | return tempPosition; // Added 206 | } 207 | 208 | /** 209 | * Gets the header view for the associated position. If it doesn't exist yet, it will be 210 | * created, measured, and laid out. 211 | * 212 | * @param parent the recyclerview 213 | * @param position the position to get the header view for 214 | * @return Header view 215 | */ 216 | public View getHeaderView(RecyclerView parent, int position) { 217 | return mHeaderProvider.getHeader(parent, position); 218 | } 219 | 220 | public View getFooterView(RecyclerView parent, int position) { 221 | return mFooterProvider.getFooter(parent, position); 222 | } 223 | 224 | public RecyclerView.ViewHolder getHeaderViewHolder(int position) { 225 | return mHeaderProvider.getHeaderViewHolder(position); 226 | } 227 | 228 | public RecyclerView.ViewHolder getFooterViewHolder(int position) { 229 | return mFooterProvider.getFooterViewHolder(position); 230 | } 231 | 232 | /** 233 | * Invalidates cached headers. This does not invalidate the recyclerview, you should do that manually after 234 | * calling this method. 235 | */ 236 | public void invalidateHeaders() { 237 | mHeaderProvider.invalidate(); 238 | mHeaderRects.clear(); 239 | } 240 | 241 | public void invalidateFooters() { 242 | mFooterProvider.invalidate(); 243 | mFooterRects.clear(); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Abyssinian 5 | Adelie Penguin 6 | Affenpinscher 7 | Afghan Hound 8 | African Bush Elephant 9 | African Civet 10 | African Clawed Frog 11 | African Forest Elephant 12 | African Palm Civet 13 | African Penguin 14 | African Tree Toad 15 | African Wild Dog 16 | Ainu Dog 17 | Airedale Terrier 18 | Akbash 19 | Akita 20 | Alaskan Malamute 21 | Albatross 22 | Aldabra Giant Tortoise 23 | Alligator 24 | Alpine Dachsbracke 25 | American Bulldog 26 | American Cocker Spaniel 27 | American Coonhound 28 | American Eskimo Dog 29 | American Foxhound 30 | American Pit Bull Terrier 31 | American Staffordshire Terrier 32 | American Water Spaniel 33 | Anatolian Shepherd Dog 34 | Angelfish 35 | Ant 36 | Anteater 37 | Antelope 38 | Appenzeller Dog 39 | Arctic Fox 40 | Arctic Hare 41 | Arctic Wolf 42 | Armadillo 43 | Asian Elephant 44 | Asian Giant Hornet 45 | Asian Palm Civet 46 | Asiatic Black Bear 47 | Australian Cattle Dog 48 | Australian Kelpie Dog 49 | Australian Mist 50 | Australian Shepherd 51 | Australian Terrier 52 | Avocet 53 | Axolotl 54 | Aye Aye 55 | Baboon 56 | Bactrian Camel 57 | Badger 58 | Balinese 59 | Banded Palm Civet 60 | Bandicoot 61 | Barb 62 | Barn Owl 63 | Barnacle 64 | Barracuda 65 | Basenji Dog 66 | Basking Shark 67 | Basset Hound 68 | Bat 69 | Bavarian Mountain Hound 70 | Beagle 71 | Bear 72 | Bearded Collie 73 | Bearded Dragon 74 | Beaver 75 | Bedlington Terrier 76 | Beetle 77 | Bengal Tiger 78 | Bernese Mountain Dog 79 | Bichon Frise 80 | Binturong 81 | Bird 82 | Birds Of Paradise 83 | Birman 84 | Bison 85 | Black Bear 86 | Black Rhinoceros 87 | Black Russian Terrier 88 | Black Widow Spider 89 | Bloodhound 90 | Blue Lacy Dog 91 | Blue Whale 92 | Bluetick Coonhound 93 | Bobcat 94 | Bolognese Dog 95 | Bombay 96 | Bongo 97 | Bonobo 98 | Booby 99 | Border Collie 100 | Border Terrier 101 | Bornean Orang-utan 102 | Borneo Elephant 103 | Boston Terrier 104 | Bottle Nosed Dolphin 105 | Boxer Dog 106 | Boykin Spaniel 107 | Brazilian Terrier 108 | Brown Bear 109 | Budgerigar 110 | Buffalo 111 | Bull Mastiff 112 | Bull Shark 113 | Bull Terrier 114 | Bulldog 115 | Bullfrog 116 | Bumble Bee 117 | Burmese 118 | Burrowing Frog 119 | Butterfly 120 | Butterfly Fish 121 | Caiman 122 | Caiman Lizard 123 | Cairn Terrier 124 | Camel 125 | Canaan Dog 126 | Capybara 127 | Caracal 128 | Carolina Dog 129 | Cassowary 130 | Cat 131 | Caterpillar 132 | Catfish 133 | Cavalier King Charles Spaniel 134 | Centipede 135 | Cesky Fousek 136 | Chameleon 137 | Chamois 138 | Cheetah 139 | Chesapeake Bay Retriever 140 | Chicken 141 | Chihuahua 142 | Chimpanzee 143 | Chinchilla 144 | Chinese Crested Dog 145 | Chinook 146 | Chinstrap Penguin 147 | Chipmunk 148 | Chow Chow 149 | Cichlid 150 | Clouded Leopard 151 | Clown Fish 152 | Clumber Spaniel 153 | Coati 154 | Cockroach 155 | Collared Peccary 156 | Collie 157 | Common Buzzard 158 | Common Frog 159 | Common Loon 160 | Common Toad 161 | Coral 162 | Cottontop Tamarin 163 | Cougar 164 | Cow 165 | Coyote 166 | Crab 167 | Crab-Eating Macaque 168 | Crane 169 | Crested Penguin 170 | Crocodile 171 | Cross River Gorilla 172 | Curly Coated Retriever 173 | Cuscus 174 | Cuttlefish 175 | Dachshund 176 | Dalmatian 177 | Darwin\'s Frog 178 | Deer 179 | Desert Tortoise 180 | Deutsche Bracke 181 | Dhole 182 | Dingo 183 | Discus 184 | Doberman Pinscher 185 | Dodo 186 | Dog 187 | Dogo Argentino 188 | Dogue De Bordeaux 189 | Dolphin 190 | Donkey 191 | Dormouse 192 | Dragonfly 193 | Drever 194 | Duck 195 | Dugong 196 | Dunker 197 | Dusky Dolphin 198 | Dwarf Crocodile 199 | Eagle 200 | Earwig 201 | Eastern Gorilla 202 | Eastern Lowland Gorilla 203 | Echidna 204 | Edible Frog 205 | Egyptian Mau 206 | Electric Eel 207 | Elephant 208 | Elephant Seal 209 | Elephant Shrew 210 | Emperor Penguin 211 | Emperor Tamarin 212 | Emu 213 | English Cocker Spaniel 214 | English Shepherd 215 | English Springer Spaniel 216 | Entlebucher Mountain Dog 217 | Epagneul Pont Audemer 218 | Eskimo Dog 219 | Estrela Mountain Dog 220 | Falcon 221 | Fennec Fox 222 | Ferret 223 | Field Spaniel 224 | Fin Whale 225 | Finnish Spitz 226 | Fire-Bellied Toad 227 | Fish 228 | Fishing Cat 229 | Flamingo 230 | Flat Coat Retriever 231 | Flounder 232 | Fly 233 | Flying Squirrel 234 | Fossa 235 | Fox 236 | Fox Terrier 237 | French Bulldog 238 | Frigatebird 239 | Frilled Lizard 240 | Frog 241 | Fur Seal 242 | Galapagos Penguin 243 | Galapagos Tortoise 244 | Gar 245 | Gecko 246 | Gentoo Penguin 247 | Geoffroys Tamarin 248 | Gerbil 249 | German Pinscher 250 | German Shepherd 251 | Gharial 252 | Giant African Land Snail 253 | Giant Clam 254 | Giant Panda Bear 255 | Giant Schnauzer 256 | Gibbon 257 | Gila Monster 258 | Giraffe 259 | Glass Lizard 260 | Glow Worm 261 | Goat 262 | Golden Lion Tamarin 263 | Golden Oriole 264 | Golden Retriever 265 | Goose 266 | Gopher 267 | Gorilla 268 | Grasshopper 269 | Great Dane 270 | Great White Shark 271 | Greater Swiss Mountain Dog 272 | Green Bee-Eater 273 | Greenland Dog 274 | Grey Mouse Lemur 275 | Grey Reef Shark 276 | Grey Seal 277 | Greyhound 278 | Grizzly Bear 279 | Grouse 280 | Guinea Fowl 281 | Guinea Pig 282 | Guppy 283 | Hammerhead Shark 284 | Hamster 285 | Hare 286 | Harrier 287 | Havanese 288 | Hedgehog 289 | Hercules Beetle 290 | Hermit Crab 291 | Heron 292 | Highland Cattle 293 | Himalayan 294 | Hippopotamus 295 | Honey Bee 296 | Horn Shark 297 | Horned Frog 298 | Horse 299 | Horseshoe Crab 300 | Howler Monkey 301 | Human 302 | Humboldt Penguin 303 | Hummingbird 304 | Humpback Whale 305 | Hyena 306 | Ibis 307 | Ibizan Hound 308 | Iguana 309 | Impala 310 | Indian Elephant 311 | Indian Palm Squirrel 312 | Indian Rhinoceros 313 | Indian Star Tortoise 314 | Indochinese Tiger 315 | Indri 316 | Insect 317 | Irish Setter 318 | Irish WolfHound 319 | Jack Russel 320 | Jackal 321 | Jaguar 322 | Japanese Chin 323 | Japanese Macaque 324 | Javan Rhinoceros 325 | Javanese 326 | Jellyfish 327 | Kakapo 328 | Kangaroo 329 | Keel Billed Toucan 330 | Killer Whale 331 | King Crab 332 | King Penguin 333 | Kingfisher 334 | Kiwi 335 | Koala 336 | Komodo Dragon 337 | Kudu 338 | Labradoodle 339 | Labrador Retriever 340 | Ladybird 341 | Leaf-Tailed Gecko 342 | Lemming 343 | Lemur 344 | Leopard 345 | Leopard Cat 346 | Leopard Seal 347 | Leopard Tortoise 348 | Liger 349 | Lion 350 | Lionfish 351 | Little Penguin 352 | Lizard 353 | Llama 354 | Lobster 355 | Long-Eared Owl 356 | Lynx 357 | Macaroni Penguin 358 | Macaw 359 | Magellanic Penguin 360 | Magpie 361 | Maine Coon 362 | Malayan Civet 363 | Malayan Tiger 364 | Maltese 365 | Manatee 366 | Mandrill 367 | Manta Ray 368 | Marine Toad 369 | Markhor 370 | Marsh Frog 371 | Masked Palm Civet 372 | Mastiff 373 | Mayfly 374 | Meerkat 375 | Millipede 376 | Minke Whale 377 | Mole 378 | Molly 379 | Mongoose 380 | Mongrel 381 | Monitor Lizard 382 | Monkey 383 | Monte Iberia Eleuth 384 | Moorhen 385 | Moose 386 | Moray Eel 387 | Moth 388 | Mountain Gorilla 389 | Mountain Lion 390 | Mouse 391 | Mule 392 | Neanderthal 393 | Neapolitan Mastiff 394 | Newfoundland 395 | Newt 396 | Nightingale 397 | Norfolk Terrier 398 | Norwegian Forest 399 | Numbat 400 | Nurse Shark 401 | Ocelot 402 | Octopus 403 | Okapi 404 | Old English Sheepdog 405 | Olm 406 | Opossum 407 | Orang-utan 408 | Ostrich 409 | Otter 410 | Oyster 411 | Pademelon 412 | Panther 413 | Parrot 414 | Patas Monkey 415 | Peacock 416 | Pekingese 417 | Pelican 418 | Penguin 419 | Persian 420 | Pheasant 421 | Pied Tamarin 422 | Pig 423 | Pika 424 | Pike 425 | Pink Fairy Armadillo 426 | Piranha 427 | Platypus 428 | Pointer 429 | Poison Dart Frog 430 | Polar Bear 431 | Pond Skater 432 | Poodle 433 | Pool Frog 434 | Porcupine 435 | Possum 436 | Prawn 437 | Proboscis Monkey 438 | Puffer Fish 439 | Puffin 440 | Pug 441 | Puma 442 | Purple Emperor 443 | Puss Moth 444 | Pygmy Hippopotamus 445 | Pygmy Marmoset 446 | Quail 447 | Quetzal 448 | Quokka 449 | Quoll 450 | Rabbit 451 | Raccoon 452 | Raccoon Dog 453 | Radiated Tortoise 454 | Ragdoll 455 | Rat 456 | Rattlesnake 457 | Red Knee Tarantula 458 | Red Panda 459 | Red Wolf 460 | Red-handed Tamarin 461 | Reindeer 462 | Rhinoceros 463 | River Dolphin 464 | River Turtle 465 | Robin 466 | Rock Hyrax 467 | Rockhopper Penguin 468 | Roseate Spoonbill 469 | Rottweiler 470 | Royal Penguin 471 | Russian Blue 472 | Sabre-Toothed Tiger 473 | Saint Bernard 474 | Salamander 475 | Sand Lizard 476 | Saola 477 | Scorpion 478 | Scorpion Fish 479 | Sea Dragon 480 | Sea Lion 481 | Sea Otter 482 | Sea Slug 483 | Sea Squirt 484 | Sea Turtle 485 | Sea Urchin 486 | Seahorse 487 | Seal 488 | Serval 489 | Sheep 490 | Shih Tzu 491 | Shrimp 492 | Siamese 493 | Siamese Fighting Fish 494 | Siberian 495 | Siberian Husky 496 | Siberian Tiger 497 | Silver Dollar 498 | Skunk 499 | Sloth 500 | Slow Worm 501 | Snail 502 | Snake 503 | Snapping Turtle 504 | Snowshoe 505 | Snowy Owl 506 | Somali 507 | South China Tiger 508 | Spadefoot Toad 509 | Sparrow 510 | Spectacled Bear 511 | Sperm Whale 512 | Spider Monkey 513 | Spiny Dogfish 514 | Sponge 515 | Squid 516 | Squirrel 517 | Squirrel Monkey 518 | Sri Lankan Elephant 519 | Staffordshire Bull Terrier 520 | Stag Beetle 521 | Starfish 522 | Stellers Sea Cow 523 | Stick Insect 524 | Stingray 525 | Stoat 526 | Striped Rocket Frog 527 | Sumatran Elephant 528 | Sumatran Orang-utan 529 | Sumatran Rhinoceros 530 | Sumatran Tiger 531 | Sun Bear 532 | Swan 533 | Tang 534 | Tapir 535 | Tarsier 536 | Tasmanian Devil 537 | Tawny Owl 538 | Termite 539 | Tetra 540 | Thorny Devil 541 | Tibetan Mastiff 542 | Tiffany 543 | Tiger 544 | Tiger Salamander 545 | Tiger Shark 546 | Tortoise 547 | Toucan 548 | Tree Frog 549 | Tropicbird 550 | Tuatara 551 | Turkey 552 | Turkish Angora 553 | Uakari 554 | Uguisu 555 | Umbrellabird 556 | Vampire Bat 557 | Vervet Monkey 558 | Vulture 559 | Wallaby 560 | Walrus 561 | Warthog 562 | Wasp 563 | Water Buffalo 564 | Water Dragon 565 | Water Vole 566 | Weasel 567 | Welsh Corgi 568 | West Highland Terrier 569 | Western Gorilla 570 | Western Lowland Gorilla 571 | Whale Shark 572 | Whippet 573 | White Faced Capuchin 574 | White Rhinoceros 575 | White Tiger 576 | Wild Boar 577 | Wildebeest 578 | Wolf 579 | Wolverine 580 | Wombat 581 | Woodlouse 582 | Woodpecker 583 | Woolly Mammoth 584 | Woolly Monkey 585 | Wrasse 586 | X-Ray Tetra 587 | XGB 588 | Yak 589 | Yellow-Eyed Penguin 590 | Yorkshire Terrier 591 | Zebra 592 | Zebra Shark 593 | Zebu 594 | Zonkey 595 | Zorse 596 | Zebrar 597 | Zebraf Shark 598 | Zebue 599 | Zonkeyq 600 | Zorsee 601 | Zebraw 602 | Zebra Sharkr 603 | Zebura 604 | Zonkeyma 605 | Zorsere 606 | 607 | -------------------------------------------------------------------------------- /app/src/main/java/com/wujf/stickyheaderfooter/headerfooterutil/HeaderPositionCalculator.java: -------------------------------------------------------------------------------- 1 | package com.wujf.stickyheaderfooter.headerfooterutil; 2 | 3 | import android.graphics.Rect; 4 | import android.support.v7.widget.LinearLayoutManager; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.util.Log; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.LinearLayout; 10 | 11 | import com.wujf.stickyheaderfooter.headerfooterutil.caching.FooterProvider; 12 | import com.wujf.stickyheaderfooter.headerfooterutil.caching.HeaderProvider; 13 | import com.wujf.stickyheaderfooter.headerfooterutil.calculation.DimensionCalculator; 14 | import com.wujf.stickyheaderfooter.headerfooterutil.util.OrientationProvider; 15 | 16 | 17 | /** 18 | * Calculates the position and location of header views 19 | */ 20 | public class HeaderPositionCalculator { 21 | 22 | private final StickyRecyclerAdapter mAdapter; 23 | private final OrientationProvider mOrientationProvider; 24 | private final HeaderProvider mHeaderProvider; 25 | private final DimensionCalculator mDimensionCalculator; 26 | private FooterProvider mFooterProvider; 27 | 28 | /** 29 | * The following fields are used as buffers for internal calculations. Their sole purpose is to avoid 30 | * allocating new Rect every time we need one. 31 | */ 32 | private final Rect mTempRect1 = new Rect(); 33 | private final Rect mTempRect2 = new Rect(); 34 | 35 | public HeaderPositionCalculator(StickyRecyclerAdapter adapter, HeaderProvider headerProvider, FooterProvider footerProvider, 36 | OrientationProvider orientationProvider, DimensionCalculator dimensionCalculator) { 37 | mAdapter = adapter; 38 | mHeaderProvider = headerProvider; 39 | mFooterProvider = footerProvider; 40 | mOrientationProvider = orientationProvider; 41 | mDimensionCalculator = dimensionCalculator; 42 | } 43 | 44 | /** 45 | * Determines if a view should have a sticky header. 46 | * The view has a sticky header if: 47 | * 1. It is the first element in the recycler view 48 | * 2. It has a valid ID associated to its position 49 | * 50 | * @param itemView given by the RecyclerView 51 | * @param orientation of the Recyclerview 52 | * @param position of the list item in question 53 | * @return True if the view should have a sticky header 54 | */ 55 | public boolean hasStickyHeader(View itemView, int orientation, int position) { 56 | int offset, margin; 57 | mDimensionCalculator.initMargins(mTempRect1, itemView); 58 | if (orientation == LinearLayout.VERTICAL) { 59 | offset = itemView.getTop(); 60 | margin = mTempRect1.top; 61 | } else { 62 | offset = itemView.getLeft(); 63 | margin = mTempRect1.left; 64 | } 65 | 66 | return offset >= -itemView.getHeight() - mTempRect1.bottom && offset <= margin && mAdapter.getHeaderId(position) >= 0; 67 | //return offset <= margin&& mAdapter.getHeaderId(position) >= 0; 68 | } 69 | 70 | public boolean hasStickyFooter(RecyclerView parent, View itemView, int orientation, int position) { 71 | int offset = 0, margin = 0; 72 | mDimensionCalculator.initMargins(mTempRect1, itemView); 73 | if (orientation == LinearLayout.VERTICAL) { 74 | offset = itemView.getBottom(); 75 | margin = mTempRect1.bottom; 76 | } else { 77 | 78 | } 79 | return parent.getHeight() - margin <= offset && mAdapter.getHeaderId(position) >= 0; 80 | } 81 | 82 | 83 | /** 84 | * Determines if an item in the list should have a header that is different than the item in the 85 | * list that immediately precedes it. Items with no headers will always return false. 86 | * 87 | * @param position of the list item in questions 88 | * @param isReverseLayout TRUE if layout manager has flag isReverseLayout 89 | * @return true if this item has a different header than the previous item in the list 90 | */ 91 | public boolean hasNewHeader(int position, boolean isReverseLayout) { 92 | if (indexOutOfBounds(position)) { 93 | return false; 94 | } 95 | 96 | long headerId = mAdapter.getHeaderId(position); 97 | 98 | if (headerId < 0) { 99 | return false; 100 | } 101 | 102 | long nextItemHeaderId = -1; 103 | int nextItemPosition = position + (isReverseLayout ? 1 : -1); 104 | if (!indexOutOfBounds(nextItemPosition)) { 105 | nextItemHeaderId = mAdapter.getHeaderId(nextItemPosition); 106 | } 107 | return headerId != nextItemHeaderId; 108 | } 109 | 110 | public boolean hasNewFooter(int position, boolean isReverseLayout) { 111 | if (indexOutOfBounds(position)) { 112 | return false; 113 | } 114 | 115 | long headerId = mAdapter.getHeaderId(position); 116 | 117 | if (headerId < 0) { 118 | return false; 119 | } 120 | 121 | long nextItemHeaderId = -1; 122 | int nextItemPosition = position + (isReverseLayout ? -1 : 1); 123 | if (!indexOutOfBounds(nextItemPosition)) { 124 | nextItemHeaderId = mAdapter.getHeaderId(nextItemPosition); 125 | } 126 | return headerId != nextItemHeaderId; 127 | } 128 | 129 | private boolean indexOutOfBounds(int position) { 130 | return position < 0 || position >= mAdapter.getItemCount(); 131 | } 132 | 133 | public void initHeaderBounds(Rect bounds, RecyclerView recyclerView, View header, View firstView, boolean firstHeader) { 134 | int orientation = mOrientationProvider.getOrientation(recyclerView); 135 | initDefaultHeaderOffset(bounds, recyclerView, header, firstView, orientation); 136 | if (firstHeader && isStickyHeaderBeingPushedOffscreen(recyclerView, header)) { 137 | View lastViewObscuredByHeader = getLastViewObscuredByHeader(recyclerView, header); 138 | translateHeaderWithLastViewObscureByHeader(recyclerView, mOrientationProvider.getOrientation(recyclerView), bounds, 139 | header, lastViewObscuredByHeader); 140 | } 141 | } 142 | 143 | public void initFooterBounds(Rect bounds, RecyclerView recyclerView, View footer, View firstView, boolean firstFooter) { 144 | int orientation = mOrientationProvider.getOrientation(recyclerView); 145 | initDefaultFooterOffset(bounds, recyclerView, footer, firstView, orientation); 146 | if (firstFooter && isStickyFooterBeingPushedOffscreen(recyclerView, footer)) { 147 | View lastViewObscuredByFooter = getLastViewObscuredByFooter(recyclerView, footer); 148 | translateFooterWithLastViewObscureByHeader(recyclerView, mOrientationProvider.getOrientation(recyclerView), bounds, 149 | footer, lastViewObscuredByFooter); 150 | } 151 | } 152 | 153 | private void initDefaultHeaderOffset(Rect headerMargins, RecyclerView recyclerView, View header, View firstView, int orientation) { 154 | int translationX, translationY; 155 | mDimensionCalculator.initMargins(mTempRect1, header); 156 | 157 | ViewGroup.LayoutParams layoutParams = firstView.getLayoutParams(); 158 | int leftMargin = 0; 159 | int topMargin = 0; 160 | if (layoutParams instanceof ViewGroup.MarginLayoutParams) { 161 | ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) layoutParams; 162 | leftMargin = marginLayoutParams.leftMargin; 163 | topMargin = marginLayoutParams.topMargin; 164 | } 165 | 166 | if (orientation == LinearLayoutManager.VERTICAL) { 167 | translationX = firstView.getLeft() - leftMargin + mTempRect1.left; 168 | translationY = Math.max( 169 | firstView.getTop() - topMargin - header.getHeight() - mTempRect1.bottom, 170 | getListTop(recyclerView) + mTempRect1.top); 171 | } else { 172 | translationY = firstView.getTop() - topMargin + mTempRect1.top; 173 | translationX = Math.max( 174 | firstView.getLeft() - leftMargin - header.getWidth() - mTempRect1.right, 175 | getListLeft(recyclerView) + mTempRect1.left); 176 | } 177 | 178 | headerMargins.set(translationX, translationY, translationX + header.getWidth(), 179 | translationY + header.getHeight()); 180 | } 181 | 182 | private void initDefaultFooterOffset(Rect footerMargins, RecyclerView recyclerView, View footer, View firstView, int orientation) { 183 | int translationX = -1, translationY = -1; 184 | mDimensionCalculator.initMargins(mTempRect1, footer); 185 | ViewGroup.LayoutParams layoutParams = firstView.getLayoutParams(); 186 | int leftMargin = 0; 187 | int bottomMargin = 0; 188 | if (layoutParams instanceof ViewGroup.MarginLayoutParams) { 189 | ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) layoutParams; 190 | leftMargin = marginLayoutParams.leftMargin; 191 | bottomMargin = marginLayoutParams.bottomMargin; 192 | } 193 | 194 | if (orientation == LinearLayoutManager.VERTICAL) { 195 | translationX = firstView.getLeft() - leftMargin + mTempRect1.left; 196 | translationY = Math.min( 197 | firstView.getBottom() + bottomMargin + mTempRect1.top + footer.getHeight(), 198 | getListBottom(recyclerView) - mTempRect1.bottom); 199 | } else { 200 | // translationY = firstView.getTop() - topMargin + mTempRect1.top; 201 | // translationX = Math.max( 202 | // firstView.getLeft() - leftMargin - header.getWidth() - mTempRect1.right, 203 | // getListLeft(recyclerView) + mTempRect1.left); 204 | } 205 | footerMargins.set(translationX, translationY - footer.getHeight(), translationX + footer.getWidth(), 206 | translationY); 207 | } 208 | 209 | // private boolean isStickyHeaderBeingPushedOffscreen(RecyclerView recyclerView, View stickyHeader) { 210 | // View viewAfterHeader = getFirstViewUnobscuredByHeader(recyclerView, stickyHeader); 211 | // int firstViewUnderHeaderPosition = recyclerView.getChildAdapterPosition(viewAfterHeader); 212 | // if (firstViewUnderHeaderPosition == RecyclerView.NO_POSITION) { 213 | // return false; 214 | // } 215 | // 216 | // boolean isReverseLayout = mOrientationProvider.isReverseLayout(recyclerView); 217 | // if (firstViewUnderHeaderPosition > 0 && hasNewHeader(firstViewUnderHeaderPosition, isReverseLayout)) { 218 | // View nextHeader = mHeaderProvider.getHeader(recyclerView, firstViewUnderHeaderPosition); 219 | // mDimensionCalculator.initMargins(mTempRect1, nextHeader); 220 | // mDimensionCalculator.initMargins(mTempRect2, stickyHeader); 221 | // 222 | // if (mOrientationProvider.getOrientation(recyclerView) == LinearLayoutManager.VERTICAL) { 223 | // int topOfNextHeader = viewAfterHeader.getTop() - mTempRect1.bottom - nextHeader.getHeight() - mTempRect1.top; 224 | // int bottomOfThisHeader = recyclerView.getPaddingTop() + stickyHeader.getBottom() + mTempRect2.top + mTempRect2.bottom; 225 | // if (topOfNextHeader < bottomOfThisHeader) { 226 | // return true; 227 | // } 228 | // } else { 229 | // int leftOfNextHeader = viewAfterHeader.getLeft() - mTempRect1.right - nextHeader.getWidth() - mTempRect1.left; 230 | // int rightOfThisHeader = recyclerView.getPaddingLeft() + stickyHeader.getRight() + mTempRect2.left + mTempRect2.right; 231 | // if (leftOfNextHeader < rightOfThisHeader) { 232 | // return true; 233 | // } 234 | // } 235 | // } 236 | // 237 | // 238 | // return false; 239 | // } 240 | private boolean isStickyHeaderBeingPushedOffscreen(RecyclerView recyclerView, View stickyHeader) { 241 | View viewAfterHeader = getLastViewObscuredByHeader(recyclerView, stickyHeader); 242 | int lastViewUnderHeaderPosition = recyclerView.getChildAdapterPosition(viewAfterHeader); 243 | if (lastViewUnderHeaderPosition == RecyclerView.NO_POSITION) { 244 | return false; 245 | } 246 | boolean isReverseLayout = mOrientationProvider.isReverseLayout(recyclerView); 247 | boolean hasNewFooter = hasNewFooter(lastViewUnderHeaderPosition, isReverseLayout); 248 | if (lastViewUnderHeaderPosition > 0 && hasNewFooter) { 249 | View nextFooter = mFooterProvider.getFooter(recyclerView, lastViewUnderHeaderPosition); 250 | mDimensionCalculator.initMargins(mTempRect1, nextFooter); 251 | mDimensionCalculator.initMargins(mTempRect2, stickyHeader); 252 | 253 | if (mOrientationProvider.getOrientation(recyclerView) == LinearLayoutManager.VERTICAL) { 254 | int topOfNextFooter = viewAfterHeader.getBottom() + mTempRect1.top; 255 | int bottomOfThisHeader = recyclerView.getPaddingTop() + stickyHeader.getBottom() + mTempRect2.top + mTempRect2.bottom; 256 | if (topOfNextFooter < bottomOfThisHeader) { 257 | return true; 258 | // } 259 | } 260 | } else { 261 | // int leftOfNextHeader = viewAfterHeader.getLeft() - mTempRect1.right - nextHeader.getWidth() - mTempRect1.left; 262 | // int rightOfThisHeader = recyclerView.getPaddingLeft() + stickyHeader.getRight() + mTempRect2.left + mTempRect2.right; 263 | // if (leftOfNextHeader < rightOfThisHeader) { 264 | // return true; 265 | // } 266 | } 267 | } 268 | return false; 269 | } 270 | 271 | private boolean isStickyFooterBeingPushedOffscreen(RecyclerView recyclerView, View stickyFooter) { 272 | View lastViewObscuredByFooter = getLastViewObscuredByFooter(recyclerView, stickyFooter); 273 | int lastViewObscuredByFooterPostion = recyclerView.getChildAdapterPosition(lastViewObscuredByFooter); 274 | if (lastViewObscuredByFooterPostion == RecyclerView.NO_POSITION) { 275 | return false; 276 | } 277 | boolean isReverseLayout = mOrientationProvider.isReverseLayout(recyclerView); 278 | boolean hasNewHeader = hasNewHeader(lastViewObscuredByFooterPostion, isReverseLayout); 279 | if (lastViewObscuredByFooterPostion > 0 && hasNewHeader) { 280 | View nextHeader = mHeaderProvider.getHeader(recyclerView, lastViewObscuredByFooterPostion); 281 | mDimensionCalculator.initMargins(mTempRect1, nextHeader); 282 | mDimensionCalculator.initMargins(mTempRect2, stickyFooter); 283 | ViewGroup.LayoutParams layoutParams = lastViewObscuredByFooter.getLayoutParams(); 284 | int leftMargin = 0; 285 | int topMargin = 0; 286 | if (layoutParams instanceof ViewGroup.MarginLayoutParams) { 287 | ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) layoutParams; 288 | leftMargin = marginLayoutParams.leftMargin; 289 | topMargin = marginLayoutParams.topMargin; 290 | } 291 | if (mOrientationProvider.getOrientation(recyclerView) == LinearLayoutManager.VERTICAL) { 292 | int bottomOfNextHeader = lastViewObscuredByFooter.getTop() - topMargin; 293 | int footerHeight = stickyFooter.getHeight(); 294 | int topOfThisFooter = getListBottom(recyclerView) - footerHeight - mTempRect2.bottom - mTempRect2.top; 295 | if (topOfThisFooter < bottomOfNextHeader) { 296 | return true; 297 | // } 298 | } 299 | } else { 300 | // int leftOfNextHeader = viewAfterHeader.getLeft() - mTempRect1.right - nextHeader.getWidth() - mTempRect1.left; 301 | // int rightOfThisHeader = recyclerView.getPaddingLeft() + stickyHeader.getRight() + mTempRect2.left + mTempRect2.right; 302 | // if (leftOfNextHeader < rightOfThisHeader) { 303 | // return true; 304 | // } 305 | } 306 | } 307 | 308 | 309 | return false; 310 | } 311 | 312 | private void translateHeaderWithNextHeader(RecyclerView recyclerView, int orientation, Rect translation, 313 | View currentHeader, View viewAfterNextHeader, View nextHeader) { 314 | mDimensionCalculator.initMargins(mTempRect1, nextHeader); 315 | mDimensionCalculator.initMargins(mTempRect2, currentHeader); 316 | if (orientation == LinearLayoutManager.VERTICAL) { 317 | int topOfStickyHeader = getListTop(recyclerView) + mTempRect2.top + mTempRect2.bottom; 318 | int shiftFromNextHeader = viewAfterNextHeader.getTop() - nextHeader.getHeight() - mTempRect1.bottom - mTempRect1.top - currentHeader.getHeight() - topOfStickyHeader; 319 | if (shiftFromNextHeader < topOfStickyHeader) { 320 | translation.top += shiftFromNextHeader; 321 | } 322 | } else { 323 | int leftOfStickyHeader = getListLeft(recyclerView) + mTempRect2.left + mTempRect2.right; 324 | int shiftFromNextHeader = viewAfterNextHeader.getLeft() - nextHeader.getWidth() - mTempRect1.right - mTempRect1.left - currentHeader.getWidth() - leftOfStickyHeader; 325 | if (shiftFromNextHeader < leftOfStickyHeader) { 326 | translation.left += shiftFromNextHeader; 327 | } 328 | } 329 | } 330 | 331 | private void translateHeaderWithLastViewObscureByHeader(RecyclerView recyclerView, int orientation, Rect translation, View currentHeader, View lastViewUnderHeader) { 332 | mDimensionCalculator.initMargins(mTempRect2, currentHeader); 333 | if (orientation == LinearLayoutManager.VERTICAL) { 334 | int shiftFromlastViewUnderHeader = lastViewUnderHeader.getBottom() + mTempRect2.bottom - currentHeader.getHeight() - getListTop(recyclerView) - mTempRect2.top - mTempRect2.bottom; 335 | translation.top += shiftFromlastViewUnderHeader; 336 | } else { 337 | 338 | } 339 | } 340 | 341 | private void translateFooterWithLastViewObscureByHeader(RecyclerView recyclerView, int orientation, Rect translation, View currentFooter, View lastViewUnderFooter) { 342 | mDimensionCalculator.initMargins(mTempRect2, currentFooter); 343 | if (orientation == LinearLayoutManager.VERTICAL) { 344 | ViewGroup.LayoutParams layoutParams = lastViewUnderFooter.getLayoutParams(); 345 | int leftMargin = 0; 346 | int topMargin = 0; 347 | if (layoutParams instanceof ViewGroup.MarginLayoutParams) { 348 | ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) layoutParams; 349 | leftMargin = marginLayoutParams.leftMargin; 350 | topMargin = marginLayoutParams.bottomMargin; 351 | } 352 | // int shiftFromlastViewUnderFooter = lastViewUnderFooter.getBottom()+mTempRect2.bottom-(getListBottom(recyclerView)- currentFooter.getHeight()-mTempRect2.bottom-mTempRect2.top); 353 | int shiftFormlastViewUnderFooter = lastViewUnderFooter.getTop() - topMargin - (getListBottom(recyclerView) - currentFooter.getHeight() - mTempRect2.bottom - mTempRect2.top); 354 | if (shiftFormlastViewUnderFooter > 0) { 355 | translation.top += shiftFormlastViewUnderFooter; 356 | } 357 | 358 | } else { 359 | 360 | } 361 | } 362 | 363 | 364 | /** 365 | * Returns the first item currently in the RecyclerView that is not obscured by a header. 366 | * 367 | * @param parent Recyclerview containing all the list items 368 | * @return first item that is fully beneath a header 369 | */ 370 | private View getFirstViewUnobscuredByHeader(RecyclerView parent, View firstHeader) { 371 | boolean isReverseLayout = mOrientationProvider.isReverseLayout(parent); 372 | int step = isReverseLayout ? -1 : 1; 373 | int from = isReverseLayout ? parent.getChildCount() - 1 : 0; 374 | for (int i = from; i >= 0 && i <= parent.getChildCount() - 1; i += step) { 375 | View child = parent.getChildAt(i); 376 | if (!itemIsObscuredByHeader(parent, child, firstHeader, mOrientationProvider.getOrientation(parent))) { 377 | return child; 378 | } 379 | } 380 | return null; 381 | } 382 | 383 | private View getLastViewObscuredByHeader(RecyclerView parent, View firstHeader) { 384 | boolean isReverseLayout = mOrientationProvider.isReverseLayout(parent); 385 | int step = isReverseLayout ? -1 : 1; 386 | int from = isReverseLayout ? parent.getChildCount() - 1 : 0; 387 | for (int i = from; i >= 0 && i <= parent.getChildCount() - 1; i += step) { 388 | View child = parent.getChildAt(i); 389 | if (!itemIsObscuredByHeader(parent, child, firstHeader, mOrientationProvider.getOrientation(parent))) { 390 | int lastViewObscuredByHeaderPosition = i - 1; 391 | if (lastViewObscuredByHeaderPosition >= 0) { 392 | return parent.getChildAt(lastViewObscuredByHeaderPosition); 393 | } else { 394 | return null; 395 | } 396 | } 397 | } 398 | return null; 399 | 400 | } 401 | 402 | private View getLastViewObscuredByFooter(RecyclerView parent, View firstFooter) { 403 | boolean isReverseLayout = mOrientationProvider.isReverseLayout(parent); 404 | int step = isReverseLayout ? -1 : +1; 405 | int from = isReverseLayout ? parent.getChildCount() - 1 : 0; 406 | for (int i = from; i >= 0 && i <= parent.getChildCount() - 1; i += step) { 407 | View child = parent.getChildAt(i); 408 | if (itemIsObscuredByFooter(parent, child, firstFooter, mOrientationProvider.getOrientation(parent))) { 409 | return child; 410 | } 411 | } 412 | return null; 413 | 414 | } 415 | 416 | /** 417 | * Determines if an item is obscured by a header 418 | * 419 | * @param parent 420 | * @param item to determine if obscured by header 421 | * @param header that might be obscuring the item 422 | * @param orientation of the {@link RecyclerView} 423 | * @return true if the item view is obscured by the header view 424 | */ 425 | private boolean itemIsObscuredByHeader(RecyclerView parent, View item, View header, int orientation) { 426 | RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) item.getLayoutParams(); 427 | mDimensionCalculator.initMargins(mTempRect1, header); 428 | 429 | int adapterPosition = parent.getChildAdapterPosition(item); 430 | if (adapterPosition == RecyclerView.NO_POSITION || mHeaderProvider.getHeader(parent, adapterPosition) != header) { 431 | // Resolves https://github.com/timehop/sticky-headers-recyclerview/issues/36 432 | // Handles an edge case where a trailing header is smaller than the current sticky header. 433 | return false; 434 | } 435 | 436 | if (orientation == LinearLayoutManager.VERTICAL) { 437 | int itemTop = item.getTop() - layoutParams.topMargin; 438 | int headerBottom = getListTop(parent) + header.getBottom() + mTempRect1.bottom + mTempRect1.top; 439 | if (itemTop >= headerBottom) { 440 | return false; 441 | } 442 | } else { 443 | int itemLeft = item.getLeft() - layoutParams.leftMargin; 444 | int headerRight = getListLeft(parent) + header.getRight() + mTempRect1.right + mTempRect1.left; 445 | if (itemLeft >= headerRight) { 446 | return false; 447 | } 448 | } 449 | 450 | return true; 451 | } 452 | 453 | private boolean itemIsObscuredByFooter(RecyclerView parent, View item, View footer, int orientation) { 454 | RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) item.getLayoutParams(); 455 | mDimensionCalculator.initMargins(mTempRect1, footer); 456 | 457 | int adapterPosition = parent.getChildAdapterPosition(item); 458 | if (adapterPosition == RecyclerView.NO_POSITION || mFooterProvider.getFooter(parent, adapterPosition) != footer) { 459 | // Resolves https://github.com/timehop/sticky-headers-recyclerview/issues/36 460 | // Handles an edge case where a trailing header is smaller than the current sticky header. 461 | if (parent.getChildAdapterPosition(item) == 583) { 462 | } 463 | return false; 464 | } 465 | 466 | if (orientation == LinearLayoutManager.VERTICAL) { 467 | int itemBottom = item.getBottom() + layoutParams.bottomMargin; 468 | int footerTop = getListBottom(parent) - footer.getHeight() - mTempRect1.bottom - mTempRect1.top; 469 | if (itemBottom >= footerTop) { 470 | return true; 471 | } 472 | } else { 473 | // int itemLeft = item.getLeft() - layoutParams.leftMargin; 474 | // int headerRight = getListLeft(parent) + header.getRight() + mTempRect1.right + mTempRect1.left; 475 | // if (itemLeft >= headerRight) { 476 | // return false; 477 | // } 478 | } 479 | 480 | return false; 481 | } 482 | 483 | private int getListTop(RecyclerView view) { 484 | if (view.getLayoutManager().getClipToPadding()) { 485 | return view.getPaddingTop(); 486 | } else { 487 | return 0; 488 | } 489 | } 490 | 491 | private int getListBottom(RecyclerView view) { 492 | if (view.getLayoutManager().getClipToPadding()) { 493 | return view.getHeight() - view.getPaddingBottom(); 494 | } else { 495 | return view.getHeight(); 496 | } 497 | } 498 | 499 | private int getListLeft(RecyclerView view) { 500 | if (view.getLayoutManager().getClipToPadding()) { 501 | return view.getPaddingLeft(); 502 | } else { 503 | return 0; 504 | } 505 | } 506 | } 507 | --------------------------------------------------------------------------------