├── .gitignore ├── README.md ├── build.gradle ├── demo ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── qozix │ │ └── endlessrecyclerview │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── books.png │ │ └── json │ │ │ ├── mock-search-1.json │ │ │ ├── mock-search-10.json │ │ │ ├── mock-search-2.json │ │ │ ├── mock-search-3.json │ │ │ ├── mock-search-4.json │ │ │ ├── mock-search-5.json │ │ │ ├── mock-search-6.json │ │ │ ├── mock-search-7.json │ │ │ ├── mock-search-8.json │ │ │ └── mock-search-9.json │ ├── java │ │ └── com │ │ │ └── qozix │ │ │ └── endlessrecyclerview │ │ │ └── demo │ │ │ ├── CommonDemoEndlessAdapter.java │ │ │ ├── ItemHolder.java │ │ │ ├── MainActivity.java │ │ │ ├── models │ │ │ ├── JsonResponse.java │ │ │ └── MediaItem.java │ │ │ ├── network │ │ │ ├── MockClient.java │ │ │ ├── MockNetworkDemoActivity.java │ │ │ └── MockNetworkDemoEndlessAdapter.java │ │ │ └── simple │ │ │ ├── SimpleDemoActivity.java │ │ │ └── SimpleDemoEndlessAdapter.java │ └── res │ │ ├── drawable-xxxhdpi │ │ └── ic_headphones.png │ │ ├── drawable │ │ └── shape_border_thin.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── endless_row.xml │ │ └── endlessrecyclerview.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── qozix │ └── endlessrecyclerview │ └── ExampleUnitTest.java ├── docs ├── allclasses-frame.html ├── allclasses-noframe.html ├── com │ └── qozix │ │ └── widget │ │ ├── EndlessAdapter.html │ │ ├── EndlessListener.html │ │ ├── EndlessRecyclerView.OnPopulationListener.html │ │ ├── EndlessRecyclerView.Orientation.html │ │ ├── EndlessRecyclerView.html │ │ ├── package-frame.html │ │ ├── package-summary.html │ │ └── package-tree.html ├── constant-values.html ├── deprecated-list.html ├── help-doc.html ├── index-files │ ├── index-1.html │ ├── index-2.html │ ├── index-3.html │ ├── index-4.html │ ├── index-5.html │ ├── index-6.html │ ├── index-7.html │ ├── index-8.html │ └── index-9.html ├── index.html ├── overview-tree.html ├── package-list ├── resources │ ├── background.gif │ ├── tab.gif │ ├── titlebar.gif │ └── titlebar_end.gif └── stylesheet.css ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── aar-release.gradle ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── qozix │ │ └── widget │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── qozix │ │ │ └── widget │ │ │ ├── EndlessAdapter.java │ │ │ ├── EndlessListener.java │ │ │ └── EndlessRecyclerView.java │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── qozix │ └── widget │ └── ExampleUnitTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EndlessRecyclerView 2 | 3 | Endless/infinite RecyclerView with smooth scroll and fling... 4 | 5 | ![Mock Network Demo](https://cloud.githubusercontent.com/assets/701344/17218804/c0d99fe4-54ae-11e6-977f-1394c5a40f1b.gif) 6 | ![Simple Demo](https://cloud.githubusercontent.com/assets/701344/17451342/a8a4a616-5b2b-11e6-9cbd-3f68c922c28e.gif) 7 | 8 | Most open source endless `ListView` or `RecyclerView` implementations in Android have poor UX; a scroll or fling action 9 | will stop when the list reaches its current capacity, the more data is loaded - some more sophisticated 10 | implementations might then "peek" the new content, but as often as not it's simply loaded up off screen without 11 | alerting the user. 12 | 13 | A correctly configured `EndlessRecyclerView` will behave as if the content were truly endless, and allow scrolling 14 | or flinging as far as the developer instructs. 15 | 16 | ## How does it work? 17 | Specify a threshold, in pixels. This is an arbitrary value and can be dynamic - e.g., you could set the threshold 18 | to three times the height of the RecyclerView, updating the value in `onLayout`. Once a threshold is set, 19 | any scroll operation will measure the existing content against the current scroll and dimension of the recycler view - 20 | if there is not enough content to meet or exceed the threshold value off screen (generally, below), then a callback 21 | is fired with the number of items (generally rows) needed to consume that space entirely: `space to fill / 22 | average size of an item (row)`. How that callback is handled is up to you - since `RecyclerViews` and `Adapters` are 23 | generally very custom implementations, we just provide a callback on the adapter - the `fill` method, which is 24 | passed a single parameter: the quantity of items the widget thinks is needed to create enough content to reach 25 | the defined threshold. 26 | 27 | If your `RecyclerView` is displaying data from a remote server, you can us the technique shown in the demo - 28 | immediately fill the adapter's data set with placeholder values (e.g., nulls, or maybe data items with an 29 | "uninitialized" flag), `notify` the adapter, then send a network request to replace those placeholders when data becomes 30 | available (and `notify` again). 31 | 32 | If your `RecyclerView` is displaying local data, or data sets that can be constructed immediately, then that second 33 | step is unnecessary. 34 | 35 | ## Installation 36 | EndlessRecyclerView is available on jcenter. Make sure `jcenter` is in your module's build.gradle repositories 37 | (default for Android Studio). Then add the following line to your dependencies: 38 | ``` 39 | compile 'com.qozix:endlessrecyclerview:1.0' 40 | ``` 41 | 42 | ## Usage 43 | Use `EndlessRecyclerView` in place of a normal `RecyclerView`. Set your threshold using 44 | `setVerticalThreshold(int threshold)` for vertical layouts, or the horizontal version for horizontal layouts. 45 | 46 | For your Adapter, subclass `EndlessAdapter` just as you would a standard `RecyclerView.Adapter`, but with one additional 47 | method defined: `fill(int quantity)`. This method should handle adding items to your dataset when the user scrolls near 48 | the bounds defined by your threshold. 49 | 50 | ## Documentation 51 | JavaDocs are included in the repo, under the top-level `docs` directory. 52 | 53 | ## Demo 54 | There is a demo module included in the repo. The `SimpleDemoActivity` uses a very straightforward implementation, 55 | while the `MockNetworkDemoActivity` uses some of the more advanced techniques described above. -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.1.2' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.qozix.endlessrecyclerview" 9 | minSdkVersion 16 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:23.4.0' 26 | compile 'com.google.code.gson:gson:2.7' 27 | compile 'com.squareup.picasso:picasso:2.5.2' 28 | compile project(path: ':library') 29 | } 30 | -------------------------------------------------------------------------------- /demo/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/michaeldunn/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /demo/src/androidTest/java/com/qozix/endlessrecyclerview/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.qozix.endlessrecyclerview; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /demo/src/main/assets/books.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moagrius/EndlessRecyclerView/bbb26bd00419460b53094d960d563bbc028408b1/demo/src/main/assets/books.png -------------------------------------------------------------------------------- /demo/src/main/java/com/qozix/endlessrecyclerview/demo/CommonDemoEndlessAdapter.java: -------------------------------------------------------------------------------- 1 | package com.qozix.endlessrecyclerview.demo; 2 | 3 | import android.content.Context; 4 | import android.text.TextUtils; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import com.qozix.endlessrecyclerview.R; 10 | import com.qozix.endlessrecyclerview.demo.models.MediaItem; 11 | import com.qozix.widget.EndlessAdapter; 12 | import com.squareup.picasso.Picasso; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | /** 18 | * Created by michaeldunn on 8/5/16. 19 | */ 20 | public abstract class CommonDemoEndlessAdapter extends EndlessAdapter { 21 | 22 | private List mMediaItems = new ArrayList<>(); 23 | private LayoutInflater mLayoutInflater; 24 | private View.OnClickListener mOnClickListener; 25 | private int mLimit = Integer.MAX_VALUE; 26 | 27 | public CommonDemoEndlessAdapter(Context context) { 28 | mLayoutInflater = LayoutInflater.from(context); 29 | } 30 | 31 | @Override 32 | public ItemHolder onCreateViewHolder(ViewGroup parent, int viewType) { 33 | View itemView = mLayoutInflater.inflate(R.layout.endless_row, parent, false); 34 | return new ItemHolder(itemView); 35 | } 36 | 37 | @Override 38 | public void onBindViewHolder(ItemHolder holder, int position) { 39 | MediaItem mediaItem = mMediaItems.get(position); 40 | if (mediaItem == null) { 41 | holder.readyContainer.setOnClickListener(null); 42 | holder.waitingContainer.setVisibility(View.VISIBLE); 43 | holder.readyContainer.setVisibility(View.GONE); 44 | } else { 45 | holder.readyContainer.setOnClickListener(mOnClickListener); 46 | holder.waitingContainer.setVisibility(View.GONE); 47 | holder.readyContainer.setVisibility(View.VISIBLE); 48 | holder.titleTextView.setText(position + ", " + mediaItem.title); 49 | boolean isAudioOrVideo = "video".equals(mediaItem.format); 50 | holder.mediaTextView.setVisibility(isAudioOrVideo ? View.VISIBLE : View.GONE); 51 | Picasso.with(holder.itemView.getContext()).load(mediaItem.cover_url).into(holder.thumbnailImageView); 52 | holder.authorsTextView.setText(TextUtils.join(", ", mediaItem.authors)); 53 | } 54 | } 55 | 56 | public int getLimit() { 57 | return mLimit; 58 | } 59 | 60 | public void setLimit(int limit) { 61 | mLimit = limit; 62 | if (mLimit < mMediaItems.size()) { 63 | mMediaItems.subList(mLimit, mMediaItems.size()).clear(); 64 | } 65 | } 66 | 67 | @Override 68 | public int getItemCount() { 69 | return mMediaItems.size(); 70 | } 71 | 72 | public void setOnItemClickListener(View.OnClickListener listener) { 73 | mOnClickListener = listener; 74 | } 75 | 76 | public List getMediaItems() { 77 | return mMediaItems; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /demo/src/main/java/com/qozix/endlessrecyclerview/demo/ItemHolder.java: -------------------------------------------------------------------------------- 1 | package com.qozix.endlessrecyclerview.demo; 2 | 3 | /** 4 | * Created by michaeldunn on 8/5/16. 5 | */ 6 | 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.ImageView; 11 | import android.widget.TextView; 12 | 13 | import com.qozix.endlessrecyclerview.R; 14 | 15 | public class ItemHolder extends RecyclerView.ViewHolder { 16 | public ViewGroup readyContainer; 17 | public ViewGroup waitingContainer; 18 | public ImageView thumbnailImageView; 19 | public TextView mediaTextView; 20 | public TextView titleTextView; 21 | public TextView authorsTextView; 22 | 23 | public ItemHolder(View itemView) { 24 | super(itemView); 25 | readyContainer = (ViewGroup) itemView.findViewById(R.id.endless_row_ready); 26 | waitingContainer = (ViewGroup) itemView.findViewById(R.id.endless_row_waiting); 27 | thumbnailImageView = (ImageView) itemView.findViewById(R.id.imageview_endless_row_thumb); 28 | mediaTextView = (TextView) itemView.findViewById(R.id.textview_endless_row_media); 29 | titleTextView = (TextView) itemView.findViewById(R.id.textview_endless_row_title); 30 | authorsTextView = (TextView) itemView.findViewById(R.id.textview_endless_row_authors); 31 | } 32 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/qozix/endlessrecyclerview/demo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.qozix.endlessrecyclerview.demo; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.view.View; 7 | 8 | import com.qozix.endlessrecyclerview.demo.network.MockNetworkDemoActivity; 9 | import com.qozix.endlessrecyclerview.R; 10 | import com.qozix.endlessrecyclerview.demo.simple.SimpleDemoActivity; 11 | 12 | public class MainActivity extends AppCompatActivity { 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_main); 18 | } 19 | 20 | public void startSimpleDemo(View view) { 21 | Intent intent = new Intent(this, SimpleDemoActivity.class); 22 | startActivity(intent); 23 | } 24 | 25 | public void startMockNetworkDemo(View view) { 26 | Intent intent = new Intent(this, MockNetworkDemoActivity.class); 27 | startActivity(intent); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /demo/src/main/java/com/qozix/endlessrecyclerview/demo/models/JsonResponse.java: -------------------------------------------------------------------------------- 1 | package com.qozix.endlessrecyclerview.demo.models; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Created by michaeldunn on 7/21/16. 7 | */ 8 | public class JsonResponse { 9 | public int total; 10 | public int page; 11 | public List results; 12 | } 13 | -------------------------------------------------------------------------------- /demo/src/main/java/com/qozix/endlessrecyclerview/demo/models/MediaItem.java: -------------------------------------------------------------------------------- 1 | package com.qozix.endlessrecyclerview.demo.models; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Created by michaeldunn on 7/21/16. 7 | */ 8 | public class MediaItem { 9 | public String title; 10 | public String cover_url; 11 | public List authors; 12 | public String format; 13 | } 14 | -------------------------------------------------------------------------------- /demo/src/main/java/com/qozix/endlessrecyclerview/demo/network/MockClient.java: -------------------------------------------------------------------------------- 1 | package com.qozix.endlessrecyclerview.demo.network; 2 | 3 | import android.content.Context; 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | 7 | import com.google.gson.Gson; 8 | import com.qozix.endlessrecyclerview.demo.models.JsonResponse; 9 | 10 | import java.io.BufferedReader; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.InputStreamReader; 14 | import java.util.Random; 15 | 16 | /** 17 | * Created by michaeldunn on 7/21/16. 18 | */ 19 | public class MockClient { 20 | 21 | private Random mRandom = new Random(); 22 | private Handler mHandler; 23 | private Context mContext; 24 | private int mPage; 25 | 26 | public MockClient(Context context) { 27 | mContext = context; 28 | mHandler = new Handler(Looper.getMainLooper()); 29 | } 30 | 31 | private String readAssetFile(String fileName) { 32 | try { 33 | InputStream inputStream = mContext.getAssets().open(fileName); 34 | InputStreamReader inputStreamReader = new InputStreamReader(inputStream); 35 | BufferedReader bufferedReader = new BufferedReader(inputStreamReader); 36 | StringBuilder stringBuilder = new StringBuilder(); 37 | String line = bufferedReader.readLine(); 38 | while (line != null) { 39 | stringBuilder.append(line); 40 | line = bufferedReader.readLine(); 41 | } 42 | return stringBuilder.toString(); 43 | } catch (IOException e) { 44 | e.printStackTrace(); 45 | } 46 | return null; 47 | } 48 | 49 | private void increment() { 50 | mPage++; 51 | if (mPage > 10) { 52 | mPage = 1; 53 | } 54 | } 55 | 56 | private String getMockUri() { 57 | return "json/mock-search-" + mPage + ".json"; 58 | } 59 | 60 | private String getNextResult() { 61 | return readAssetFile(getMockUri()); 62 | } 63 | 64 | public void fetch(final ResponseReceivedListener responseReceivedListener) { 65 | increment(); 66 | new Thread(new Runnable() { 67 | @Override 68 | public void run() { 69 | int delay = 500 + mRandom.nextInt(700); 70 | try { 71 | Thread.sleep(delay); 72 | } catch (InterruptedException e) { 73 | e.printStackTrace(); 74 | } 75 | String json = getNextResult(); 76 | final JsonResponse jsonResponse = new Gson().fromJson(json, JsonResponse.class); 77 | mHandler.post(new Runnable() { 78 | @Override 79 | public void run() { 80 | responseReceivedListener.onResponse(jsonResponse); 81 | } 82 | }); 83 | } 84 | }).start(); 85 | } 86 | 87 | public interface ResponseReceivedListener { 88 | void onResponse(JsonResponse jsonResponse); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /demo/src/main/java/com/qozix/endlessrecyclerview/demo/network/MockNetworkDemoActivity.java: -------------------------------------------------------------------------------- 1 | package com.qozix.endlessrecyclerview.demo.network; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.support.v7.widget.LinearLayoutManager; 6 | import android.util.Log; 7 | import android.view.View; 8 | 9 | import com.qozix.endlessrecyclerview.R; 10 | import com.qozix.endlessrecyclerview.demo.MainActivity; 11 | import com.qozix.widget.EndlessRecyclerView; 12 | 13 | /** 14 | * Created by michaeldunn on 8/5/16. 15 | */ 16 | public class MockNetworkDemoActivity extends AppCompatActivity { 17 | 18 | private EndlessRecyclerView mEndlessRecyclerView; 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.endlessrecyclerview); 25 | 26 | final MockNetworkDemoEndlessAdapter mockNetworkDemoEndlessAdapter = new MockNetworkDemoEndlessAdapter(this); 27 | mockNetworkDemoEndlessAdapter.setOnItemClickListener(mOnItemClickListener); 28 | mockNetworkDemoEndlessAdapter.setLimit(1000); 29 | mockNetworkDemoEndlessAdapter.setOnFillCompleteListener(new MockNetworkDemoEndlessAdapter.OnFillCompleteListener() { 30 | @Override 31 | public void onFillComplete(boolean expectsMore) { 32 | mEndlessRecyclerView.requestLayout(); 33 | } 34 | }); 35 | 36 | mEndlessRecyclerView = (EndlessRecyclerView) findViewById(R.id.endlessrecyclerview_main); 37 | mEndlessRecyclerView.setCanExpectConsistentItemSize(true); 38 | mEndlessRecyclerView.setAdapter(mockNetworkDemoEndlessAdapter); 39 | mEndlessRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); 40 | mEndlessRecyclerView.addOnLayoutChangeListener(mOnLayoutChangeListener); 41 | 42 | } 43 | 44 | private void updateEndlessRecyclerViewThreshold() { 45 | mEndlessRecyclerView.setVerticalThreshold(mEndlessRecyclerView.getHeight() * 3); 46 | } 47 | 48 | private View.OnClickListener mOnItemClickListener = new View.OnClickListener() { 49 | @Override 50 | public void onClick(View view) { 51 | Log.d(MainActivity.class.getSimpleName(), "clicked!"); 52 | } 53 | }; 54 | 55 | private View.OnLayoutChangeListener mOnLayoutChangeListener = new View.OnLayoutChangeListener() { 56 | @Override 57 | public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { 58 | updateEndlessRecyclerViewThreshold(); 59 | } 60 | }; 61 | 62 | } 63 | -------------------------------------------------------------------------------- /demo/src/main/java/com/qozix/endlessrecyclerview/demo/network/MockNetworkDemoEndlessAdapter.java: -------------------------------------------------------------------------------- 1 | package com.qozix.endlessrecyclerview.demo.network; 2 | 3 | import android.content.Context; 4 | 5 | import com.qozix.endlessrecyclerview.demo.CommonDemoEndlessAdapter; 6 | import com.qozix.endlessrecyclerview.demo.models.JsonResponse; 7 | import com.qozix.endlessrecyclerview.demo.models.MediaItem; 8 | 9 | import java.util.LinkedList; 10 | import java.util.Queue; 11 | 12 | /** 13 | * Created by michaeldunn on 7/21/16. 14 | */ 15 | public class MockNetworkDemoEndlessAdapter extends CommonDemoEndlessAdapter { 16 | 17 | public interface OnFillCompleteListener { 18 | void onFillComplete(boolean expectsMore); 19 | } 20 | 21 | private MockClient mMockClient; 22 | private Queue mNullPositions = new LinkedList<>(); 23 | private boolean mIsFetching; 24 | private OnFillCompleteListener mOnFillCompleteListener; 25 | 26 | public MockNetworkDemoEndlessAdapter(Context context) { 27 | super(context); 28 | mMockClient = new MockClient(context); 29 | } 30 | 31 | public void setOnFillCompleteListener(OnFillCompleteListener onFillCompleteListener) { 32 | mOnFillCompleteListener = onFillCompleteListener; 33 | } 34 | 35 | private boolean canUseMoreDataFromServer() { 36 | return getMediaItems().size() < getLimit() || !mNullPositions.isEmpty(); 37 | } 38 | 39 | /** 40 | * 41 | * @param quantity 42 | */ 43 | @Override 44 | public void fill(int quantity) { 45 | for (int i = 0; i < quantity; i++) { 46 | if (getMediaItems().size() < getLimit()) { 47 | int position = getMediaItems().size(); 48 | mNullPositions.add(position); 49 | getMediaItems().add(null); 50 | notifyItemInserted(position); 51 | } 52 | } 53 | fetch(quantity); 54 | } 55 | 56 | /** 57 | * Since our API only returns 10 at a time, we can't take advantage of the quantity param and have to keep requesting 58 | * items until we've met our total debt. 59 | * 60 | * @param quantity 61 | */ 62 | public void fetch(int quantity) { 63 | if (!mIsFetching && canUseMoreDataFromServer()) { 64 | mIsFetching = true; 65 | mMockClient.fetch(mResponseReceivedListener); 66 | } 67 | } 68 | 69 | private MockClient.ResponseReceivedListener mResponseReceivedListener = new MockClient.ResponseReceivedListener() { 70 | @Override 71 | public void onResponse(JsonResponse jsonResponse) { 72 | for (MediaItem mediaItem : jsonResponse.results) { 73 | if (mNullPositions.size() > 0) { 74 | int position = mNullPositions.poll(); 75 | getMediaItems().set(position, mediaItem); 76 | notifyItemChanged(position); 77 | } else { 78 | if (getMediaItems().size() < getLimit()) { 79 | int position = getMediaItems().size(); 80 | getMediaItems().add(mediaItem); 81 | notifyItemInserted(position); 82 | } 83 | } 84 | } 85 | mIsFetching = false; 86 | // strangely, if the initial estimated item height is get height 87 | // by not providing an estimate, and explicitly disallowing computation from the adapter 88 | // the granular notification methods commented out in this method will fail to requestLayout 89 | // dispatch it here manually 90 | boolean expectAnotherFetch = mNullPositions.size() > 0; 91 | if (mOnFillCompleteListener != null) { 92 | mOnFillCompleteListener.onFillComplete(expectAnotherFetch); 93 | } 94 | if (expectAnotherFetch) { // TODO: probably should not have this here 95 | fetch(mNullPositions.size()); 96 | } 97 | } 98 | }; 99 | 100 | } 101 | -------------------------------------------------------------------------------- /demo/src/main/java/com/qozix/endlessrecyclerview/demo/simple/SimpleDemoActivity.java: -------------------------------------------------------------------------------- 1 | package com.qozix.endlessrecyclerview.demo.simple; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.support.v7.widget.LinearLayoutManager; 6 | import android.util.Log; 7 | import android.view.View; 8 | 9 | import com.qozix.endlessrecyclerview.R; 10 | import com.qozix.endlessrecyclerview.demo.MainActivity; 11 | import com.qozix.widget.EndlessRecyclerView; 12 | 13 | /** 14 | * Created by michaeldunn on 8/5/16. 15 | */ 16 | public class SimpleDemoActivity extends AppCompatActivity { 17 | 18 | private EndlessRecyclerView mEndlessRecyclerView; 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.endlessrecyclerview); 25 | 26 | SimpleDemoEndlessAdapter simpleDemoEndlessAdapter = new SimpleDemoEndlessAdapter(this); 27 | simpleDemoEndlessAdapter.setOnItemClickListener(mOnItemClickListener); 28 | simpleDemoEndlessAdapter.setLimit(5000); 29 | 30 | mEndlessRecyclerView = (EndlessRecyclerView) findViewById(R.id.endlessrecyclerview_main); 31 | mEndlessRecyclerView.setCanExpectConsistentItemSize(true); 32 | mEndlessRecyclerView.setAdapter(simpleDemoEndlessAdapter); 33 | mEndlessRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); 34 | mEndlessRecyclerView.addOnLayoutChangeListener(mOnLayoutChangeListener); 35 | 36 | } 37 | 38 | private void updateEndlessRecyclerViewThreshold() { 39 | mEndlessRecyclerView.setVerticalThreshold(mEndlessRecyclerView.getHeight() * 3); 40 | } 41 | 42 | private View.OnClickListener mOnItemClickListener = new View.OnClickListener() { 43 | @Override 44 | public void onClick(View view) { 45 | Log.d(MainActivity.class.getSimpleName(), "clicked!"); 46 | } 47 | }; 48 | 49 | private View.OnLayoutChangeListener mOnLayoutChangeListener = new View.OnLayoutChangeListener() { 50 | @Override 51 | public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { 52 | updateEndlessRecyclerViewThreshold(); 53 | } 54 | }; 55 | 56 | } 57 | -------------------------------------------------------------------------------- /demo/src/main/java/com/qozix/endlessrecyclerview/demo/simple/SimpleDemoEndlessAdapter.java: -------------------------------------------------------------------------------- 1 | package com.qozix.endlessrecyclerview.demo.simple; 2 | 3 | import android.content.Context; 4 | 5 | import com.qozix.endlessrecyclerview.demo.CommonDemoEndlessAdapter; 6 | import com.qozix.endlessrecyclerview.demo.models.MediaItem; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * Created by michaeldunn on 7/21/16. 13 | */ 14 | public class SimpleDemoEndlessAdapter extends CommonDemoEndlessAdapter { 15 | 16 | public SimpleDemoEndlessAdapter(Context context) { 17 | super(context); 18 | } 19 | 20 | @Override 21 | public void fill(int quantity) { 22 | for (int i = 0; i < quantity; i++) { 23 | if (getMediaItems().size() < getLimit()) { 24 | getMediaItems().add(getDummyMediaItem(getMediaItems().size())); // TODO 25 | notifyItemInserted(getMediaItems().size() - 1); 26 | } 27 | } 28 | } 29 | 30 | private List mDummyAuthorsList = new ArrayList<>(); 31 | { 32 | mDummyAuthorsList.add("Author A"); 33 | mDummyAuthorsList.add("Author B"); 34 | } 35 | 36 | private MediaItem getDummyMediaItem(int position){ 37 | MediaItem mediaItem = new MediaItem(); 38 | mediaItem.title = "Item #" + position; 39 | mediaItem.cover_url = "file:///android_asset/books.png"; 40 | mediaItem.authors = mDummyAuthorsList; 41 | mediaItem.format = "book"; 42 | return mediaItem; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xxxhdpi/ic_headphones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moagrius/EndlessRecyclerView/bbb26bd00419460b53094d960d563bbc028408b1/demo/src/main/res/drawable-xxxhdpi/ic_headphones.png -------------------------------------------------------------------------------- /demo/src/main/res/drawable/shape_border_thin.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |