├── .gitignore ├── README.md ├── build.gradle ├── dbexample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── tonicartos │ │ └── superslimdbexample │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── tonicartos │ │ └── superslimdbexample │ │ ├── Adapter.java │ │ ├── ContentFragment.java │ │ ├── DataProvider.java │ │ ├── DatabaseHelper.java │ │ ├── FirstRunHelper.java │ │ ├── LinearInterceptManager.java │ │ ├── MainActivity.java │ │ └── model │ │ ├── Country.java │ │ ├── DbModel.java │ │ ├── JsonData.java │ │ ├── Region.java │ │ └── Subregion.java │ └── res │ ├── layout-v17 │ ├── country_item.xml │ ├── region_header.xml │ └── subregion_header.xml │ ├── layout │ ├── activity_main.xml │ ├── country_item.xml │ ├── fragment_main.xml │ ├── region_header.xml │ └── subregion_header.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── raw │ └── init_db_data.json │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── doc ├── LayoutManager.html ├── section.png └── src │ └── LayoutManager.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── tonicartos │ │ └── superslim │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── com │ │ │ └── tonicartos │ │ │ └── superslim │ │ │ ├── adapter │ │ │ ├── FooterStyle.java │ │ │ ├── HeaderStyle.java │ │ │ ├── adapter.kt │ │ │ └── graph.kt │ │ │ ├── extensions.kt │ │ │ ├── graph.kt │ │ │ ├── internal │ │ │ ├── config_helpers.kt │ │ │ ├── extensions.kt │ │ │ ├── graph.kt │ │ │ ├── item_management.kt │ │ │ ├── layout │ │ │ │ ├── footer.kt │ │ │ │ ├── generic.kt │ │ │ │ ├── header.kt │ │ │ │ └── padding.kt │ │ │ └── layout_helpers.kt │ │ │ ├── layout.kt │ │ │ ├── layout │ │ │ ├── flexbox.kt │ │ │ ├── grid.kt │ │ │ ├── linear.kt │ │ │ └── staggered_grid.kt │ │ │ ├── layout_manager.kt │ │ │ └── section_data_mixins.kt │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ ├── kotlin │ └── com │ │ └── tonicartos │ │ └── superslim │ │ ├── grid_layout.kt │ │ ├── header_layout.kt │ │ ├── linear_layout.kt │ │ └── section.kt │ └── resources │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | #/.idea/workspace.xml 4 | #/.idea/libraries 5 | .DS_Store 6 | #.idea/vcs.xml 7 | captures 8 | 9 | .idea/ 10 | .gradle 11 | build/ 12 | *.iml 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SuperSLiM 2 | 3 | This is the version 5 development branch. 4 | 5 | [Project Plan](https://trello.com/b/g3Wctuey) 6 | [Support me on Patreon](https://www.patreon.com/Tonic) 7 | [Blog](https://branchmerge.blogspot.com) 8 | 9 | ## What is Version 5 10 | Version 5 is the current development branch for SuperSLiM. Once Milestone 3 is reached it will become the main branch for the library. Currently available from this branch are the following things: 11 | 12 | - an adapter implementation backed by a DAG 13 | - a layout manager that can layout a hierarchy of nested sections 14 | - a linear section layout 15 | - predictive animations for item changes 16 | - support for layout configurations; layout direction (LTR, RTL), reverse layout, stack from end, and horizontal and vertical layout configurations. 17 | - an example app 18 | - way too much work for what you see 19 | 20 | What does it not yet have? **Lots**. See the [project plan](https://trello.com/b/g3Wctuey) for more details, and support me on [patreon](https://www.pateron.com/Tonic). 21 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | kotlin_version = '1.1.1' 6 | build_tools_version = '25.0.2' 7 | supportlib_version = '25.3.0' 8 | anko_version = '0.9.1' 9 | compile_sdk = 25 10 | target_sdk = 25 11 | } 12 | 13 | repositories { 14 | jcenter() 15 | mavenCentral() 16 | maven { 17 | url "http://dl.bintray.com/kotlin/kotlin-eap-1.1" 18 | } 19 | } 20 | 21 | dependencies { 22 | classpath 'com.android.tools.build:gradle:2.3.0' 23 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 24 | 25 | // NOTE: Do not place your application dependencies here; they belong 26 | // in the individual module build.gradle files 27 | } 28 | } 29 | 30 | task createTestResources << { 31 | def mockMakerFile = new File("$projectDir/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker") 32 | if (System.env.MOCK_MAKER != null) { 33 | logger.info("Using MockMaker ${System.env.MOCK_MAKER}") 34 | mockMakerFile.parentFile.mkdirs() 35 | mockMakerFile.createNewFile() 36 | mockMakerFile.write(System.env.MOCK_MAKER) 37 | } else { 38 | logger.info("Using default MockMaker") 39 | } 40 | } 41 | 42 | allprojects { 43 | repositories { 44 | jcenter() 45 | mavenCentral() 46 | maven { 47 | url "http://dl.bintray.com/kotlin/kotlin-eap-1.1" 48 | } 49 | } 50 | } 51 | 52 | task clean(type: Delete) { 53 | delete rootProject.buildDir 54 | } -------------------------------------------------------------------------------- /dbexample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /dbexample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion compile_sdk 5 | buildToolsVersion build_tools_version 6 | 7 | defaultConfig { 8 | applicationId "com.tonicartos.superslimdbexample" 9 | minSdkVersion 11 10 | targetSdkVersion target_sdk 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 | 25 | compile 'com.squareup.moshi:moshi:1.1.0' 26 | compile 'com.squareup.okio:okio:1.6.0' 27 | 28 | compile "com.android.support:recyclerview-v7:$supportlib_version" 29 | compile "com.android.support:appcompat-v7:$supportlib_version" 30 | compile "com.android.support:design:$supportlib_version" 31 | compile project(':library') 32 | } 33 | -------------------------------------------------------------------------------- /dbexample/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 /home/tonic/.local/share/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 mName to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /dbexample/src/androidTest/java/com/tonicartos/superslimdbexample/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslimdbexample; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | 11 | public ApplicationTest() { 12 | super(Application.class); 13 | } 14 | } -------------------------------------------------------------------------------- /dbexample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /dbexample/src/main/java/com/tonicartos/superslimdbexample/Adapter.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslimdbexample; 2 | 3 | import com.tonicartos.superslim.SectionConfig; 4 | import com.tonicartos.superslim.adapter.Item; 5 | import com.tonicartos.superslim.adapter.Section; 6 | import com.tonicartos.superslim.adapter.SuperSlimAdapter; 7 | import com.tonicartos.superslim.layout.LinearSectionConfig; 8 | 9 | import android.database.Cursor; 10 | import android.support.annotation.NonNull; 11 | import android.support.design.widget.Snackbar; 12 | import android.support.v7.widget.RecyclerView; 13 | import android.text.TextUtils; 14 | import android.view.LayoutInflater; 15 | import android.view.View; 16 | import android.view.ViewGroup; 17 | import android.widget.TextView; 18 | 19 | class Adapter extends SuperSlimAdapter { 20 | 21 | private final static int VIEW_TYPE_REGION_HEADER = 1; 22 | 23 | private final static int VIEW_TYPE_SUBREGION_HEADER = 2; 24 | 25 | private final static int VIEW_TYPE_COUNTRY_VIEW = 3; 26 | 27 | private final static int VIEW_TYPE_REGION_FOOTER = 4; 28 | 29 | private final static int VIEW_TYPE_SUBREGION_FOOTER = 5; 30 | 31 | private Cursor mCursor; 32 | 33 | Adapter() { 34 | super(); 35 | } 36 | 37 | @Override 38 | public void onBindViewHolder(@NonNull ViewHolder holder, @NonNull Item item) { 39 | holder.bind(item); 40 | } 41 | 42 | @Override 43 | public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { 44 | return ViewHolder.make(this, viewGroup, viewType); 45 | } 46 | 47 | void swapCursor(Cursor data) { 48 | mCursor = data; 49 | if (mCursor == null) { 50 | reset(); 51 | return; 52 | } 53 | 54 | updateGraph(); 55 | } 56 | 57 | private void reset() { 58 | } 59 | 60 | private void updateGraph() { 61 | final int count = mCursor.getCount(); 62 | mCursor.moveToFirst(); 63 | for (int i = 0; i < count; ) { 64 | final String currentRegionName = mCursor.getString(1); 65 | final Item currentRegion = new Item(VIEW_TYPE_REGION_HEADER, currentRegionName); 66 | final Section regionSection = createSection(currentRegionName, 67 | new LinearSectionConfig(SectionConfig.DEFAULT_GUTTER, SectionConfig.DEFAULT_GUTTER, 68 | SectionConfig.HEADER_INLINE, SectionConfig.FOOTER_STICKY), 69 | currentRegion); 70 | 71 | while (i < count) { 72 | final String region = mCursor.getString(1); 73 | if (!TextUtils.equals(currentRegionName, region)) { 74 | break; 75 | } 76 | 77 | final String currentSubRegionName = mCursor.getString(4); 78 | final Item currentSubRegion = new Item(VIEW_TYPE_SUBREGION_HEADER, currentSubRegionName); 79 | final Section subregionSection = createSection(currentSubRegionName, 80 | new LinearSectionConfig(SectionConfig.DEFAULT_GUTTER, SectionConfig.DEFAULT_GUTTER, 81 | SectionConfig.HEADER_INLINE, SectionConfig.FOOTER_STICKY), 82 | currentSubRegion); 83 | regionSection.add(subregionSection); 84 | for (; i < count; i++) { 85 | final String subRegion = mCursor.getString(4); 86 | if (!TextUtils.equals(currentSubRegionName, subRegion)) { 87 | break; 88 | } 89 | 90 | subregionSection.add(new Item(VIEW_TYPE_COUNTRY_VIEW, mCursor.getString(7))); 91 | 92 | mCursor.moveToNext(); 93 | } 94 | subregionSection.setFooter( 95 | new Item(VIEW_TYPE_SUBREGION_FOOTER, subregionSection.getChildCount() + " countries")); 96 | } 97 | regionSection.setFooter( 98 | new Item(VIEW_TYPE_REGION_FOOTER, regionSection.getChildCount() + " sub-regions")); 99 | addSection(regionSection); 100 | } 101 | } 102 | 103 | static class ViewHolder extends RecyclerView.ViewHolder { 104 | 105 | final TextView mTextView; 106 | 107 | final Adapter mAdapter; 108 | 109 | protected String name; 110 | 111 | ViewHolder(View itemView, Adapter adapter) { 112 | super(itemView); 113 | mTextView = (TextView) itemView.findViewById(R.id.text); 114 | mAdapter = adapter; 115 | } 116 | 117 | static ViewHolder make(Adapter adapter, ViewGroup viewGroup, int viewType) { 118 | LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); 119 | switch (viewType) { 120 | case VIEW_TYPE_COUNTRY_VIEW: 121 | return new CountryViewHolder( 122 | inflater.inflate(R.layout.country_item, viewGroup, false), adapter); 123 | case VIEW_TYPE_REGION_HEADER: 124 | return new RegionViewHolder( 125 | inflater.inflate(R.layout.region_header, viewGroup, false), adapter); 126 | case VIEW_TYPE_SUBREGION_HEADER: 127 | return new SubregionViewHolder( 128 | inflater.inflate(R.layout.subregion_header, viewGroup, false), adapter); 129 | case VIEW_TYPE_REGION_FOOTER: 130 | return new RegionViewHolder( 131 | inflater.inflate(R.layout.region_header, viewGroup, false), adapter); 132 | case VIEW_TYPE_SUBREGION_FOOTER: 133 | return new RegionViewHolder( 134 | inflater.inflate(R.layout.subregion_header, viewGroup, false), adapter); 135 | default: 136 | throw new RuntimeException("Unknown view type " + viewType); 137 | 138 | } 139 | } 140 | 141 | void bind(Item item) { 142 | Object data = item.getData(); 143 | assert data != null; 144 | name = data.toString(); 145 | mTextView.setText(name); 146 | } 147 | } 148 | 149 | private static class ClickableHolder extends ViewHolder implements View.OnClickListener { 150 | 151 | ClickableHolder(View view, Adapter adapter) { 152 | super(view, adapter); 153 | view.setOnClickListener(this); 154 | } 155 | 156 | @Override 157 | public void onClick(View v) { 158 | Snackbar.make(itemView, 159 | "Click on item " + getAdapterPosition() + " with text " + mTextView.getText(), 160 | Snackbar.LENGTH_SHORT).show(); 161 | } 162 | } 163 | 164 | private static class RegionViewHolder extends ClickableHolder { 165 | 166 | RegionViewHolder(View itemView, Adapter adapter) { 167 | super(itemView, adapter); 168 | } 169 | 170 | @Override 171 | public void onClick(View v) { 172 | final Section section = mAdapter.getSectionWithId(name); 173 | if (section != null) { 174 | section.toggleChildren(); 175 | // Snackbar.make(itemView, 176 | // (section.getCollapsed() ? "Collapsed region " : "Expanded region ") + mTextView 177 | // .getText(), 178 | // Snackbar.LENGTH_SHORT).show(); 179 | } 180 | } 181 | } 182 | 183 | private static class SubregionViewHolder extends ClickableHolder { 184 | 185 | SubregionViewHolder(View itemView, Adapter adapter) { 186 | super(itemView, adapter); 187 | } 188 | 189 | @Override 190 | public void onClick(View v) { 191 | Section section = mAdapter.getSectionWithId(name); 192 | if (section != null) { 193 | section.toggleChildren(); 194 | // Snackbar.make(itemView, 195 | // (section.getCollapsed() ? "Collapsed subregion " : "Expanded subregion ") 196 | // + mTextView.getText(), 197 | // Snackbar.LENGTH_SHORT).show(); 198 | } 199 | } 200 | } 201 | 202 | private static class CountryViewHolder extends ClickableHolder { 203 | 204 | CountryViewHolder(View itemView, Adapter adapter) { 205 | super(itemView, adapter); 206 | } 207 | 208 | @Override 209 | public void onClick(View v) { 210 | Snackbar.make(itemView, "Clicked on country " + mTextView.getText(), 211 | Snackbar.LENGTH_SHORT) 212 | .show(); 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /dbexample/src/main/java/com/tonicartos/superslimdbexample/ContentFragment.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslimdbexample; 2 | 3 | import com.tonicartos.superslim.adapter.Section; 4 | 5 | import android.database.Cursor; 6 | import android.net.Uri; 7 | import android.os.Bundle; 8 | import android.support.v4.app.Fragment; 9 | import android.support.v4.app.LoaderManager; 10 | import android.support.v4.content.CursorLoader; 11 | import android.support.v4.content.Loader; 12 | import android.support.v7.widget.RecyclerView; 13 | import android.view.LayoutInflater; 14 | import android.view.Menu; 15 | import android.view.MenuInflater; 16 | import android.view.MenuItem; 17 | import android.view.View; 18 | import android.view.ViewGroup; 19 | 20 | /** 21 | * A placeholder fragment containing a simple view. 22 | */ 23 | public class ContentFragment extends Fragment { 24 | 25 | private static final int LOADER = 0; 26 | 27 | private static final Uri URI = Uri.parse("content://com.tonicartos.superslimdbexample.provider/all"); 28 | 29 | Adapter mAdapter; 30 | 31 | private LoaderManager.LoaderCallbacks mLoaderCallbacks 32 | = new LoaderManager.LoaderCallbacks() { 33 | 34 | @Override 35 | public Loader onCreateLoader(int id, Bundle args) { 36 | return new CursorLoader(getActivity(), URI, null, null, null, null); 37 | } 38 | 39 | @Override 40 | public void onLoadFinished(Loader loader, Cursor data) { 41 | mAdapter.swapCursor(data); 42 | // recyclerView.scrollToPosition(1); 43 | } 44 | 45 | @Override 46 | public void onLoaderReset(Loader loader) { 47 | mAdapter.swapCursor(null); 48 | } 49 | }; 50 | 51 | private Section removedSection = null; 52 | 53 | private RecyclerView recyclerView; 54 | 55 | public ContentFragment() { 56 | } 57 | 58 | @Override 59 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 60 | Bundle savedInstanceState) { 61 | return inflater.inflate(R.layout.fragment_main, container, false); 62 | } 63 | 64 | @Override 65 | public void onViewCreated(View view, Bundle savedInstanceState) { 66 | super.onViewCreated(view, savedInstanceState); 67 | 68 | recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); 69 | getLoaderManager().restartLoader(LOADER, null, mLoaderCallbacks); 70 | 71 | setHasOptionsMenu(true); 72 | 73 | mAdapter = new Adapter(); 74 | recyclerView.setAdapter(mAdapter); 75 | } 76 | 77 | @Override 78 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 79 | menu.add("sadf"); 80 | } 81 | 82 | @Override 83 | public boolean onOptionsItemSelected(MenuItem item) { 84 | // if (removedSection == null) { 85 | // removedSection = mAdapter.removeSection(1); 86 | // } else { 87 | // mAdapter.insertSection(1, removedSection); 88 | // removedSection = null; 89 | // } 90 | mAdapter.moveSection(0, 1); 91 | return false; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /dbexample/src/main/java/com/tonicartos/superslimdbexample/DataProvider.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslimdbexample; 2 | 3 | import com.tonicartos.superslimdbexample.model.Country; 4 | import com.tonicartos.superslimdbexample.model.Region; 5 | import com.tonicartos.superslimdbexample.model.Subregion; 6 | 7 | import android.content.ContentProvider; 8 | import android.content.ContentValues; 9 | import android.content.UriMatcher; 10 | import android.database.Cursor; 11 | import android.database.sqlite.SQLiteDatabase; 12 | import android.database.sqlite.SQLiteQueryBuilder; 13 | import android.net.Uri; 14 | import android.text.TextUtils; 15 | 16 | /** 17 | * 18 | */ 19 | public class DataProvider extends ContentProvider { 20 | 21 | private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 22 | 23 | private static final String AUTHORITY = "com.tonicartos.superslimdbexample.provider"; 24 | 25 | private static final int BIT_SINGLE = 1; 26 | 27 | private static final int BIT_REGION = 2; 28 | 29 | private static final int BIT_SUB_REGION = 4; 30 | 31 | private static final int BIT_COUNTRY = 8; 32 | 33 | private static final int BIT_ALL = 16; 34 | 35 | private DatabaseHelper mDbHelper; 36 | 37 | @Override 38 | public boolean onCreate() { 39 | mDbHelper = new DatabaseHelper(getContext()); 40 | 41 | return true; 42 | } 43 | 44 | @Override 45 | public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 46 | String sortOrder) { 47 | SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 48 | 49 | final int bits = sUriMatcher.match(uri); 50 | if ((bits & BIT_SINGLE) == BIT_SINGLE) { 51 | selection = addIdToSelection(selection); 52 | selectionArgs = addUriToSelectionArgs(uri, selectionArgs); 53 | } else { 54 | if (TextUtils.isEmpty(sortOrder)) { 55 | sortOrder = "_id ASC"; 56 | } 57 | } 58 | 59 | final String table; 60 | switch (bits & ~BIT_SINGLE) { 61 | case BIT_COUNTRY: 62 | table = Country.TABLE_NAME; 63 | break; 64 | case BIT_REGION: 65 | table = Region.TABLE_NAME; 66 | break; 67 | case BIT_SUB_REGION: 68 | table = Subregion.TABLE_NAME; 69 | break; 70 | case BIT_ALL: 71 | table = Region.TABLE_NAME + " r LEFT OUTER JOIN " + Subregion.TABLE_NAME 72 | + " s ON r._id = s.region_id LEFT OUTER JOIN " + Country.TABLE_NAME 73 | + " c ON s._id = c.sub_region_id"; 74 | break; 75 | default: 76 | throw new IllegalArgumentException("Unknown URI: " + uri); 77 | } 78 | 79 | SQLiteDatabase db = mDbHelper.getReadableDatabase(); 80 | Cursor cursor = db.query(table, projection, selection, selectionArgs, null, null, 81 | sortOrder); 82 | cursor.setNotificationUri(getContext().getContentResolver(), uri); 83 | 84 | return cursor; 85 | } 86 | 87 | @Override 88 | public String getType(Uri uri) { 89 | final String firstPart; 90 | final int bits = sUriMatcher.match(uri); 91 | if ((bits & BIT_SINGLE) == BIT_SINGLE) { 92 | firstPart = "vnd.android.cursor.item/vnd.com.tonicartos.superslimdbexample.db."; 93 | } else { 94 | firstPart = "vnd.android.cursor.dir/vnd.com.tonicartos.superslimdbexample.db."; 95 | } 96 | 97 | final String secondPart; 98 | switch (bits & ~BIT_SINGLE) { 99 | case BIT_COUNTRY: 100 | secondPart = "country"; 101 | break; 102 | case BIT_SUB_REGION: 103 | secondPart = "region"; 104 | break; 105 | case BIT_REGION: 106 | secondPart = "sub_region"; 107 | break; 108 | case BIT_ALL: 109 | secondPart = "all"; 110 | break; 111 | default: 112 | throw new IllegalArgumentException("Unknown URI: " + uri); 113 | } 114 | return firstPart + secondPart; 115 | } 116 | 117 | @Override 118 | public Uri insert(Uri uri, ContentValues values) { 119 | SQLiteDatabase db = mDbHelper.getWritableDatabase(); 120 | final int bits = sUriMatcher.match(uri); 121 | 122 | final String table; 123 | switch (bits & ~BIT_SINGLE) { 124 | case BIT_COUNTRY: 125 | table = Country.TABLE_NAME; 126 | break; 127 | case BIT_REGION: 128 | table = Region.TABLE_NAME; 129 | break; 130 | case BIT_SUB_REGION: 131 | table = Subregion.TABLE_NAME; 132 | break; 133 | default: 134 | throw new IllegalArgumentException("Unknown URI: " + uri); 135 | } 136 | 137 | final long id = db.insert(table, null, values); 138 | 139 | if (id == -1) { 140 | return null; 141 | } 142 | 143 | getContext().getContentResolver().notifyChange(uri, null); 144 | getContext().getContentResolver() 145 | .notifyChange(Uri.parse("content://com.tonicartos.superslimdbexample.provider/all"), 146 | null); 147 | return Uri.parse(uri.getPath() + "/" + id); 148 | } 149 | 150 | @Override 151 | public int delete(Uri uri, String selection, String[] selectionArgs) { 152 | final int rowsChanged; 153 | 154 | final int bits = sUriMatcher.match(uri); 155 | if ((bits & BIT_SINGLE) == BIT_SINGLE) { 156 | selection = addIdToSelection(selection); 157 | selectionArgs = addUriToSelectionArgs(uri, selectionArgs); 158 | } 159 | 160 | final String table; 161 | switch (bits & ~BIT_SINGLE) { 162 | case BIT_COUNTRY: 163 | table = Country.TABLE_NAME; 164 | break; 165 | case BIT_SUB_REGION: 166 | table = Subregion.TABLE_NAME; 167 | break; 168 | case BIT_REGION: 169 | table = Region.TABLE_NAME; 170 | break; 171 | default: 172 | throw new IllegalArgumentException("Unknown URI: " + uri); 173 | } 174 | 175 | SQLiteDatabase db = mDbHelper.getWritableDatabase(); 176 | rowsChanged = db.delete(table, selection, selectionArgs); 177 | 178 | if (rowsChanged > 0) { 179 | getContext().getContentResolver().notifyChange(uri, null); 180 | getContext().getContentResolver().notifyChange( 181 | Uri.parse("content://com.tonicartos.superslimdbexample.provider/all"), null); 182 | } 183 | return rowsChanged; 184 | } 185 | 186 | @Override 187 | public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 188 | final int rowsChanged; 189 | 190 | final int bits = sUriMatcher.match(uri); 191 | if ((bits & BIT_SINGLE) == BIT_SINGLE) { 192 | selection = addIdToSelection(selection); 193 | selectionArgs = addUriToSelectionArgs(uri, selectionArgs); 194 | } 195 | 196 | final String table; 197 | switch (bits & ~BIT_SINGLE) { 198 | case BIT_COUNTRY: 199 | table = Country.TABLE_NAME; 200 | break; 201 | case BIT_SUB_REGION: 202 | table = Subregion.TABLE_NAME; 203 | break; 204 | case BIT_REGION: 205 | table = Region.TABLE_NAME; 206 | break; 207 | default: 208 | throw new IllegalArgumentException("Unknown URI: " + uri); 209 | } 210 | 211 | SQLiteDatabase db = mDbHelper.getWritableDatabase(); 212 | rowsChanged = db.update(table, values, selection, selectionArgs); 213 | 214 | if (rowsChanged > 0) { 215 | getContext().getContentResolver().notifyChange(uri, null); 216 | getContext().getContentResolver().notifyChange( 217 | Uri.parse("content://com.tonicartos.superslimdbexample.provider/all"), null); 218 | } 219 | 220 | return rowsChanged; 221 | } 222 | 223 | private String addIdToSelection(String selection) { 224 | if (TextUtils.isEmpty(selection)) { 225 | selection = "_ID = ?"; 226 | } else { 227 | selection = "_ID = ? AND " + selection; 228 | } 229 | return selection; 230 | } 231 | 232 | private String[] addUriToSelectionArgs(Uri uri, String[] selectionArgs) { 233 | if (selectionArgs != null && selectionArgs.length > 0) { 234 | final String[] newSelectionArgs = new String[selectionArgs.length + 1]; 235 | System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); 236 | newSelectionArgs[0] = uri.getLastPathSegment(); 237 | selectionArgs = newSelectionArgs; 238 | } else { 239 | selectionArgs = new String[]{uri.getLastPathSegment()}; 240 | } 241 | return selectionArgs; 242 | } 243 | 244 | static { 245 | sUriMatcher.addURI(AUTHORITY, "all", BIT_ALL); 246 | sUriMatcher.addURI(AUTHORITY, Region.TABLE_NAME, BIT_REGION); 247 | sUriMatcher.addURI(AUTHORITY, Region.TABLE_NAME + "/#", BIT_REGION | BIT_SINGLE); 248 | sUriMatcher.addURI(AUTHORITY, Subregion.TABLE_NAME, BIT_SUB_REGION); 249 | sUriMatcher.addURI(AUTHORITY, Subregion.TABLE_NAME + "/#", BIT_SUB_REGION | BIT_SINGLE); 250 | sUriMatcher.addURI(AUTHORITY, Country.TABLE_NAME, BIT_COUNTRY); 251 | sUriMatcher.addURI(AUTHORITY, Country.TABLE_NAME + "/#", BIT_COUNTRY | BIT_SINGLE); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /dbexample/src/main/java/com/tonicartos/superslimdbexample/DatabaseHelper.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslimdbexample; 2 | 3 | import com.tonicartos.superslimdbexample.model.Subregion; 4 | import com.tonicartos.superslimdbexample.model.Country; 5 | import com.tonicartos.superslimdbexample.model.Region; 6 | 7 | import android.content.Context; 8 | import android.database.sqlite.SQLiteDatabase; 9 | import android.database.sqlite.SQLiteOpenHelper; 10 | 11 | /** 12 | * 13 | */ 14 | public class DatabaseHelper extends SQLiteOpenHelper { 15 | 16 | private static final String DATABASE_NAME = "countries"; 17 | 18 | private static final int DATABASE_VERSION = 1; 19 | 20 | private final Context mContext; 21 | 22 | public DatabaseHelper(Context context) { 23 | super(context, DATABASE_NAME, null, DATABASE_VERSION); 24 | mContext = context; 25 | } 26 | 27 | @Override 28 | public void onCreate(SQLiteDatabase db) { 29 | db.execSQL(Country.CREATE_TABLE); 30 | db.execSQL(Region.CREATE_TABLE); 31 | db.execSQL(Subregion.CREATE_TABLE); 32 | 33 | if (!FirstRunHelper.isInitialised(mContext)) { 34 | FirstRunHelper.setIsInitialised( 35 | FirstRunHelper.performInitialisation(db, mContext), mContext); 36 | } 37 | } 38 | 39 | @Override 40 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /dbexample/src/main/java/com/tonicartos/superslimdbexample/FirstRunHelper.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslimdbexample; 2 | 3 | import com.squareup.moshi.JsonAdapter; 4 | import com.squareup.moshi.Moshi; 5 | import com.tonicartos.superslimdbexample.model.Country; 6 | import com.tonicartos.superslimdbexample.model.JsonData; 7 | import com.tonicartos.superslimdbexample.model.Region; 8 | import com.tonicartos.superslimdbexample.model.Subregion; 9 | 10 | import android.content.Context; 11 | import android.content.SharedPreferences; 12 | import android.database.sqlite.SQLiteDatabase; 13 | import android.util.Log; 14 | 15 | import java.io.IOException; 16 | 17 | import okio.Okio; 18 | 19 | /** 20 | * A helper to setup the application on the first run. 21 | */ 22 | public class FirstRunHelper { 23 | 24 | private static final String TAG = "First Run Helper"; 25 | 26 | private static final String PREFS = "cac"; 27 | 28 | private static final String PREF_FIRST_RUN = "is_first_run"; 29 | 30 | private static final String PREF_INITIALISED = "initialised"; 31 | 32 | /** 33 | * Default mode for accessing shared preferences. 34 | */ 35 | private static final int MODE = Context.MODE_PRIVATE; 36 | 37 | public static boolean isFirstRun(Context context) { 38 | SharedPreferences prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE); 39 | return prefs.getBoolean(PREF_FIRST_RUN, true); 40 | } 41 | 42 | public static boolean isInitialised(Context context) { 43 | SharedPreferences prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE); 44 | return prefs.getBoolean(PREF_INITIALISED, false); 45 | } 46 | 47 | public static boolean performInitialisation(SQLiteDatabase db, Context context) { 48 | Moshi moshi = new Moshi.Builder().build(); 49 | JsonAdapter jsonAdapter = moshi.adapter(JsonData.class); 50 | JsonData jsonData; 51 | try { 52 | jsonData = jsonAdapter.fromJson(Okio.buffer( 53 | Okio.source(context.getResources().openRawResource(R.raw.init_db_data)))); 54 | } catch (IOException e) { 55 | String msg = "Error loading initial database data file."; 56 | Log.e(TAG, msg); 57 | e.printStackTrace(); 58 | return false; 59 | } 60 | 61 | db.beginTransaction(); 62 | try { 63 | for (JsonData.Region r : jsonData.regions) { 64 | Region region = Region.from(r); 65 | region.createRecord(db); 66 | 67 | for (JsonData.SubRegion s : r.sub_regions) { 68 | Subregion subregion = Subregion.from(s); 69 | subregion.setRegion(region); 70 | subregion.createRecord(db); 71 | 72 | for (JsonData.Country c : s.countries) { 73 | Country country = Country.from(c); 74 | country.setSubRegion(subregion); 75 | country.createRecord(db); 76 | } 77 | } 78 | } 79 | 80 | setIsInitialised(true, context); 81 | db.setTransactionSuccessful(); 82 | } finally { 83 | db.endTransaction(); 84 | } 85 | 86 | return true; 87 | } 88 | 89 | public static void setFirstRunComplete(boolean value, Context context) { 90 | SharedPreferences.Editor prefsEditor = context.getSharedPreferences(PREFS, MODE).edit(); 91 | prefsEditor.putBoolean(PREF_FIRST_RUN, false); 92 | prefsEditor.commit(); 93 | } 94 | 95 | public static void setIsInitialised(boolean value, Context context) { 96 | SharedPreferences.Editor prefsEditor = context.getSharedPreferences(PREFS, MODE).edit(); 97 | prefsEditor.putBoolean(PREF_INITIALISED, false); 98 | prefsEditor.commit(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /dbexample/src/main/java/com/tonicartos/superslimdbexample/LinearInterceptManager.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslimdbexample; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.LinearLayoutManager; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.util.AttributeSet; 7 | import android.util.Log; 8 | 9 | /** 10 | * 11 | */ 12 | public class LinearInterceptManager extends LinearLayoutManager { 13 | 14 | public LinearInterceptManager(Context context) { 15 | super(context); 16 | } 17 | 18 | public LinearInterceptManager(Context context, int orientation, boolean reverseLayout) { 19 | super(context, orientation, reverseLayout); 20 | } 21 | 22 | public LinearInterceptManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 23 | super(context, attrs, defStyleAttr, defStyleRes); 24 | } 25 | 26 | @Override 27 | public void onItemsChanged(RecyclerView recyclerView) { 28 | super.onItemsChanged(recyclerView); 29 | } 30 | 31 | @Override 32 | public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 33 | // if (state.isPreLayout()) { 34 | // Log.d("asdf", "Pre-layout children"); 35 | // } else { 36 | // Log.d("asdf", "Post-layout children"); 37 | // } 38 | super.onLayoutChildren(recycler, state); 39 | } 40 | 41 | @Override 42 | public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 43 | // Log.d("asdf", "Items added - positionStart: " + positionStart + " itemCount: " + itemCount); 44 | super.onItemsAdded(recyclerView, positionStart, itemCount); 45 | } 46 | 47 | @Override 48 | public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { 49 | // Log.d("asdf", "Items removed - positionStart: " + positionStart + " itemCount: " + itemCount); 50 | super.onItemsRemoved(recyclerView, positionStart, itemCount); 51 | } 52 | 53 | @Override 54 | public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) { 55 | // Log.d("asdf", "Items updated - positionStart: " + positionStart + " itemCount: " + itemCount); 56 | super.onItemsUpdated(recyclerView, positionStart, itemCount); 57 | } 58 | 59 | @Override 60 | public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) { 61 | // Log.d("asdf", "Items moved - from: " + from + " to:" + to + " itemCount: " + itemCount); 62 | super.onItemsMoved(recyclerView, from, to, itemCount); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /dbexample/src/main/java/com/tonicartos/superslimdbexample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslimdbexample; 2 | 3 | import android.os.Bundle; 4 | import android.support.design.widget.AppBarLayout; 5 | import android.support.design.widget.CoordinatorLayout; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.support.v7.widget.Toolbar; 8 | import android.view.Menu; 9 | import android.view.MenuItem; 10 | import android.view.MotionEvent; 11 | 12 | 13 | public class MainActivity extends AppCompatActivity { 14 | 15 | @Override 16 | public boolean onCreateOptionsMenu(Menu menu) { 17 | // Inflate the menu; this adds items to the action bar if it is present. 18 | getMenuInflater().inflate(R.menu.menu_main, menu); 19 | return true; 20 | } 21 | 22 | @Override 23 | public boolean onOptionsItemSelected(MenuItem item) { 24 | // Handle action bar item clicks here. The action bar will 25 | // automatically handle clicks on the Home/Up button, so long 26 | // as you specify a parent activity in AndroidManifest.xml. 27 | int id = item.getItemId(); 28 | 29 | //noinspection SimplifiableIfStatement 30 | if (id == R.id.action_settings) { 31 | return true; 32 | } 33 | 34 | return super.onOptionsItemSelected(item); 35 | } 36 | 37 | @Override 38 | protected void onCreate(Bundle savedInstanceState) { 39 | super.onCreate(savedInstanceState); 40 | setContentView(R.layout.activity_main); 41 | setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); 42 | 43 | // FIXME: Remove hack around bug in coordinator layout 23.0.0 when fix is released. 44 | ((CoordinatorLayout.LayoutParams) findViewById(R.id.appbar).getLayoutParams()) 45 | .setBehavior(new AppBarLayoutBehavior()); 46 | } 47 | 48 | public class AppBarLayoutBehavior extends AppBarLayout.Behavior { 49 | 50 | @Override 51 | public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, 52 | MotionEvent ev) { 53 | return !(parent != null && child != null && ev != null) || super 54 | .onInterceptTouchEvent(parent, child, ev); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /dbexample/src/main/java/com/tonicartos/superslimdbexample/model/Country.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslimdbexample.model; 2 | 3 | import android.content.ContentValues; 4 | 5 | /** 6 | * Country database model. 7 | */ 8 | public class Country extends DbModel { 9 | 10 | private static final String KEY_SUB_REGION_ID = "sub_region_id"; 11 | 12 | private static final String KEY_NAME = "name"; 13 | 14 | public static final String TABLE_NAME = "country"; 15 | 16 | public static final java.lang.String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + 17 | KEY_ID + " integer primary key, " + 18 | KEY_SUB_REGION_ID + " integer, " + 19 | KEY_NAME + " text " + 20 | ");"; 21 | 22 | protected long mSubRegionId; 23 | 24 | protected String mName; 25 | 26 | public static Country from(JsonData.Country source) { 27 | Country c = new Country(); 28 | c.mName = source.name; 29 | return c; 30 | } 31 | 32 | public static Country from(ContentValues values) { 33 | Country c = new Country(); 34 | 35 | if (values.containsKey(KEY_ID)) { 36 | c.mId = values.getAsLong(KEY_ID); 37 | } 38 | 39 | if (values.containsKey(KEY_NAME)) { 40 | c.mName = values.getAsString(KEY_NAME); 41 | } 42 | 43 | if (values.containsKey(KEY_SUB_REGION_ID)) { 44 | c.mSubRegionId = values.getAsLong(KEY_SUB_REGION_ID); 45 | } 46 | 47 | return c; 48 | } 49 | 50 | public String getName() { 51 | return mName; 52 | } 53 | 54 | public void setName(String name) { 55 | mName = name; 56 | markAsDirty(); 57 | } 58 | 59 | public long getSubRegionId() { 60 | return mSubRegionId; 61 | } 62 | 63 | public void setSubRegionId(long subRegionId) { 64 | mSubRegionId = subRegionId; 65 | markAsDirty(); 66 | } 67 | 68 | @Override 69 | public ContentValues getValues() { 70 | ContentValues values = new ContentValues(); 71 | values.put(KEY_SUB_REGION_ID, mSubRegionId); 72 | values.put(KEY_NAME, mName); 73 | return values; 74 | } 75 | 76 | @Override 77 | protected String getTableName() { 78 | return TABLE_NAME; 79 | } 80 | 81 | public void setSubRegion(Subregion subregion) { 82 | mSubRegionId = subregion.getId(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /dbexample/src/main/java/com/tonicartos/superslimdbexample/model/DbModel.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslimdbexample.model; 2 | 3 | import android.content.ContentValues; 4 | import android.database.sqlite.SQLiteDatabase; 5 | 6 | /** 7 | * 8 | */ 9 | public abstract class DbModel { 10 | 11 | protected static final String KEY_ID = "_id"; 12 | 13 | protected long mId = -1; 14 | 15 | /** 16 | * Flag to tell if the object matches the database version. 17 | */ 18 | private boolean mIsDirty = true; 19 | 20 | public void createRecord(SQLiteDatabase db) { 21 | mId = db.insert(getTableName(), null, getValues()); 22 | } 23 | 24 | public void deleteRecord(SQLiteDatabase db) { 25 | db.delete(getTableName(), "WHERE _id = " + mId, null); 26 | } 27 | 28 | public long getId() { 29 | return mId; 30 | } 31 | 32 | public abstract ContentValues getValues(); 33 | 34 | public boolean isDirty() { 35 | return mIsDirty; 36 | } 37 | 38 | public void markAsClean() { 39 | mIsDirty = false; 40 | } 41 | 42 | public void markAsDirty() { 43 | mIsDirty = true; 44 | } 45 | 46 | public void updateRecord(SQLiteDatabase db) { 47 | if (mIsDirty) { 48 | db.update(getTableName(), getValues(), "WHERE _id = " + mId, null); 49 | } 50 | } 51 | 52 | protected abstract String getTableName(); 53 | } 54 | -------------------------------------------------------------------------------- /dbexample/src/main/java/com/tonicartos/superslimdbexample/model/JsonData.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslimdbexample.model; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Quick model for json data to initialise the database with. 7 | */ 8 | public class JsonData { 9 | 10 | public List regions; 11 | 12 | public static class Region { 13 | 14 | public String name; 15 | 16 | public List sub_regions; 17 | } 18 | 19 | public static class SubRegion { 20 | 21 | public String name; 22 | 23 | public List countries; 24 | } 25 | 26 | public static class Country { 27 | 28 | public String name; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /dbexample/src/main/java/com/tonicartos/superslimdbexample/model/Region.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslimdbexample.model; 2 | 3 | import android.content.ContentValues; 4 | 5 | /** 6 | * Region db model 7 | */ 8 | public class Region extends DbModel { 9 | 10 | private static final String KEY_NAME = "name"; 11 | 12 | public static final String TABLE_NAME = "region"; 13 | 14 | public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "( " + 15 | KEY_ID + " integer primary key, " + 16 | KEY_NAME + " text " + 17 | ");"; 18 | 19 | protected String mName; 20 | 21 | public static Region from(JsonData.Region source) { 22 | Region r = new Region(); 23 | r.mName = source.name; 24 | return r; 25 | } 26 | 27 | public static Region from(ContentValues values) { 28 | Region r = new Region(); 29 | r.mId = values.getAsLong(KEY_ID); 30 | r.mName = values.getAsString(KEY_NAME); 31 | return r; 32 | } 33 | 34 | public String getName() { 35 | return mName; 36 | } 37 | 38 | public void setName(String name) { 39 | mName = name; 40 | markAsDirty(); 41 | } 42 | 43 | @Override 44 | public ContentValues getValues() { 45 | ContentValues values = new ContentValues(); 46 | values.put(KEY_NAME, mName); 47 | return values; 48 | } 49 | 50 | @Override 51 | protected String getTableName() { 52 | return TABLE_NAME; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /dbexample/src/main/java/com/tonicartos/superslimdbexample/model/Subregion.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslimdbexample.model; 2 | 3 | import android.content.ContentValues; 4 | 5 | /** 6 | * 7 | */ 8 | public class Subregion extends DbModel { 9 | 10 | private static final String KEY_ID = "_id"; 11 | 12 | private static final String KEY_NAME = "name"; 13 | 14 | private static final String KEY_REGION_ID = "region_id"; 15 | 16 | public static final String TABLE_NAME = "subregion"; 17 | 18 | public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "( " + 19 | KEY_ID + " integer primary key, " + 20 | KEY_REGION_ID + " integer, " + 21 | KEY_NAME + " text " + 22 | ");"; 23 | 24 | 25 | protected String mName; 26 | 27 | private long mRegionId; 28 | 29 | public static Subregion from(JsonData.SubRegion source) { 30 | Subregion s = new Subregion(); 31 | s.mName = source.name; 32 | return s; 33 | } 34 | 35 | public static Subregion from(ContentValues values) { 36 | Subregion s = new Subregion(); 37 | if (values.containsKey(KEY_ID)) { 38 | s.mId = values.getAsLong(KEY_ID); 39 | } 40 | 41 | if (values.containsKey(KEY_NAME)) { 42 | s.mName = values.getAsString(KEY_NAME); 43 | } 44 | 45 | if (values.containsKey(KEY_REGION_ID)) { 46 | s.mRegionId = values.getAsLong(KEY_REGION_ID); 47 | } 48 | return s; 49 | } 50 | 51 | public String getName() { 52 | return mName; 53 | } 54 | 55 | public void setName(String name) { 56 | mName = name; 57 | markAsDirty(); 58 | } 59 | 60 | public long getRegionId() { 61 | return mRegionId; 62 | } 63 | 64 | public void setRegionId(long regionId) { 65 | mRegionId = regionId; 66 | } 67 | 68 | @Override 69 | public ContentValues getValues() { 70 | ContentValues values = new ContentValues(); 71 | values.put(KEY_REGION_ID, mRegionId); 72 | values.put(KEY_NAME, mName); 73 | return values; 74 | } 75 | 76 | @Override 77 | protected String getTableName() { 78 | return TABLE_NAME; 79 | } 80 | 81 | public void setRegion(Region region) { 82 | mRegionId = region.getId(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /dbexample/src/main/res/layout-v17/country_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | -------------------------------------------------------------------------------- /dbexample/src/main/res/layout-v17/region_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | -------------------------------------------------------------------------------- /dbexample/src/main/res/layout-v17/subregion_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | -------------------------------------------------------------------------------- /dbexample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /dbexample/src/main/res/layout/country_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | -------------------------------------------------------------------------------- /dbexample/src/main/res/layout/fragment_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 20 | 21 | 26 | 27 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /dbexample/src/main/res/layout/region_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | -------------------------------------------------------------------------------- /dbexample/src/main/res/layout/subregion_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | -------------------------------------------------------------------------------- /dbexample/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /dbexample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonicArtos/SuperSLiM/b21bb218455bc75ac34470e3d594a83c1c8ff30c/dbexample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /dbexample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonicArtos/SuperSLiM/b21bb218455bc75ac34470e3d594a83c1c8ff30c/dbexample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /dbexample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonicArtos/SuperSLiM/b21bb218455bc75ac34470e3d594a83c1c8ff30c/dbexample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /dbexample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonicArtos/SuperSLiM/b21bb218455bc75ac34470e3d594a83c1c8ff30c/dbexample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /dbexample/src/main/res/raw/init_db_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "regions": [ 3 | { 4 | "name": "Africa", 5 | "sub_regions": [ 6 | { 7 | "name": "Eastern Africa", 8 | "countries": [ 9 | {"name": "Burundi"}, 10 | {"name": "Comoros"}, 11 | {"name": "Djibouti"}, 12 | {"name": "Eritrea"}, 13 | {"name": "Ethiopia"}, 14 | {"name": "Kenya"}, 15 | {"name": "Madagascar"}, 16 | {"name": "Malawi"}, 17 | {"name": "Mauritius"}, 18 | {"name": "Mayotte"}, 19 | {"name": "Mozambique"}, 20 | {"name": "Réunion"}, 21 | {"name": "Rwanda"}, 22 | {"name": "Seychelles"}, 23 | {"name": "Somalia"}, 24 | {"name": "South Sudan"}, 25 | {"name": "Uganda"}, 26 | {"name": "United Republic of Tanzania"}, 27 | {"name": "Zambia"}, 28 | {"name": "Zimbabwe"} 29 | ] 30 | }, 31 | { 32 | "name": "Middle Africa", 33 | "countries": [ 34 | {"name": "Angola"}, 35 | {"name": "Cameroon"}, 36 | {"name": "Central African Republic"}, 37 | {"name": "Chad"}, 38 | {"name": "Congo"}, 39 | {"name": "Democratic Republic of the Congo"}, 40 | {"name": "Equatorial Guinea"}, 41 | {"name": "Gabon"}, 42 | {"name": "Sao Tome and Principe"} 43 | ] 44 | }, 45 | { 46 | "name": "Northern Africa", 47 | "countries": [ 48 | {"name": "Algeria"}, 49 | {"name": "Egypt"}, 50 | {"name": "Libya"}, 51 | {"name": "Morocco"}, 52 | {"name": "Sudan"}, 53 | {"name": "Tunisia"}, 54 | {"name": "Western Sahara"} 55 | ] 56 | }, 57 | { 58 | "name": "Southern Africa", 59 | "countries": [ 60 | {"name": "Botswana"}, 61 | {"name": "Lesotho"}, 62 | {"name": "Namibia"}, 63 | {"name": "South Africa"}, 64 | {"name": "Swaziland"} 65 | ] 66 | }, 67 | { 68 | "name": "Western Africa", 69 | "countries": [ 70 | {"name": "Benin"}, 71 | {"name": "Burkina Faso"}, 72 | {"name": "Cabo Verde"}, 73 | {"name": "Cote d'Ivoire"}, 74 | {"name": "Gambia"}, 75 | {"name": "Ghana"}, 76 | {"name": "Guinea"}, 77 | {"name": "Guinea-Bissau"}, 78 | {"name": "Liberia"}, 79 | {"name": "Mali"}, 80 | {"name": "Mauritania"}, 81 | {"name": "Niger"}, 82 | {"name": "Nigeria"}, 83 | {"name": "Saint Helena"}, 84 | {"name": "Senegal"}, 85 | {"name": "Sierra Leone"}, 86 | {"name": "Togo "} 87 | ] 88 | } 89 | ] 90 | }, 91 | { 92 | "name": "Americas", 93 | "sub_regions": [ 94 | { 95 | "name": "Latin America and the Caribbean", 96 | "countries": [ 97 | {"name": "Caribbean"}, 98 | {"name": "Anguilla"}, 99 | {"name": "Antigua and Barbuda"}, 100 | {"name": "Aruba"}, 101 | {"name": "Bahamas"}, 102 | {"name": "Barbados"}, 103 | {"name": "Bonaire, Sint Eustatius and Saba"}, 104 | {"name": "British Virgin Islands"}, 105 | {"name": "Cayman Islands"}, 106 | {"name": "Cuba"}, 107 | {"name": "Curaçao"}, 108 | {"name": "Dominica"}, 109 | {"name": "Dominican Republic"}, 110 | {"name": "Grenada"}, 111 | {"name": "Guadeloupe"}, 112 | {"name": "Haiti"}, 113 | {"name": "Jamaica"}, 114 | {"name": "Martinique"}, 115 | {"name": "Montserrat"}, 116 | {"name": "Puerto Rico"}, 117 | {"name": "Saint-Barthélemy"}, 118 | {"name": "Saint Kitts and Nevis"}, 119 | {"name": "Saint Lucia"}, 120 | {"name": "Saint Martin (French part)"}, 121 | {"name": "Saint Vincent and the Grenadines"}, 122 | {"name": "Sint Maarten (Dutch part)"}, 123 | {"name": "Trinidad and Tobago"}, 124 | {"name": "Turks and Caicos Islands"}, 125 | {"name": "United States Virgin Islands"} 126 | ] 127 | }, 128 | { 129 | "name": "Central America", 130 | "countries": [ 131 | {"name": "Belize"}, 132 | {"name": "Costa Rica"}, 133 | {"name": "El Salvador"}, 134 | {"name": "Guatemala"}, 135 | {"name": "Honduras"}, 136 | {"name": "Mexico"}, 137 | {"name": "Nicaragua"}, 138 | {"name": "Panama"} 139 | ] 140 | }, 141 | { 142 | "name": "South America", 143 | "countries": [ 144 | {"name": "Argentina"}, 145 | {"name": "Bolivia (Plurinational State of)"}, 146 | {"name": "Brazil"}, 147 | {"name": "Chile"}, 148 | {"name": "Colombia"}, 149 | {"name": "Ecuador"}, 150 | {"name": "Falkland Islands (Malvinas)"}, 151 | {"name": "French Guiana"}, 152 | {"name": "Guyana"}, 153 | {"name": "Paraguay"}, 154 | {"name": "Peru"}, 155 | {"name": "Suriname"}, 156 | {"name": "Uruguay"}, 157 | {"name": "Venezuela (Bolivarian Republic of)"} 158 | ] 159 | }, 160 | { 161 | "name": "Northern America", 162 | "countries": [ 163 | {"name": "Bermuda"}, 164 | {"name": "Canada"}, 165 | {"name": "Greenland"}, 166 | {"name": "Saint Pierre and Miquelon"}, 167 | {"name": "United States of America"} 168 | ] 169 | } 170 | ] 171 | }, 172 | { 173 | "name": "Asia", 174 | "sub_regions": [ 175 | { 176 | "name": "Central Asia", 177 | "countries": [ 178 | {"name": "Kazakhstan"}, 179 | {"name": "Kyrgyzstan"}, 180 | {"name": "Tajikistan"}, 181 | {"name": "Turkmenistan"}, 182 | {"name": "Uzbekistan"} 183 | ] 184 | }, 185 | { 186 | "name": "Eastern Asia", 187 | "countries": [ 188 | {"name": "China"}, 189 | {"name": "China, Hong Kong Special Administrative Region"}, 190 | {"name": "China, Macao Special Administrative Region"}, 191 | {"name": "Democratic People's Republic of Korea"}, 192 | {"name": "Japan"}, 193 | {"name": "Mongolia"}, 194 | {"name": "Republic of Korea"} 195 | ] 196 | }, 197 | { 198 | "name": "Southern Asia", 199 | "countries": [ 200 | {"name": "Afghanistan"}, 201 | {"name": "Bangladesh"}, 202 | {"name": "Bhutan"}, 203 | {"name": "India"}, 204 | {"name": "Iran (Islamic Republic of)"}, 205 | {"name": "Maldives"}, 206 | {"name": "Nepal"}, 207 | {"name": "Pakistan"}, 208 | {"name": "Sri Lanka"} 209 | ] 210 | }, 211 | { 212 | "name": "South-Eastern Asia", 213 | "countries": [ 214 | {"name": "Brunei Darussalam"}, 215 | {"name": "Cambodia"}, 216 | {"name": "Indonesia"}, 217 | {"name": "Lao People's Democratic Republic"}, 218 | {"name": "Malaysia"}, 219 | {"name": "Myanmar"}, 220 | {"name": "Philippines"}, 221 | {"name": "Singapore"}, 222 | {"name": "Thailand"}, 223 | {"name": "Timor-Leste"}, 224 | {"name": "Viet Nam"} 225 | ] 226 | }, 227 | { 228 | "name": "Western Asia", 229 | "countries": [ 230 | {"name": "Armenia"}, 231 | {"name": "Azerbaijan"}, 232 | {"name": "Bahrain"}, 233 | {"name": "Cyprus"}, 234 | {"name": "Georgia"}, 235 | {"name": "Iraq"}, 236 | {"name": "Israel"}, 237 | {"name": "Jordan"}, 238 | {"name": "Kuwait"}, 239 | {"name": "Lebanon"}, 240 | {"name": "Oman"}, 241 | {"name": "Qatar"}, 242 | {"name": "Saudi Arabia"}, 243 | {"name": "State of Palestine"}, 244 | {"name": "Syrian Arab Republic"}, 245 | {"name": "Turkey"}, 246 | {"name": "United Arab Emirates"}, 247 | {"name": "Yemen "} 248 | ] 249 | } 250 | ] 251 | }, 252 | { 253 | "name": "Europe", 254 | "sub_regions": [ 255 | { 256 | "name": "Eastern Europe", 257 | "countries": [ 258 | {"name": "Belarus"}, 259 | {"name": "Bulgaria"}, 260 | {"name": "Czech Republic"}, 261 | {"name": "Hungary"}, 262 | {"name": "Poland"}, 263 | {"name": "Republic of Moldova"}, 264 | {"name": "Romania"}, 265 | {"name": "Russian Federation"}, 266 | {"name": "Slovakia"}, 267 | {"name": "Ukraine"} 268 | ] 269 | }, 270 | { 271 | "name": "Northern Europe", 272 | "countries": [ 273 | {"name": "Åland Islands"}, 274 | {"name": "Channel Islands"}, 275 | {"name": "Denmark"}, 276 | {"name": "Estonia"}, 277 | {"name": "Faeroe Islands"}, 278 | {"name": "Finland"}, 279 | {"name": "Guernsey"}, 280 | {"name": "Iceland"}, 281 | {"name": "Ireland"}, 282 | {"name": "Isle of Man"}, 283 | {"name": "Jersey"}, 284 | {"name": "Latvia"}, 285 | {"name": "Lithuania"}, 286 | {"name": "Norway"}, 287 | {"name": "Sark"}, 288 | {"name": "Svalbard and Jan Mayen Islands"}, 289 | {"name": "Sweden"}, 290 | {"name": "United Kingdom of Great Britain and Northern Ireland"} 291 | ] 292 | }, 293 | { 294 | "name": "Southern Europe", 295 | "countries": [ 296 | {"name": "Albania"}, 297 | {"name": "Andorra"}, 298 | {"name": "Bosnia and Herzegovina"}, 299 | {"name": "Croatia"}, 300 | {"name": "Gibraltar"}, 301 | {"name": "Greece"}, 302 | {"name": "Holy See"}, 303 | {"name": "Italy"}, 304 | {"name": "Malta"}, 305 | {"name": "Montenegro"}, 306 | {"name": "Portugal"}, 307 | {"name": "San Marino"}, 308 | {"name": "Serbia"}, 309 | {"name": "Slovenia"}, 310 | {"name": "Spain"}, 311 | {"name": "The former Yugoslav Republic of Macedonia"} 312 | ] 313 | }, 314 | { 315 | "name": "Western Europe", 316 | "countries": [ 317 | {"name": "Austria"}, 318 | {"name": "Belgium"}, 319 | {"name": "France"}, 320 | {"name": "Germany"}, 321 | {"name": "Liechtenstein"}, 322 | {"name": "Luxembourg"}, 323 | {"name": "Monaco"}, 324 | {"name": "Netherlands"}, 325 | {"name": "Switzerland"} 326 | ] 327 | } 328 | ] 329 | }, 330 | { 331 | "name": "Oceania", 332 | "sub_regions": [ 333 | { 334 | "name": "Australia and New Zealand", 335 | "countries": [ 336 | {"name": "Australia"}, 337 | {"name": "New Zealand"}, 338 | {"name": "Norfolk Island"} 339 | ] 340 | }, 341 | { 342 | "name": "Melanesia", 343 | "countries": [ 344 | {"name": "Fiji"}, 345 | {"name": "New Caledonia"}, 346 | {"name": "Papua New Guinea"}, 347 | {"name": "Solomon Islands"}, 348 | {"name": "Vanuatu"} 349 | ] 350 | }, 351 | { 352 | "name": "Micronesia", 353 | "countries": [ 354 | {"name": "Guam"}, 355 | {"name": "Kiribati"}, 356 | {"name": "Marshall Islands"}, 357 | {"name": "Micronesia (Federated States of)"}, 358 | {"name": "Nauru"}, 359 | {"name": "Northern Mariana Islands"}, 360 | {"name": "Palau"} 361 | ] 362 | }, 363 | { 364 | "name": "Polynesia", 365 | "countries": [ 366 | {"name": "American Samoa"}, 367 | {"name": "Cook Islands"}, 368 | {"name": "French Polynesia"}, 369 | {"name": "Niue"}, 370 | {"name": "Pitcairn"}, 371 | {"name": "Samoa"}, 372 | {"name": "Tokelau"}, 373 | {"name": "Tonga"}, 374 | {"name": "Tuvalu"}, 375 | {"name": "Wallis and Futuna Islands"} 376 | ] 377 | } 378 | ] 379 | } 380 | ] 381 | } -------------------------------------------------------------------------------- /dbexample/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /dbexample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #8BC34A 4 | #689F38 5 | #DCEDC8 6 | #FFC107 7 | #212121 8 | #727272 9 | #212121 10 | #B6B6B6 11 | 12 | -------------------------------------------------------------------------------- /dbexample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /dbexample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SuperSLiM DB Example 3 | Hello world! 4 | Settings 5 | 6 | -------------------------------------------------------------------------------- /dbexample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /doc/section.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonicArtos/SuperSLiM/b21bb218455bc75ac34470e3d594a83c1c8ff30c/doc/section.png -------------------------------------------------------------------------------- /doc/src/LayoutManager.md: -------------------------------------------------------------------------------- 1 | # LayoutManager 2 | [TOC] 3 | #Concepts 4 | ## General 5 | Change animation 6 | : The recycler view's animation for changes in the data set. The initial conditions are set by the pre-layout pass, and the final state of each view change animation is given by the real-layout pass. 7 | 8 | Pre-layout 9 | : `RecyclerView.State.isPreLayout() == true` 10 | : The role of the pre-layout pass is to set up the initial conditions of the change animation. The pre-layout pass lays-out regular on-screen views, on-screen disappearing views, and off-screen appearing views. 11 | 12 | Real-layout 13 | : `RecyclerView.State.isPreLayout() == false` 14 | : The second layout pass which gives the final position of views after any animations have run. The real-layout pass lays-out regular on-screen views, on-screen appearing views, and off-screen disappearing views. 15 | 16 | Added views 17 | : Added views are available for laying out in the real-layout pass. 18 | 19 | Removed views 20 | : Removed views are returned from the scrap list in the pre-layout pass to help with correctly laying out the initial position of views affected by the change animation, especially appearing views. Removed views should not be added to the child list, they are only a positional aid. 21 | 22 | Disappearing view 23 | : A view that is on-screen before being animated off-screen. 24 | : A disappearing view is laid out on-screen in the pre-layout pass, and is laid out off-screen in the real-layout. 25 | : In the post-layout pass the recycler view must be told the view is disappearing by calling `addDisappearingView()` on the layout manager. 26 | : Disappearing views are a result of either; the view being moved, or views being added before it, thus pushing it off-screen. 27 | 28 | Appearing view 29 | : A view that is off-screen before being animated onto the screen. 30 | : An appearing view is laid out off-screen in the pre-layout pass, and is laid out on-screen in the real-layout. 31 | : An appearing view can be discovered by detecting views that will be removed after pre-layout, laying them out, but not accounting for the removed views in determining the visual limit of the layout pass; i.e., for a linear layout removed views count for zero height when being laid out, this is to the effect that 100 pixels of removed views results in at least 100 pixels of space being available for appearing views. 32 | 33 | Scrap views 34 | : Views attached to the recycler view which are marked for removal or reuse. 35 | : When layout starts SuperSLiM removes and scraps all child views of the recycler view. When SLMs get views they may be reused from the scrap list or populated from the adapter. Scrap views are automatically removed from the scrap list when they are added back to the recycler view. 36 | : Scrap views remaining after the regular post-layout pass are views that were on-screen in the pre-layout pass. These scrap views are now either removed and should be ignored, or are disappearing views and are to be laid out off-screen. 37 | 38 | Item decorator 39 | : Item decorators can inset child views and add extra content without complicating layout passes. Handling decorators correctly requires using helper methods with the child view as an argument rather than interacting with the child view directly. 40 | 41 | Item changes 42 | : Changes to the data set are notified to the recycler view through a listener interface. The changes are propagated through to the layout manager by calls to `onItemsAdded()`, `onItemsUpdated()`, `onItemsChanged()`, `onItemsMoved()`, and `onItemsRemoved()`. The affected position and ranges are passed in these calls. If the change is outside of the currently laid out area the calls are made before the pre-layout pass. 43 | 44 | Scroll event 45 | : A call to one to the scroll methods `scrollVerticallyBy()` or `scrollHorizontallyBy()`. Scroll distance is given in pixels and the result indicates overscroll. 46 | 47 | Anchor position 48 | : Logically, the current position that the user has scrolled to. For touch scrolling, this is the adapter position of the view that is logically and visually closest to the reference edge. Alternatively, for focus based scrolling, which is assistive or keyboard driven, this is the position of the focused view. 49 | : The anchor position is updated after each scroll or focus event, or is directly modified by calls to *scroll to* or *smooth scroll to* a position. 50 | : The current position is retained through configuration changes and used to re-layout the content with that position as an anchor. 51 | 52 | Anchor position offset 53 | : The distance from the reference edge that the view indicated by the anchor position is located. Use of this value allows for the logical recreation of the scrolled view after configuration changes. 54 | : This offset is updated each time the anchor position is updated. 55 | 56 | Over scroll 57 | : When a layout pass displays less content than should have been. The visual effect is where the scrolled content is displayed with its bottom edge not aligned with the bottom padding edge or view edge, but further up the view instead. This is caused when items from the anchor position to the last item are laid out, but there is still space below the content and there are items before the anchor position. The solution is to detect this situation after a layout pass, adjust all the laid out views down to the correct position, and then fill in the revealed space. 58 | 59 | ## SuperSLiM 60 | 61 | Scroll layout 62 | : Scrolling in SuperSLiM is handled by moving all content on the screen and then laying out revealed content on the leading edge as a special scroll layout pass. 63 | 64 | Scroll pre-trim 65 | : Before the scroll trim is performed, a scroll pre-trim pass is made on sections intersecting the trailing edge. Any special positioning of views after the scroll event can be made here. The base `SectionLayoutManager` implementation uses this to correctly reposition stickied headers on the up edge. 66 | 67 | Scroll trim 68 | : After a scroll layout, any content wholly outside the trailing edge is trimmed. 69 | 70 | Trim notification 71 | : When a view is trimmed due to a scroll, a notification is passed down the section hierarchy so that section layout managers can update any cached data about a section. 72 | 73 | Section coordinate space 74 | : Each section has its own virtual space. Values in this space are transformed up the section graph to the root coordinate space used by the layout manager and the recycler view. 75 | 76 | Config transformation 77 | : The layout manager is configured by the locale and user set values. A config transformation is applied to coordinates coming into and out of the recycler view. In this manner, it is easy to support different layout directions without custom logic in each section layout manager implementation. 78 | 79 | Start edge 80 | : The '*left edge*' of the layout managed area. For a RTL locale, this is actually the right edge. 81 | 82 | End edge 83 | : The '*right edge*' of the layout managed area. For a RTL locale, this is actually the left edge. 84 | 85 | Reverse layout 86 | : Layout is typically performed from top left to bottom right, translated for locale differences. When reversed, the layout is performed from the bottom right to the top right. 87 | 88 | Orientation 89 | : The orientation can be horizontal or vertical. 90 | 91 | Stack from end 92 | : Views are laid out from the bottom to the top. The bottom edge is the reference edge for determining current position. Headers still appear at the top of the section. 93 | 94 | Headers at end 95 | : Headers are positioned after the section content. This makes for a reversal of the sticky logic. Instead of sticking to the logical top of the section, they will stick to the logical bottom of the section. 96 | : Handled inside `BaseSectionLayoutManager`. 97 | 98 | Configuration transformer 99 | : A function that transforms one coordinate system into another; i.e., turning the LTR, TTB coordinate system used by section layout managers, into the coordinate system used by a RTL recycler view - a configuration derived from the users locale. 100 | 101 | Semi-real coordinate 102 | : A coordinate that has been transformed into a virtual coordinate by a configuration transformation, but is also not nested within another virtual coordinate space. In this manner the top edge of this space can be called the semi-real top, though it could actually be the left edge of a horizontally scrolling recycler view. 103 | 104 | # Layout transformation 105 | Section layout managers work in their own virtual coordinate space that is transformed into the window coordinates. As such, section layout managers behave as though they are always operating in a vertical scrolling, top-to-bottom, left-to-right coordinate space. The width of the section space is defined by the parent section, and may have an invisible offset as defined by the parent. 106 | 107 | **Writes** 108 | ```sequence 109 | section->helper: layoutChild(view, values) 110 | helper->helper: apply inverse subspace transformation 111 | helper->helper parent: layoutChild(view, values) 112 | helper parent->helper parent: apply inverse subspace transformation 113 | Note over helper parent: recurse layoutChild up graph ancestry 114 | helper parent->layout manager: layoutChild(view, values) 115 | layout manager->layout manager: apply inverse configuration transformation 116 | layout manager->recycler view: layoutChild(view, values) 117 | ``` 118 | 119 | **Reads** 120 | ```sequence 121 | section->helper: request 122 | helper->helper parent: request 123 | Note over helper parent: recurse request up graph ancestry 124 | helper parent->layout manager: request 125 | layout manager->recycler view: request 126 | recycler view-->layout manager: result 127 | layout manager->layout manager: apply configuration transformation 128 | layout manager-->helper parent: result 129 | helper parent->helper parent: apply subspace transformation 130 | helper parent-->helper: result 131 | helper->helper: apply subspace transformation 132 | helper-->section: result 133 | ``` 134 | 135 | The above diagrams give the logical effect of the behaviour of the helper. However, transformations can be combined together and be expressed 136 | 137 | **Writes** 138 | ```sequence 139 | section->helper: write values 140 | helper->helper: apply inverse subspace transformation 141 | helper->layout manager: write values in layout managers coords 142 | layout manager->layout manager: apply inverse configuration transformation 143 | layout manager->recycler view: write values in recycler views coords 144 | ``` 145 | 146 | **Reads** 147 | ```sequence 148 | section->helper: request 149 | helper->layout manager: request 150 | layout manager->recycler view: request 151 | recycler view-->layout manager: result in recycler views coords 152 | layout manager->layout manager: apply configuration transformation 153 | layout manager-->helper: result in layout managers coords 154 | helper->helper: apply subspace transformation 155 | helper-->section: result in subsections coords 156 | ``` 157 | 158 | ## Configuration transformations 159 | The `LayoutHelper` has a number of `ConfigTransformation` s. 160 | > w = width of recycler view, h = height of recycler view. 161 | *All of the config transformations are involutory, so can be applied to transform the coordinate space in either direction.* 162 | 163 | LTR 164 | : A no-op transformation. 165 | $$\text{ltr}\begin{pmatrix}l\\t\\r\\b\end{pmatrix} = \begin{pmatrix}l\\t\\r\\b\end{pmatrix}$$ 166 | 167 | RTL 168 | : Mirrors the horizontal values. 169 | $$\text{rtl}\begin{pmatrix}l\\t\\r\\b\end{pmatrix} = \begin{pmatrix}w-r\\t\\w-l\\b\end{pmatrix}$$ 170 | 171 | Stack from end 172 | : Mirrors the vertical values. 173 | $$\text{sfe}\begin{pmatrix}l\\t\\r\\b\end{pmatrix} = \begin{pmatrix}l\\h-b\\r\\h-t\end{pmatrix}$$ 174 | 175 | Reverse 176 | : Mirrors both the horizontal and vertical values. 177 | $$\text{rev}\begin{pmatrix}l\\t\\r\\b\end{pmatrix} = \begin{pmatrix}w - r\\h-b\\w-l\\h-t\end{pmatrix}$$ 178 | 179 | Vertical orientation 180 | : A no-op transformation. 181 | $$\text{ver}\begin{pmatrix}l\\t\\r\\b\end{pmatrix} = \begin{pmatrix}l\\t\\r\\b\end{pmatrix}$$ 182 | 183 | Horizontal orientation 184 | : Swaps the top and left values, and the bottom and right values. Effectively mirroring along a line drawn from the NW corner to the SE corner. 185 | $$\text{hor}\begin{pmatrix}l\\t\\r\\b\end{pmatrix} = \begin{pmatrix}t\\l\\b\\r\end{pmatrix}$$ 186 | 187 | : It is important to note that for horizontal scrolling configurations, width and height measurement calls are transposed. 188 | 189 | ## Nesting 190 | Subsections have their virtual space inset inside the parent section. When creating the subsection layout the parent SLM gives the available width, and the point of the top left corner. The derivative values for the subsections total x and y offsets within the root coordinate system are calculated and calls from the subsection helper can be made directly to the root interface. 191 | 192 | # Tracking graph changes 193 | ~~Recycler view maintains its own state for tracking what items are available for layout or not. When items changes are notified, the layout manager gets informed through the data change callbacks - *onItems\*()*. These events happen at a time determined by the recycler view, either before layout or after pre-layout. Superslim layout manager also receives events for changes to the section graph direct from the adapter. The events from the adapter are stored pending the events from the recycler view. Once matching events, one from each source, are received, they are reconciled and the appropriate change is made to the internal section graph.~~ 194 | 195 | To keep the internal section graph consistent with the recycler view model, the layout manager has to reconcile recycler views reordered item change notifications with changes that happened before that to the adapter section graph. To do this, the layout manager receives item change callbacks from the recycler view, and updates an internal section graph accordingly. When needed, the layout manager can refer to the section graph in the adapter to fetch additional data about the event; e.g., is the item being added, in the callback, a member of a new or already existing section? 196 | 197 | To track sections, an internal id is used separate from the user given section id. 198 | 199 | ## Section graph events 200 | Section level events need additional section data passed to the layout manager. This needs to be paired with the appropriate callback from recycler view before updating the internal graph. 201 | 202 | Add section 203 | : Add section -> add items to item manager -> notify items added 204 | 205 | Remove section 206 | : Remove section -> remove items from item manager -> notify items removed 207 | 208 | Move section 209 | : Move section -> move items in item manager -> notify items moved one by one 210 | 211 | Update section 212 | : Change section configuration -> update items in item manager -> notify items updated 213 | 214 | Item level events are simple and don't involve extra data. 215 | 216 | Add item or header 217 | : Add item or header -> add item to item manager -> notify item added 218 | 219 | Remove item or header 220 | : Remove item or header -> remove item in item manager -> notify item removed 221 | 222 | Move item 223 | : Move item -> move item in item manager -> notify item moved 224 | 225 | Update item 226 | : Update item -> update item in item manager -> notify item updated 227 | 228 | 229 | # Layout manager code paths 230 | 231 | onLayoutChild 232 | scrollViewBy 233 | 234 | # Adapter 235 | sectionLookup: map of T to section 236 | internalLookup: map of integer to section 237 | root: section 238 | 239 | #Section graph 240 | ##Section 241 | userId: T 242 | hasHeader: boolean 243 | totalChildren: integer 244 | : constraint: totalChildren >= subsections.size() 245 | 246 | subsections: list of sections 247 | 248 | # Internal section graph 249 | ## Section 250 | adapterPosition: integer 251 | : Position of section in adapter. If the section is not empty, this is also the adapter position of the first item in the section. 252 | 253 | hasHeader: boolean 254 | totalChildren: integer 255 | : **constraint:** totalChildren >= subsections.size() + 1 if hasHeader is true 256 | 257 | subsections: list of sections 258 | 259 | # Section layout manager 260 | ## Laying out children 261 | The difference between items and subsections is hidden from the SLM. Children can be measured as per the usual layout manager pattern, however subsections always fill the available width and have an undefined height, items behave as normal. To layout and add the child, the SLM defines the bounds and position of the child and the abstraction handles the rest. Laying out a subsection cascades into a layout call for that section and SLM with the given layout area. 262 | 263 | ## Pre-layout child handling 264 | A child object abstracts the difference between subsections and items. The layout helper 265 | 266 | ``` 267 | child = getChild(i) 268 | measure(child) 269 | layoutAndAdd(child, left, top, right, bottom) 270 | child.done() 271 | ``` 272 | 273 | ## Child abstraction 274 | ```Kotlin 275 | interface Child { 276 | val measuredWidth 277 | val measuredHeight 278 | val left 279 | val top 280 | val right 281 | val bottom 282 | } 283 | ``` 284 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | org.gradle.jvmargs=-Xms512m -Xmx1024m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | kotlin.incremental=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonicArtos/SuperSLiM/b21bb218455bc75ac34470e3d594a83c1c8ff30c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Mar 02 23:03:18 GMT 2017 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-3.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion compile_sdk 6 | buildToolsVersion build_tools_version 7 | 8 | defaultConfig { 9 | minSdkVersion 9 10 | targetSdkVersion target_sdk 11 | versionCode 5 12 | versionName "0.5" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | buildConfigField("boolean", "UNIT_TEST", "false") 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | debuggable true 20 | } 21 | debug { 22 | buildConfigField("boolean", "UNIT_TEST", "true") 23 | } 24 | unitTest { 25 | buildConfigField("boolean", "UNIT_TEST", "true") 26 | debuggable true 27 | } 28 | } 29 | 30 | // Always show the result of every unit test, even if it passes. 31 | testOptions.unitTests.all { 32 | testLogging { 33 | events 'passed', 'skipped', 'failed', 'standardOut', 'standardError' 34 | } 35 | } 36 | 37 | sourceSets { 38 | main.java.srcDirs += 'src/main/kotlin' 39 | test.java.srcDirs += 'src/test/kotlin' 40 | } 41 | } 42 | 43 | dependencies { 44 | testCompile 'junit:junit:4.12' 45 | testCompile 'org.hamcrest:hamcrest-all:1.3' 46 | testCompile "com.nhaarman:mockito-kotlin:1.3.0" 47 | 48 | 49 | compile fileTree(dir: 'libs', include: ['*.jar']) 50 | compile "com.android.support:appcompat-v7:$supportlib_version" 51 | compile "com.android.support:support-v4:$supportlib_version" 52 | compile "com.android.support:recyclerview-v7:$supportlib_version" 53 | 54 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 55 | compile "org.jetbrains.anko:anko-sdk15:$anko_version" 56 | } 57 | -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/tonic/.local/share/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 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/tonicartos/superslim/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | 11 | public ApplicationTest() { 12 | super(Application.class); 13 | } 14 | } -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/adapter/FooterStyle.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.adapter; 2 | 3 | import com.tonicartos.superslim.SectionConfig; 4 | 5 | import android.support.annotation.IntDef; 6 | 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | 10 | @IntDef({SectionConfig.FOOTER_END, 11 | SectionConfig.FOOTER_STICKY, 12 | SectionConfig.FOOTER_INLINE, 13 | SectionConfig.FOOTER_START}) 14 | @Retention(RetentionPolicy.SOURCE) 15 | public @interface FooterStyle { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/adapter/HeaderStyle.java: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.adapter; 2 | 3 | import com.tonicartos.superslim.SectionConfig; 4 | 5 | import android.support.annotation.IntDef; 6 | 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | 10 | @IntDef({SectionConfig.HEADER_END, 11 | SectionConfig.HEADER_STICKY, 12 | SectionConfig.HEADER_INLINE, 13 | SectionConfig.HEADER_START}) 14 | @Retention(RetentionPolicy.SOURCE) 15 | public @interface HeaderStyle { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/adapter/adapter.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.adapter 2 | 3 | import android.support.annotation.IntDef 4 | import android.support.v7.widget.RecyclerView 5 | import com.tonicartos.superslim.* 6 | import com.tonicartos.superslim.layout.LinearSectionConfig 7 | import java.util.* 8 | 9 | interface Graph { 10 | fun getNumSections(): Int 11 | fun getSection(position: Int): Section 12 | fun addSection(section: Section) 13 | fun insertSection(position: Int, section: Section) 14 | fun removeSection(position: Int): Section 15 | fun moveSection(from: Int, to: Int) 16 | } 17 | 18 | abstract class SuperSlimAdapter, VH : RecyclerView.ViewHolder> 19 | private constructor(internal val graph: GraphImpl, internal val itemManager: ItemManager, 20 | private val adapterContract: AdapterContractImpl) : RecyclerView.Adapter(), 21 | AdapterContract by adapterContract, 22 | Graph by graph { 23 | init { 24 | graph.init(itemManager) 25 | itemManager.init(this) 26 | adapterContract.init(this) 27 | } 28 | 29 | constructor() : this(GraphImpl(), ItemManager(), AdapterContractImpl()) 30 | 31 | abstract fun onBindViewHolder(holder: VH, item: Item) 32 | final override fun onBindViewHolder(holder: VH, position: Int) { 33 | onBindViewHolder(holder, itemManager[position]) 34 | } 35 | 36 | final override fun getItemCount() = itemManager.itemCount 37 | final override fun getItemViewType(position: Int) = itemManager[position].type 38 | 39 | /**************************************************** 40 | * Section registry 41 | ****************************************************/ 42 | 43 | internal val sectionLookup = HashMap() 44 | 45 | fun getSectionWithId(id: ID): Section? = sectionLookup[id] 46 | 47 | @JvmOverloads 48 | fun createSection(id: ID, config: SectionConfig, header: Item? = null, footer: Item? = null): Section { 49 | val section = Section() 50 | section.header = header 51 | section.footer = footer 52 | section.configuration = config 53 | registerSection(id, section) 54 | return section 55 | } 56 | 57 | @JvmOverloads 58 | fun createSection(id: ID, header: Item? = null, footer: Item? = null): Section 59 | = createSection(id, LinearSectionConfig(), header, footer) 60 | 61 | /** 62 | * Register a [section] to [id]. Descendant sections will be registered if [cascade] is true. 63 | */ 64 | @JvmOverloads fun registerSection(id: ID, section: Section, cascade: Boolean = true) { 65 | sectionLookup[id]?.let { if (it !== section) deregister(id) } 66 | 67 | sectionLookup[id] = section 68 | section.registration = Registration(id, this) 69 | if (cascade) { 70 | // If there are any descendants 71 | section.subsections.forEach { subsection -> 72 | @Suppress("UNCHECKED_CAST") 73 | val reusableRegistration = subsection.registration as? Registration 74 | if (reusableRegistration == null) { 75 | // Can't understand registration, and since it can't be used, ditch it. 76 | subsection.registration = null 77 | } else { 78 | registerSection(reusableRegistration.id, subsection, cascade) 79 | } 80 | } 81 | } 82 | } 83 | 84 | @JvmOverloads fun deregister(id: ID, stripIds: Boolean = true, cascade: Boolean = true): Section? { 85 | return sectionLookup.remove(id)?.apply { 86 | if (stripIds) { 87 | registration = null 88 | } 89 | if (cascade) { 90 | subsections.forEach { it.registration?.deregister(stripIds, cascade) } 91 | } 92 | } 93 | } 94 | } 95 | 96 | private class AdapterContractImpl> : AdapterContract { 97 | private lateinit var adapter: SuperSlimAdapter 98 | 99 | fun init(adapter: SuperSlimAdapter) { 100 | this.adapter = adapter 101 | } 102 | 103 | override fun onLayoutManagerAttached(layoutManager: SuperSlimLayoutManager) { 104 | if (adapter.graph.contract != null) throw OnlySupportsOneRecyclerViewException() 105 | val contract = DataChangeContract(adapter, layoutManager) 106 | adapter.graph.contract = contract 107 | } 108 | 109 | override fun onLayoutManagerDetached(layoutManager: SuperSlimLayoutManager) { 110 | adapter.graph.contract = null 111 | } 112 | 113 | 114 | override fun getRoot() = adapter.graph.root.configuration 115 | 116 | override fun setRootId(id: Int) { 117 | adapter.graph.root.id = id 118 | } 119 | 120 | override fun populateRoot(out: SectionData) { 121 | out.adapterPosition = 0 122 | out.hasHeader = false 123 | out.itemCount = adapter.graph.root.itemCount 124 | out.childCount = adapter.graph.root.childCount 125 | out.subsectionsById = adapter.graph.root.subsections.map { it.id } 126 | } 127 | 128 | override fun getSections() = adapter.sectionLookup.mapValues { it.value.configuration } 129 | 130 | override fun setSectionIds(idMap: Map<*, Int>) { 131 | idMap.forEach { 132 | val section = adapter.sectionLookup[it.key] ?: throw IllegalArgumentException( 133 | "unknown id \"${it.key}\" for section lookup") 134 | section.id = it.value 135 | } 136 | } 137 | 138 | override fun populateSection(data: Pair<*, SectionData>) { 139 | val section = adapter.sectionLookup[data.first] ?: throw IllegalArgumentException( 140 | "unknown id \"${data.first}\" for section lookup") 141 | data.second.adapterPosition = section.positionInAdapter 142 | data.second.hasHeader = section.header != null 143 | data.second.itemCount = section.itemCount 144 | data.second.childCount = section.childCount 145 | data.second.subsectionsById = section.subsections.map { it.id } 146 | } 147 | 148 | override fun getData(position: Int): AdapterContract.Data { 149 | val item = adapter.itemManager[position] 150 | val parent = item.parent!! 151 | return AdapterContract.data.pack(parent.id, item.positionInParent, when (item) { 152 | parent.header -> AdapterContract.Data.HEADER 153 | parent.footer -> AdapterContract.Data.FOOTER 154 | else -> AdapterContract.Data.OTHER 155 | }) 156 | } 157 | } 158 | 159 | 160 | internal interface SectionContract { 161 | fun notifySectionInserted(section: Section): Int 162 | fun notifySectionRemoved(section: Section) 163 | fun notifySectionUpdated(section: Section) 164 | } 165 | 166 | private class DataChangeContract(val adapter: SuperSlimAdapter<*, *>, 167 | val layoutManager: SuperSlimLayoutManager) : SectionContract { 168 | override fun notifySectionInserted( 169 | section: Section): Int = layoutManager.notifySectionAdded(section.parent!!.id, section.positionInParent, 170 | section.configuration) 171 | 172 | override fun notifySectionRemoved(section: Section) { 173 | layoutManager.notifySectionRemoved(section.id, section.parent!!.id) 174 | } 175 | 176 | //final override fun notifySectionMoved(section: Section, toParent: Section, toPosition: Int) { 177 | // layoutManager.notifySectionMoved(section.id, section.parent!!.id, section.positionInParent, toParent.id, toPosition) 178 | //} 179 | 180 | override fun notifySectionUpdated(section: Section) { 181 | layoutManager.notifySectionUpdated(section.id, section.configuration) 182 | if (section.itemCount > 0) { 183 | adapter.notifyItemRangeChanged(section.positionInAdapter, section.itemCount) 184 | } 185 | } 186 | } 187 | 188 | internal class ItemManager { 189 | fun init(adapter: SuperSlimAdapter<*, *>) { 190 | this.adapter = adapter 191 | } 192 | 193 | private var adapter: SuperSlimAdapter<*, *>? = null 194 | 195 | private val items = ArrayList() 196 | 197 | val itemCount: Int 198 | get() = items.size 199 | 200 | operator fun get(position: Int) = items[position] 201 | 202 | fun insert(position: Int, item: Item) { 203 | items.add(position, item) 204 | if (BuildConfig.UNIT_TEST) return 205 | adapter?.notifyItemInserted(position) 206 | } 207 | 208 | fun insert(start: Int, items: List) { 209 | this.items.addAll(start, items) 210 | if (BuildConfig.UNIT_TEST) return 211 | adapter?.notifyItemRangeInserted(start, items.size) 212 | } 213 | 214 | fun move(from: Int, to: Int) { 215 | items.add(to, items.removeAt(from)) 216 | if (BuildConfig.UNIT_TEST) return 217 | adapter?.notifyItemMoved(from, to) 218 | } 219 | 220 | fun remove(position: Int) { 221 | items.removeAt(position) 222 | if (BuildConfig.UNIT_TEST) return 223 | adapter?.notifyItemRemoved(position) 224 | } 225 | 226 | fun removeRange(start: Int, range: Int) { 227 | for (i in start until start + range) { 228 | items.removeAt(start) 229 | } 230 | if (BuildConfig.UNIT_TEST) return 231 | adapter?.notifyItemRangeRemoved(start, range) 232 | } 233 | 234 | operator fun set(position: Int, value: Item) { 235 | items[position] = value 236 | if (BuildConfig.UNIT_TEST) return 237 | adapter?.notifyItemChanged(position) 238 | } 239 | } 240 | 241 | internal class Registration>(val id: ID, val manager: SuperSlimAdapter) { 242 | @JvmOverloads fun deregister(stripIds: Boolean = true, cascade: Boolean = true) { 243 | manager.deregister(id, stripIds, cascade) 244 | } 245 | } 246 | 247 | class OnlySupportsOneRecyclerViewException : RuntimeException( 248 | "SuperSLiM currently only supports a single recycler view per adapter.") 249 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/adapter/graph.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.adapter 2 | 3 | import android.util.Log 4 | import com.tonicartos.superslim.AdapterContract 5 | import com.tonicartos.superslim.SectionConfig 6 | import com.tonicartos.superslim.layout.LinearSectionConfig 7 | import java.util.* 8 | 9 | sealed class Node { 10 | var positionInParent: Int = 0 11 | val positionInAdapter: Int 12 | get() = (parent?.positionInAdapter ?: 0) + peersItemsBeforeThis 13 | 14 | var parent: Section? = null 15 | internal set 16 | 17 | var itemCount: Int = 0 18 | protected set 19 | open val childCount: Int get() = 0 20 | 21 | fun removeFromParent() = parent?.remove(positionInParent) 22 | 23 | /** 24 | * Number of items held by other nodes before this one in the same section. 25 | */ 26 | internal var peersItemsBeforeThis: Int = 0 27 | open internal var itemManager: ItemManager? = null 28 | internal abstract fun insertItemsToAdapter() 29 | internal abstract fun removeItemsFromAdapter() 30 | 31 | open internal fun init(positionInParent: Int, itemsBeforeThis: Int, parent: Section, itemManager: ItemManager?) { 32 | this.itemManager = itemManager 33 | this.parent = parent 34 | this.peersItemsBeforeThis = itemsBeforeThis 35 | this.positionInParent = positionInParent 36 | } 37 | 38 | open internal fun reset() { 39 | parent = null 40 | itemManager = null 41 | } 42 | 43 | abstract class ItemNode : Node() { 44 | init { 45 | itemCount = 1 46 | } 47 | } 48 | 49 | abstract class SectionNode : Node() { 50 | } 51 | } 52 | 53 | /** 54 | * An item in the graph. Used in [SuperSlimAdapter.onBindViewHolder] so any packaged data can be used for the 55 | * binding or to lookup data elsewhere. 56 | */ 57 | open class Item(val type: Int = 0, val data: Any? = null) : Node.ItemNode() { 58 | override fun insertItemsToAdapter() { 59 | itemManager?.insert(positionInAdapter, this) 60 | } 61 | 62 | override fun removeItemsFromAdapter() { 63 | itemManager?.remove(positionInAdapter) 64 | } 65 | } 66 | 67 | /** 68 | * A section in the graph. 69 | */ 70 | class Section internal constructor(contract: SectionContract? = null) : Node.SectionNode(), Iterable { 71 | private val _contract: SectionContract? 72 | internal val contract: SectionContract? 73 | get() = _contract ?: parent?.contract 74 | 75 | init { 76 | _contract = contract 77 | } 78 | 79 | /** 80 | * An id assigned by the layout manager. 81 | */ 82 | internal var id: Int = -1 83 | 84 | internal var registration: Registration<*>? = null 85 | fun deregister() { 86 | registration?.deregister() 87 | } 88 | 89 | var configuration: SectionConfig = LinearSectionConfig() 90 | get() = field 91 | set(value) { 92 | field = value 93 | contract?.notifySectionUpdated(this) 94 | } 95 | 96 | override var itemManager: ItemManager? 97 | get() = super.itemManager 98 | set(value) { 99 | super.itemManager = value 100 | children.forEach { child -> child.itemManager = value } 101 | header?.itemManager = value 102 | footer?.itemManager = value 103 | } 104 | 105 | /************************* 106 | * Header management 107 | *************************/ 108 | 109 | var header: Item? = null 110 | set(value) { 111 | if (value == null) { 112 | field?.let { 113 | it.removeItemsFromAdapter() 114 | totalItemsChanged(-1) 115 | children.forEach { child -> child.peersItemsBeforeThis -= 1 } 116 | field = null 117 | } 118 | return 119 | } 120 | initChild(0, value) 121 | value.peersItemsBeforeThis = 0 122 | if (field != null) { 123 | itemManager?.set(positionInAdapter, value) 124 | } else { 125 | itemManager?.insert(positionInAdapter, value) 126 | children.forEach { it.peersItemsBeforeThis += 1 } 127 | footer?.let { it.peersItemsBeforeThis += 1 } 128 | totalItemsChanged(1) 129 | } 130 | field = value 131 | } 132 | 133 | var footer: Item? = null 134 | set(value) { 135 | if (value == null) { 136 | field?.let { 137 | it.removeItemsFromAdapter() 138 | totalItemsChanged(-1) 139 | field = null 140 | } 141 | return 142 | } 143 | initChild(childCount, value) 144 | if (field != null) { 145 | itemManager?.set(value.positionInAdapter, value) 146 | } else { 147 | value.insertItemsToAdapter() 148 | totalItemsChanged(1) 149 | } 150 | field = value 151 | } 152 | 153 | fun removeHeader() { 154 | header = null 155 | } 156 | 157 | fun removeFooter() { 158 | footer = null 159 | } 160 | 161 | 162 | /************************* 163 | * Expandable section stuff 164 | *************************/ 165 | 166 | var collapsed: Boolean = false 167 | private set 168 | 169 | fun toggleChildren() = if (collapsed) expandChildren() else collapseChildren() 170 | 171 | private fun expandChildren() { 172 | var numItemsAdded = 0 173 | children.forEachIndexed { i, child -> 174 | initChild(i, child) 175 | child.insertItemsToAdapter() 176 | numItemsAdded += child.itemCount 177 | } 178 | footer?.let { footer -> 179 | initChild(childCount, footer) 180 | footer.insertItemsToAdapter() 181 | numItemsAdded += 1 182 | } 183 | totalItemsChanged(numItemsAdded) 184 | collapsed = false 185 | } 186 | 187 | private fun collapseChildren() { 188 | val headerCount = if (header == null) 0 else 1 189 | val footerCount = if (footer == null) 0 else 1 190 | val numItemsToRemove = itemCount - headerCount 191 | itemManager?.removeRange(positionInAdapter + headerCount, numItemsToRemove) 192 | for (it in children) { 193 | it.reset() 194 | } 195 | totalItemsChanged(-numItemsToRemove) 196 | collapsed = true 197 | } 198 | 199 | /************************* 200 | * Item management 201 | *************************/ 202 | 203 | override fun insertItemsToAdapter() { 204 | if (id == -1) { 205 | id = contract?.notifySectionInserted(this) ?: -1 206 | } 207 | 208 | header?.let { header -> 209 | itemManager?.insert(positionInAdapter, header) 210 | } 211 | 212 | if (!collapsed) { 213 | children.forEachIndexed { i, child -> 214 | child.itemManager = itemManager 215 | initChild(i, child) 216 | child.insertItemsToAdapter() 217 | } 218 | footer?.let { footer -> 219 | footer.itemManager = itemManager 220 | initChild(childCount, footer) 221 | footer.insertItemsToAdapter() 222 | } 223 | } 224 | 225 | } 226 | 227 | override fun removeItemsFromAdapter() { 228 | contract?.notifySectionRemoved(this) 229 | id = -1 230 | 231 | itemManager?.removeRange(positionInAdapter, itemCount) 232 | } 233 | 234 | /**************************************************** 235 | * Children stuff 236 | ****************************************************/ 237 | 238 | private val mutableChildren: ArrayList = arrayListOf() 239 | val children: List 240 | get() = mutableChildren 241 | 242 | override val childCount: Int get() = children.size 243 | 244 | private fun initChild(position: Int, child: Node) { 245 | val numItemsBeforeChild = when { 246 | position > 0 -> children[position - 1].let { it.peersItemsBeforeThis + it.itemCount } 247 | header == null -> 0 248 | else -> 1 249 | } 250 | child.init(position, numItemsBeforeChild, this, itemManager) 251 | } 252 | 253 | /************************* 254 | * Child actions 255 | *************************/ 256 | 257 | fun getAdapterPositionOfChild(position: Int): Int = children[position].positionInAdapter 258 | 259 | /** 260 | * Get child at [position]. Throws runtime exception if child isn't of the expected type. 261 | */ 262 | @Suppress("UNCHECKED_CAST") 263 | operator fun get(position: Int) = children[position] as T 264 | 265 | /** 266 | * Get child at [position]. Returns null if child isn't of the expected type. 267 | */ 268 | @Suppress("UNCHECKED_CAST") 269 | fun getOptional(position: Int) = children[position] as? T 270 | 271 | 272 | /** 273 | * Add child to end of children. 274 | */ 275 | fun add(child: Node) { 276 | insert(children.size, child) 277 | } 278 | 279 | /** 280 | * Insert [child] into [position]. 281 | */ 282 | fun insert(position: Int, child: Node) { 283 | val dest = if (position !in 0..children.size - 1) children.size else position 284 | 285 | if (!collapsed) { 286 | initChild(dest, child) 287 | child.insertItemsToAdapter() 288 | 289 | // Update children after position. 290 | val numItemsAdded = child.itemCount 291 | children.subList(dest, children.size).forEach { 292 | it.peersItemsBeforeThis += numItemsAdded 293 | it.positionInParent += 1 294 | } 295 | totalItemsChanged(numItemsAdded) 296 | } 297 | mutableChildren.add(dest, child) 298 | } 299 | 300 | /** 301 | * Move child found at [from], to [to]. 302 | */ 303 | fun move(from: Int, to: Int) { 304 | if (from == to) return 305 | 306 | val moving = mutableChildren.removeAt(from) 307 | if (!collapsed) { 308 | moving.removeItemsFromAdapter() 309 | 310 | // Update children between from and to. 311 | val numItemsRemoved = moving.itemCount 312 | if (from < to) { 313 | children.subList(from, to).forEach { 314 | it.peersItemsBeforeThis -= numItemsRemoved 315 | it.positionInParent -= 1 316 | } 317 | } else { 318 | children.subList(to, from).forEach { 319 | it.peersItemsBeforeThis += numItemsRemoved 320 | it.positionInParent += 1 321 | } 322 | } 323 | 324 | // Re-init child in new place. 325 | initChild(to, moving) 326 | moving.insertItemsToAdapter() 327 | } 328 | mutableChildren.add(to, moving) 329 | } 330 | 331 | /** 332 | * Remove child at [position]. 333 | */ 334 | fun remove(position: Int) { 335 | val removed = mutableChildren.removeAt(position) 336 | 337 | if (!collapsed) { 338 | removed.removeItemsFromAdapter() 339 | 340 | // Update children after the removed child. 341 | val numItemsRemoved = removed.itemCount 342 | children.subList(position, children.size).forEach { 343 | it.peersItemsBeforeThis -= numItemsRemoved 344 | it.positionInParent -= 1 345 | } 346 | totalItemsChanged(-numItemsRemoved) 347 | } 348 | removed.reset() 349 | } 350 | 351 | /** 352 | * Replace child at [position] with [replacement]. 353 | */ 354 | operator fun set(position: Int, replacement: Node) { 355 | val toRemove = children[position] 356 | 357 | if (!collapsed) { 358 | toRemove.removeItemsFromAdapter() 359 | initChild(position, replacement) 360 | replacement.insertItemsToAdapter() 361 | 362 | // Update children after the target position if there is a change in total items. 363 | val numItemsAdded = replacement.itemCount - toRemove.itemCount 364 | if (numItemsAdded != 0) { 365 | children.subList(position + 1, children.size).forEach { 366 | it.peersItemsBeforeThis += numItemsAdded 367 | } 368 | totalItemsChanged(numItemsAdded) 369 | } 370 | } 371 | 372 | mutableChildren[position] = replacement 373 | toRemove.reset() 374 | } 375 | 376 | /************************* 377 | * Listing and navigating children 378 | *************************/ 379 | 380 | override fun iterator(): Iterator = children.iterator() 381 | 382 | /** 383 | * Returns a list containing all items matching the given [predicate]. 384 | */ 385 | inline fun filterItems(predicate: (Item) -> Boolean): List { 386 | return items.filterTo(ArrayList(), predicate) 387 | } 388 | 389 | /** 390 | * A list containing all items in the section. 391 | */ 392 | @Suppress("WARNINGS") 393 | val items: List 394 | get() = children.filterTo(ArrayList(), { it -> it is Item }).map { it as Item } 395 | 396 | /** 397 | * Returns a list containing all sections matching the given [predicate]. 398 | */ 399 | inline fun filterSubsections(predicate: (Section) -> Boolean): List
{ 400 | return subsections.filterTo(ArrayList
(), predicate) 401 | } 402 | 403 | /** 404 | * A list of all subsections. 405 | */ 406 | val subsections: List
407 | get() = children.filterTo(ArrayList(), { it -> it is Section }).map { it as Section } 408 | 409 | /** 410 | * Returns a list containing all elements matching the given [predicate]. 411 | */ 412 | inline fun filterChildren(predicate: (Node) -> Boolean): List { 413 | return children.filterTo(ArrayList(), predicate) 414 | } 415 | 416 | override fun reset() { 417 | super.reset() 418 | } 419 | 420 | private fun totalItemsChanged(change: Int) { 421 | if (change == 0) return 422 | 423 | itemCount += change 424 | footer?.let { it.peersItemsBeforeThis += change } 425 | parent?.totalItemsChangedInChild(positionInParent, change) 426 | } 427 | 428 | private fun totalItemsChangedInChild(position: Int, change: Int) { 429 | if (change == 0) return 430 | 431 | // Update children after child that called in the change. 432 | children.subList(position + 1, children.size).forEach { it.peersItemsBeforeThis += change } 433 | 434 | totalItemsChanged(change) 435 | } 436 | 437 | } 438 | 439 | internal class GraphImpl() : Graph, SectionContract { 440 | internal val root: Section 441 | 442 | init { 443 | root = Section(this) 444 | } 445 | 446 | private var itemManager: ItemManager? 447 | get() = root.itemManager 448 | set(value) { 449 | root.itemManager = value 450 | root.insertItemsToAdapter() 451 | } 452 | 453 | fun init(itemManager: ItemManager) { 454 | this.itemManager = itemManager 455 | } 456 | 457 | override fun getNumSections() = root.childCount 458 | 459 | override fun getSection(position: Int): Section = root.get
(position) 460 | 461 | override fun addSection(section: Section) { 462 | root.add(section) 463 | } 464 | 465 | override fun insertSection(position: Int, section: Section) { 466 | root.insert(position, section) 467 | } 468 | 469 | override fun removeSection(position: Int): Section { 470 | val s = root.get
(position) 471 | root.remove(position) 472 | return s 473 | } 474 | 475 | override fun moveSection(from: Int, to: Int) { 476 | root.move(from, to) 477 | } 478 | 479 | var contract: SectionContract? = null 480 | 481 | override fun notifySectionInserted(section: Section) = contract?.notifySectionInserted(section) ?: -1 482 | 483 | override fun notifySectionRemoved(section: Section) { 484 | contract?.notifySectionRemoved(section) 485 | } 486 | 487 | override fun notifySectionUpdated(section: Section) { 488 | contract?.notifySectionUpdated(section) 489 | } 490 | 491 | // override fun notifySectionMoved(section: Section, toParent: Section, toPosition: Int) { 492 | // scw?.notifySectionMoved(section, toParent, toPosition) 493 | // } 494 | } 495 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/extensions.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | 6 | inline fun createParcel(crossinline create: (Parcel) -> T?) = object : Parcelable.Creator { 7 | override fun createFromParcel(source: Parcel) = create(source) 8 | override fun newArray(size: Int) = arrayOfNulls(size) 9 | } -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/graph.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim 2 | 3 | import com.tonicartos.superslim.adapter.FooterStyle 4 | import com.tonicartos.superslim.adapter.HeaderStyle 5 | import com.tonicartos.superslim.internal.SectionState 6 | 7 | inline fun T.use(block: (T) -> R): R { 8 | var done = false 9 | try { 10 | return block(this) 11 | } catch (e: Exception) { 12 | done = true 13 | this.done() 14 | throw e 15 | } finally { 16 | if (!done) { 17 | this.done() 18 | } 19 | } 20 | } 21 | 22 | // TODO: Make TrimChild version which wraps a section or attached view. 23 | interface Child { 24 | companion object { 25 | const val INVALID = 0 26 | const val ANIM_NONE = 0 27 | const val ANIM_APPEARING = 1 28 | const val ANIM_DISAPPEARING = 2 29 | } 30 | 31 | fun done() 32 | 33 | val numViews get() = 1 34 | 35 | /** 36 | * True if the child is being removed in this layout. 37 | */ 38 | val isRemoved: Boolean 39 | 40 | val measuredWidth: Int 41 | val measuredHeight: Int 42 | /** 43 | * Measure child. This should be done after [addToRecyclerView] as some views like ViewPager expect to be attached 44 | * before measurement. 45 | */ 46 | fun measure(usedWidth: Int = 0, usedHeight: Int = 0) 47 | 48 | val left: Int 49 | val top: Int 50 | val right: Int 51 | val bottom: Int 52 | /** 53 | * Layout the child. A backing view will have the dimensions specified. A subsection will have bounds defined by 54 | * left, top, and right, however will ignore bottom and may fill any remaining space to the bottom of the viewable 55 | * area. 56 | */ 57 | fun layout(left: Int, top: Int, right: Int, bottom: Int, numViewsBefore: Int = 0) 58 | 59 | /** 60 | * Fill distance dy at top of the child. The child will attempt to extend into this space; only if it is a section. 61 | * 62 | * @param dy Distance to fill. Value will be -ve. 63 | * @param left Left edge of area to fill. 64 | * @param top Top edge of area to fill. 65 | * @param right Right edge of area to fill. 66 | * @param bottom Bottom edge of area to fill. 67 | * 68 | * @return How much of dy filled. 69 | */ 70 | fun fillTop(dy: Int, left: Int, top: Int, right: Int, bottom: Int, numViewsBefore: Int = 0): Int 71 | 72 | fun trimTop(scrolled: Int, top: Int, helper: LayoutHelper, numViewsBefore: Int = 0): Int 73 | 74 | /** 75 | * Fill distance dy at bottom of the child. The child will attempt to extend into this space; only if it is a section. 76 | * 77 | * @param dy Distance to fill. Value will be +ve. 78 | * @param left Left edge of area to fill. 79 | * @param top Top edge of area to fill. 80 | * @param right Right edge of area to fill. 81 | * @param bottom Bottom edge of area to fill. 82 | * 83 | * @return How much of dy filled. 84 | */ 85 | fun fillBottom(dy: Int, left: Int, top: Int, right: Int, bottom: Int, numViewsBefore: Int = 0): Int 86 | 87 | fun trimBottom(scrolled: Int, top: Int, helper: LayoutHelper, numViewsBefore: Int = 0): Int 88 | 89 | 90 | val width: Int 91 | val height: Int 92 | val disappearedHeight: Int 93 | 94 | /** 95 | * Adds child to the recycler view. 96 | */ 97 | fun addToRecyclerView() = addToRecyclerView(-1) 98 | 99 | /** 100 | * Adds child to the recycler view. 101 | */ 102 | fun addToRecyclerView(i: Int) 103 | } 104 | 105 | /** 106 | * Configuration of a section. 107 | */ 108 | abstract class SectionConfig(gutterStart: Int = SectionConfig.DEFAULT_GUTTER, 109 | gutterEnd: Int = SectionConfig.DEFAULT_GUTTER, 110 | @HeaderStyle @JvmField var headerStyle: Int = SectionConfig.DEFAULT_HEADER_STYLE, 111 | @FooterStyle @JvmField var footerStyle: Int = SectionConfig.DEFAULT_FOOTER_STYLE, 112 | paddingStart: Int = 0, paddingTop: Int = 0, paddingEnd: Int = 0, paddingBottom: Int = 0) { 113 | var gutterStart = 0 114 | get() = field 115 | set(value) { 116 | field = if (value < 0) GUTTER_AUTO else value 117 | } 118 | var gutterEnd = 0 119 | get() = field 120 | set(value) { 121 | field = if (value < 0) GUTTER_AUTO else value 122 | } 123 | 124 | var paddingLeft = 0 125 | var paddingTop = 0 126 | var paddingRight = 0 127 | var paddingBottom = 0 128 | 129 | init { 130 | this.gutterStart = gutterStart 131 | this.gutterEnd = gutterEnd 132 | this.paddingLeft = paddingStart 133 | this.paddingTop = paddingTop 134 | this.paddingRight = paddingEnd 135 | this.paddingBottom = paddingBottom 136 | } 137 | 138 | // Remap names since internally left and right are used since section coordinates are LTR, TTB. The start and 139 | // end intention will be applied correctly (from left and right) through the config transformations. 140 | internal var gutterLeft: Int 141 | get() = gutterStart 142 | set(value) { 143 | gutterStart = value 144 | } 145 | internal var gutterRight: Int 146 | get() = gutterEnd 147 | set(value) { 148 | gutterEnd = value 149 | } 150 | 151 | internal fun makeSection(oldState: SectionState? = null) = onMakeSection(oldState) 152 | abstract protected fun onMakeSection(oldState: SectionState?): SectionState 153 | 154 | /** 155 | * Copy the configuration. Section configs are always copied when they are passed to the layout manager. 156 | */ 157 | fun copy(): SectionConfig { 158 | return onCopy() 159 | } 160 | 161 | abstract protected fun onCopy(): SectionConfig 162 | 163 | companion object { 164 | /** 165 | * Header is positioned at the head of the section content. Content starts below the header. Sticky headers 166 | * stick to the top of the layout area until the entire area has scrolled off the screen. Use HEADER_INLINE for 167 | * a header style which is otherwise the same without the sticky property. 168 | */ 169 | const val HEADER_STICKY = 1 170 | 171 | /** 172 | * Header is positioned at the head of the section content. Content starts below the header, but the header 173 | * never becomes sticky. Linear headers can not float and ignores that flag if set. 174 | */ 175 | const val HEADER_INLINE = 1 shl 1 176 | 177 | /** 178 | * Header is placed inside the gutter at the start edge of the section. This is the left for LTR locales. 179 | * Gutter headers are always sticky. 180 | */ 181 | const val HEADER_START = 1 shl 2 182 | 183 | /** 184 | * Header is placed inside the gutter at the end edge of the section. This is the right for LTR locales. 185 | * Gutter headers are always sticky. 186 | */ 187 | const val HEADER_END = 1 shl 3 188 | 189 | /** 190 | * Footer is positioned at the head of the section content. Content starts below the footer. Sticky footers 191 | * stick to the top of the layout area until the entire area has scrolled off the screen. Use FOOTER_INLINE for 192 | * a footer style which is otherwise the same without the sticky property. 193 | */ 194 | const val FOOTER_STICKY = 1 195 | 196 | /** 197 | * Footer is positioned at the head of the section content. Content starts below the footer, but the footer 198 | * never becomes sticky. Linear footers can not float and ignores that flag if set. 199 | */ 200 | const val FOOTER_INLINE = 1 shl 1 201 | 202 | /** 203 | * Footer is placed inside the gutter at the start edge of the section. This is the left for LTR locales. 204 | * Gutter footers are always sticky. 205 | */ 206 | const val FOOTER_START = 1 shl 2 207 | 208 | /** 209 | * Footer is placed inside the gutter at the end edge of the section. This is the right for LTR locales. 210 | * Gutter footers are always sticky. 211 | */ 212 | const val FOOTER_END = 1 shl 3 213 | 214 | const val GUTTER_AUTO = -1 215 | 216 | internal const val DEFAULT_GUTTER = GUTTER_AUTO 217 | internal const val DEFAULT_HEADER_STYLE = HEADER_STICKY 218 | internal const val DEFAULT_FOOTER_STYLE = FOOTER_STICKY 219 | } 220 | } 221 | 222 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/internal/config_helpers.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.internal 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.View 5 | import com.tonicartos.superslim.SectionConfig 6 | 7 | internal interface ConfigHelper : ReadWriteLayoutHelper { 8 | fun scrollBy(d: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int 9 | } 10 | 11 | //internal class LtrConfigHelper(val base: ReadWriteLayoutHelper) : ReadWriteLayoutHelper by base {} 12 | 13 | internal class RtlConfigHelper(val base: ConfigHelper) : ConfigHelper by base { 14 | override fun layout(view: View, left: Int, top: Int, right: Int, bottom: Int, marginLeft: Int, marginTop: Int, 15 | marginRight: Int, marginBottom: Int) 16 | = base.layout(view, layoutWidth - right, top, layoutWidth - left, bottom, marginRight, marginTop, 17 | marginLeft, marginBottom) 18 | 19 | override fun getLeft(child: View): Int = layoutWidth - base.getRight(child) 20 | override fun getRight(child: View): Int = layoutWidth - base.getLeft(child) 21 | 22 | override val basePaddingLeft: Int get() = base.basePaddingRight 23 | override val basePaddingRight: Int get() = base.basePaddingLeft 24 | 25 | override fun getTransformedPaddingLeft(sectionConfig: SectionConfig) = base.getTransformedPaddingRight( 26 | sectionConfig) 27 | 28 | override fun getTransformedPaddingRight(sectionConfig: SectionConfig) = base.getTransformedPaddingLeft( 29 | sectionConfig) 30 | 31 | override fun offsetChildrenHorizontal(dx: Int) = base.offsetChildrenHorizontal(-dx) 32 | 33 | override fun scrollBy(d: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State) 34 | = -base.scrollBy(-d, recycler, state) 35 | 36 | override fun toString(): String = "RtlConfigHelper(base = $base)" 37 | } 38 | 39 | internal class StackFromEndConfigHelper(val base: ConfigHelper) : ConfigHelper by base { 40 | override fun layout(view: View, left: Int, top: Int, right: Int, bottom: Int, marginLeft: Int, marginTop: Int, 41 | marginRight: Int, marginBottom: Int) 42 | = base.layout(view, left, layoutLimit - bottom, right, layoutLimit - top, marginLeft, marginTop, 43 | marginRight, marginBottom) 44 | 45 | override fun getTop(child: View): Int = layoutLimit - base.getBottom(child) 46 | override fun getBottom(child: View): Int = layoutLimit - base.getTop(child) 47 | 48 | override val basePaddingTop: Int get() = base.basePaddingBottom 49 | override val basePaddingBottom: Int get() = base.basePaddingTop 50 | 51 | override fun getTransformedPaddingTop(sectionConfig: SectionConfig) = base.getTransformedPaddingBottom( 52 | sectionConfig) 53 | 54 | override fun getTransformedPaddingBottom(sectionConfig: SectionConfig) = base.getTransformedPaddingTop( 55 | sectionConfig) 56 | 57 | override fun offsetChildrenVertical(dy: Int) = base.offsetChildrenVertical(-dy) 58 | override fun offsetVertical(view: View, dy: Int) = base.offsetVertical(view, -dy) 59 | 60 | override fun scrollBy(d: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State) 61 | = -base.scrollBy(-d, recycler, state) 62 | 63 | override fun toString(): String = "StackFromEndConfigHelper(base = $base)" 64 | } 65 | 66 | internal class ReverseLayoutConfigHelper(val base: ConfigHelper) : ConfigHelper by base { 67 | override fun layout(view: View, left: Int, top: Int, right: Int, bottom: Int, marginLeft: Int, marginTop: Int, 68 | marginRight: Int, marginBottom: Int) 69 | = base.layout(view, layoutWidth - right, layoutLimit - bottom, layoutWidth - left, layoutLimit - top, 70 | marginRight, marginTop, marginLeft, marginBottom) 71 | 72 | override fun getLeft(child: View): Int = layoutWidth - base.getRight(child) 73 | override fun getTop(child: View): Int = layoutLimit - base.getBottom(child) 74 | override fun getRight(child: View): Int = layoutWidth - base.getLeft(child) 75 | override fun getBottom(child: View): Int = layoutLimit - base.getTop(child) 76 | 77 | override val basePaddingLeft: Int get() = base.basePaddingRight 78 | override val basePaddingRight: Int get() = base.basePaddingLeft 79 | override val basePaddingTop: Int get() = base.basePaddingBottom 80 | override val basePaddingBottom: Int get() = base.basePaddingTop 81 | 82 | override fun getTransformedPaddingLeft(sectionConfig: SectionConfig) = base.getTransformedPaddingRight( 83 | sectionConfig) 84 | 85 | override fun getTransformedPaddingRight(sectionConfig: SectionConfig) = base.getTransformedPaddingLeft( 86 | sectionConfig) 87 | 88 | override fun getTransformedPaddingTop(sectionConfig: SectionConfig) = base.getTransformedPaddingBottom( 89 | sectionConfig) 90 | 91 | override fun getTransformedPaddingBottom(sectionConfig: SectionConfig) = base.getTransformedPaddingTop( 92 | sectionConfig) 93 | 94 | override fun offsetChildrenHorizontal(dx: Int) = base.offsetChildrenHorizontal(-dx) 95 | override fun offsetChildrenVertical(dy: Int) = base.offsetChildrenVertical(-dy) 96 | override fun offsetHorizontal(view: View, dx: Int) = base.offsetHorizontal(view, -dx) 97 | override fun offsetVertical(view: View, dy: Int) = base.offsetVertical(view, -dy) 98 | 99 | override fun scrollBy(d: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State) 100 | = -base.scrollBy(-d, recycler, state) 101 | 102 | override fun toString(): String = "ReverseLayoutConfigHelper(base = $base)" 103 | } 104 | 105 | //internal class VerticalConfigHelper(val base: ReadWriteLayoutHelper) : ReadWriteLayoutHelper by base {} 106 | 107 | internal class HorizontalConfigHelper(val base: ConfigHelper) : ConfigHelper by base { 108 | override fun layout(view: View, left: Int, top: Int, right: Int, bottom: Int, marginLeft: Int, marginTop: Int, 109 | marginRight: Int, marginBottom: Int) 110 | = base.layout(view, top, left, bottom, right, marginLeft, marginTop, marginRight, marginBottom) 111 | 112 | override fun getLeft(child: View): Int = base.getTop(child) 113 | override fun getTop(child: View): Int = base.getLeft(child) 114 | override fun getRight(child: View): Int = base.getBottom(child) 115 | override fun getBottom(child: View): Int = base.getRight(child) 116 | override fun measure(view: View, usedWidth: Int, usedHeight: Int) = base.measure(view, usedHeight, usedWidth) 117 | override fun getMeasuredWidth(child: View): Int = base.getMeasuredHeight(child) 118 | override fun getMeasuredHeight(child: View): Int = base.getMeasuredWidth(child) 119 | 120 | override val basePaddingTop: Int get() = base.basePaddingLeft 121 | override val basePaddingLeft: Int get() = base.basePaddingTop 122 | override val basePaddingBottom: Int get() = base.basePaddingRight 123 | override val basePaddingRight: Int get() = base.basePaddingBottom 124 | 125 | override fun getTransformedPaddingTop(sectionConfig: SectionConfig) = base.getTransformedPaddingLeft(sectionConfig) 126 | override fun getTransformedPaddingLeft(sectionConfig: SectionConfig) = base.getTransformedPaddingTop(sectionConfig) 127 | override fun getTransformedPaddingBottom(sectionConfig: SectionConfig) = base.getTransformedPaddingRight( 128 | sectionConfig) 129 | 130 | override fun getTransformedPaddingRight(sectionConfig: SectionConfig) = base.getTransformedPaddingBottom( 131 | sectionConfig) 132 | 133 | override fun offsetChildrenHorizontal(dx: Int) = base.offsetChildrenVertical(dx) 134 | override fun offsetChildrenVertical(dy: Int) = base.offsetChildrenHorizontal(dy) 135 | override fun offsetHorizontal(view: View, dx: Int) = base.offsetVertical(view, dx) 136 | override fun offsetVertical(view: View, dy: Int) = base.offsetHorizontal(view, dy) 137 | 138 | override val layoutLimit get() = base.layoutWidth 139 | override val layoutWidth get() = base.layoutLimit 140 | 141 | override fun toString(): String = "HorizontalConfigHelper(base = $base)" 142 | } 143 | 144 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/internal/extensions.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.internal 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.View 5 | import java.util.* 6 | 7 | internal val View.rvLayoutParams: RecyclerView.LayoutParams 8 | get() = layoutParams as RecyclerView.LayoutParams 9 | 10 | inline fun , I, R> T.babushka(block: T.(I) -> R): R { 11 | val item = pop() 12 | val r = block(item) 13 | push(item) 14 | return r 15 | } 16 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/internal/item_management.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.internal 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import com.tonicartos.superslim.AdapterContract 5 | 6 | internal interface ItemManagement { 7 | fun applyChanges(adapter: AdapterContract<*>, graph: GraphManager, recycler: RecyclerView.Recycler) 8 | fun addItems(start: Int, count: Int) 9 | fun removeItems(start: Int, count: Int) 10 | fun moveItems(from: Int, to: Int, count: Int) 11 | } 12 | 13 | /** 14 | * 15 | */ 16 | internal class ItemManager : ItemManagement { 17 | private val changes = ArrayList() 18 | 19 | override fun applyChanges(adapter: AdapterContract<*>, graph: GraphManager, recycler: RecyclerView.Recycler) { 20 | changes.forEach { it.apply(adapter, graph, recycler) } 21 | changes.clear() 22 | } 23 | 24 | override fun addItems(start: Int, count: Int) { 25 | changes.add(Add.acquire(start, count)) 26 | } 27 | 28 | override fun removeItems(start: Int, count: Int) { 29 | changes.add(Remove.acquire(start, count)) 30 | } 31 | 32 | override fun moveItems(from: Int, to: Int, count: Int) { 33 | changes.add(Move.acquire(from, to, count)) 34 | } 35 | } 36 | 37 | private interface Op { 38 | fun apply(adapter: AdapterContract<*>, graph: GraphManager, recycler: RecyclerView.Recycler) 39 | } 40 | 41 | private data class Add(var start: Int, var count: Int) : Op { 42 | companion object { 43 | private val pool = arrayListOf() 44 | 45 | fun acquire(start: Int, count: Int) = 46 | if (pool.isEmpty()) { 47 | Add(start, count) 48 | } else { 49 | pool.removeAt(0).apply { 50 | this.start = start 51 | this.count = count 52 | } 53 | } 54 | 55 | fun release(obj: Add) { 56 | pool.add(obj) 57 | } 58 | } 59 | 60 | override fun apply(adapter: AdapterContract<*>, graph: GraphManager, recycler: RecyclerView.Recycler) { 61 | var currentSection = 0 62 | var currentStart = 0 63 | var currentCount = 0 64 | // Handle in chunks per section. 65 | for (i in start until start + count) { 66 | val about = adapter.getData(recycler.convertPreLayoutPositionToPostLayout(i)) 67 | when { 68 | about.isHeader -> graph.addHeader(about.section) 69 | about.isFooter -> graph.addFooter(about.section) 70 | about.section == currentSection -> currentCount += 1 71 | else -> { 72 | graph.addItems(currentSection, currentStart, currentCount) 73 | currentSection = about.section 74 | currentStart = about.position 75 | currentCount = 1 76 | } 77 | } 78 | 79 | if (i == start + count - 1) { 80 | graph.addItems(currentSection, currentStart, currentCount) 81 | } 82 | } 83 | release(this) 84 | } 85 | } 86 | 87 | private data class Remove(var start: Int, var count: Int) : Op { 88 | companion object { 89 | private val pool = arrayListOf() 90 | 91 | fun acquire(start: Int, count: Int) = 92 | if (pool.isEmpty()) { 93 | Remove(start, count) 94 | } else { 95 | pool.removeAt(0).apply { 96 | this.start = start 97 | this.count = count 98 | } 99 | } 100 | 101 | fun release(obj: Remove) { 102 | pool.add(obj) 103 | } 104 | } 105 | 106 | override fun apply(adapter: AdapterContract<*>, graph: GraphManager, recycler: RecyclerView.Recycler) { 107 | graph.root.removeItems(start, count) 108 | release(this) 109 | } 110 | } 111 | 112 | private data class Move(var from: Int, var to: Int, var count: Int) : Op { 113 | companion object { 114 | private val pool = arrayListOf() 115 | 116 | fun acquire(from: Int, to: Int, count: Int) = 117 | if (pool.isEmpty()) { 118 | Move(from, to, count) 119 | } else { 120 | pool.removeAt(0).apply { 121 | this.from = from 122 | this.to = to 123 | this.count = count 124 | } 125 | } 126 | 127 | fun release(obj: Move) { 128 | pool.add(obj) 129 | } 130 | } 131 | 132 | override fun apply(adapter: AdapterContract<*>, graph: GraphManager, recycler: RecyclerView.Recycler) { 133 | 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/internal/layout/generic.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.internal.layout 2 | 3 | import com.tonicartos.superslim.LayoutHelper 4 | import com.tonicartos.superslim.SectionLayoutManager 5 | import com.tonicartos.superslim.internal.SectionState 6 | 7 | /** 8 | * A Slm that does nothing. Used as a Flm or a Hlm for when there is no footer or header. 9 | */ 10 | internal object DoNothingSlm : SectionLayoutManager { 11 | override fun isAtTop(section: SectionState, layoutState: SectionState.LayoutState) = section.atTop 12 | 13 | override fun onLayout(helper: LayoutHelper, section: SectionState, layoutState: SectionState.LayoutState) { 14 | section.layout(helper, section.leftGutter { 0 }, 0, helper.layoutWidth - section.rightGutter { 0 }) 15 | layoutState.disappearedOrRemovedHeight += section.disappearedHeight 16 | layoutState.bottom = section.height 17 | layoutState.headPosition = 0 18 | layoutState.tailPosition = 0 19 | } 20 | 21 | override fun onFillTop(dy: Int, helper: LayoutHelper, section: SectionState, 22 | layoutState: SectionState.LayoutState): Int { 23 | val filled = Math.min(dy, section.fillTop(dy, section.leftGutter { 0 }, 0, 24 | helper.layoutWidth - section.rightGutter { 0 }, helper)) 25 | layoutState.bottom = section.height 26 | layoutState.headPosition = 0 27 | layoutState.tailPosition = 0 28 | return filled 29 | } 30 | 31 | override fun onFillBottom(dy: Int, helper: LayoutHelper, section: SectionState, 32 | layoutState: SectionState.LayoutState): Int { 33 | val filled = section.fillBottom(dy, section.leftGutter { 0 }, layoutState.bottom - section.height, 34 | helper.layoutWidth - section.rightGutter { 0 }, helper) 35 | layoutState.bottom = section.height 36 | layoutState.headPosition = 0 37 | layoutState.tailPosition = 0 38 | return filled 39 | } 40 | 41 | override fun onTrimTop(scrolled: Int, helper: LayoutHelper, section: SectionState, 42 | layoutState: SectionState.LayoutState) 43 | = section.trimTop(scrolled, 0, helper).also { layoutState.bottom = section.height } 44 | 45 | override fun onTrimBottom(scrolled: Int, helper: LayoutHelper, section: SectionState, 46 | layoutState: SectionState.LayoutState) 47 | = section.trimBottom(scrolled, 0, helper).also { layoutState.bottom = section.height } 48 | } -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/internal/layout/header.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.internal.layout 2 | 3 | import com.tonicartos.superslim.LayoutHelper 4 | import com.tonicartos.superslim.SectionConfig 5 | import com.tonicartos.superslim.SectionLayoutManager 6 | import com.tonicartos.superslim.internal.SectionState 7 | import com.tonicartos.superslim.internal.SectionState.HeaderLayoutState 8 | import com.tonicartos.superslim.internal.SectionState.LayoutState 9 | import com.tonicartos.superslim.internal.insetStickyStart 10 | import com.tonicartos.superslim.use 11 | 12 | private const val ABSENT = 1 shl 0 13 | private const val ADDED = 1 shl 1 14 | private const val FLOATING = 1 shl 2 15 | 16 | internal object HeaderLayoutManager : SectionLayoutManager { 17 | override fun isAtTop(section: SectionState, layoutState: LayoutState) 18 | = selectHeaderLayout(section).isAtTop(section, layoutState) 19 | 20 | override fun onLayout(helper: LayoutHelper, section: SectionState, layoutState: LayoutState) 21 | = selectHeaderLayout(section).onLayout(helper, section, layoutState) 22 | 23 | override fun onFillTop(dy: Int, helper: LayoutHelper, section: SectionState, layoutState: LayoutState) 24 | = selectHeaderLayout(section).onFillTop(dy, helper, section, layoutState) 25 | 26 | override fun onFillBottom(dy: Int, helper: LayoutHelper, section: SectionState, layoutState: LayoutState) 27 | = selectHeaderLayout(section).onFillBottom(dy, helper, section, layoutState) 28 | 29 | override fun onTrimTop(scrolled: Int, helper: LayoutHelper, section: SectionState, layoutState: LayoutState) 30 | = selectHeaderLayout(section).onTrimTop(scrolled, helper, section, layoutState) 31 | 32 | override fun onTrimBottom(scrolled: Int, helper: LayoutHelper, section: SectionState, layoutState: LayoutState) 33 | = selectHeaderLayout(section).onTrimBottom(scrolled, helper, section, layoutState) 34 | 35 | private fun selectHeaderLayout(section: SectionState): SectionLayoutManager { 36 | return DoNothingSlm.takeUnless { section.hasHeader } ?: when (section.baseConfig.headerStyle) { 37 | SectionConfig.HEADER_INLINE -> InlineHlm 38 | SectionConfig.HEADER_START, SectionConfig.HEADER_END -> GutterHlm 39 | else -> StickyHlm 40 | } 41 | } 42 | } 43 | 44 | private interface BaseHlm : SectionLayoutManager { 45 | override fun isAtTop(section: SectionState, layoutState: LayoutState) 46 | = layoutState.overdraw == 0 && layoutState.headPosition <= 0 && section.atTop 47 | } 48 | 49 | private object InlineHlm : BaseHlm { 50 | override fun onLayout(helper: LayoutHelper, section: SectionState, layoutState: LayoutState) { 51 | val state = layoutState as HeaderLayoutState 52 | var y = -state.overdraw 53 | if (state.headPosition <= 0) { 54 | helper.getHeader(section)?.use { header -> 55 | header.addToRecyclerView() 56 | header.measure() 57 | header.layout(0, y, header.measuredWidth, y + header.measuredHeight) 58 | if (helper.isPreLayout && header.isRemoved) helper.addIgnoredHeight(header.height) 59 | state.disappearedOrRemovedHeight += header.disappearedHeight 60 | y += header.height 61 | helper.filledArea += header.height 62 | state.mode = ADDED 63 | state.headPosition = 0 64 | state.tailPosition = 0 65 | } 66 | } else { 67 | state.mode = ABSENT 68 | state.headPosition = 1 69 | } 70 | 71 | section.layout(helper, section.leftGutter { 0 }, y, helper.layoutWidth - section.rightGutter { 0 }, 72 | if (state.mode == ADDED) 1 else 0) 73 | state.disappearedOrRemovedHeight += section.disappearedHeight 74 | y += section.height 75 | helper.filledArea += section.height 76 | if (section.numViews > 0) state.tailPosition = 1 77 | 78 | state.bottom = y 79 | } 80 | 81 | override fun onFillTop(dy: Int, helper: LayoutHelper, section: SectionState, layoutState: LayoutState): Int { 82 | val state = layoutState as HeaderLayoutState 83 | if (state.headPosition < 0) state.headPosition = 1 84 | 85 | if (state.headPosition == 1) { 86 | // Must fill section children first. 87 | state.overdraw += section.fillTop(Math.max(0, dy - state.overdraw), section.leftGutter { 0 }, 88 | 0, helper.layoutWidth - section.rightGutter { 0 }, helper) 89 | state.tailPosition = 1 90 | 91 | // Fill header if there is space left. 92 | if (dy - state.overdraw > 0) { 93 | helper.getHeader(section)?.use { header -> 94 | header.addToRecyclerView(0) 95 | header.measure() 96 | state.overdraw += header.fillTop(dy - state.overdraw, 0, 97 | -state.overdraw - header.measuredHeight, 98 | header.measuredWidth, 99 | -state.overdraw) 100 | state.mode = ADDED 101 | state.headPosition = 0 102 | } 103 | } 104 | } 105 | val filled = Math.min(dy, state.overdraw) 106 | state.overdraw -= filled 107 | state.bottom += filled 108 | return filled 109 | } 110 | 111 | override fun onFillBottom(dy: Int, helper: LayoutHelper, section: SectionState, layoutState: LayoutState): Int { 112 | val state = layoutState as HeaderLayoutState 113 | 114 | var filled = if (state.tailPosition == 0) Math.max(0, state.bottom - helper.layoutLimit) else 0 115 | if (state.headPosition < 0) { 116 | helper.getHeader(section)?.use { header -> 117 | header.addToRecyclerView() 118 | header.measure() 119 | filled += header.fillBottom(dy, 0, -state.overdraw, header.measuredWidth, 120 | -state.overdraw + header.measuredHeight) 121 | state.bottom += header.height 122 | state.mode = ADDED 123 | state.headPosition = 0 124 | } 125 | } 126 | 127 | val before = section.height 128 | filled += section.fillBottom(dy - filled, section.leftGutter { 0 }, state.bottom - section.height, 129 | helper.layoutWidth - section.rightGutter { 0 }, helper, 130 | if (state.mode == ADDED) 1 else 0) 131 | state.bottom += section.height - before 132 | state.tailPosition = 1 133 | return Math.min(dy, filled) 134 | } 135 | 136 | override fun onTrimTop(scrolled: Int, helper: LayoutHelper, section: SectionState, layoutState: LayoutState): Int { 137 | val state = layoutState as HeaderLayoutState 138 | var removedHeight = 0 139 | var contentTop = 0 140 | if (state.mode == ADDED) { 141 | helper.getAttachedViewAt(0) { header -> 142 | if (header.bottom < 0) { 143 | header.remove() 144 | removedHeight += Math.max(0, header.height - state.overdraw) 145 | state.overdraw = Math.max(0, state.overdraw - header.height) 146 | state.headPosition = 1 147 | state.mode = ABSENT 148 | } else if (header.top < 0) { 149 | val before = state.overdraw 150 | state.overdraw = -header.top 151 | removedHeight += state.overdraw - before 152 | contentTop = header.bottom 153 | } 154 | } 155 | } 156 | removedHeight += section.trimTop(scrolled, contentTop, helper, if (state.mode == ADDED) 1 else 0) 157 | if (helper.numViews == 0) { 158 | state.headPosition = -1 159 | state.headPosition = -1 160 | } 161 | state.bottom -= removedHeight 162 | return removedHeight 163 | } 164 | 165 | override fun onTrimBottom(scrolled: Int, helper: LayoutHelper, section: SectionState, 166 | layoutState: LayoutState): Int { 167 | val state = layoutState as HeaderLayoutState 168 | var removed = 0 169 | removed += section.trimBottom(scrolled, state.bottom - section.height, helper, 170 | if (state.mode == ADDED) 1 else 0) 171 | if (section.numViews == 0) { 172 | state.tailPosition = 0 173 | } 174 | if (state.mode == ADDED) { 175 | helper.getAttachedViewAt(0) { header -> 176 | if (header.top >= helper.layoutLimit) { 177 | removed += header.height 178 | header.remove() 179 | state.mode = ABSENT 180 | } 181 | } 182 | } 183 | if (helper.numViews == 0) { 184 | state.headPosition = -1 185 | state.tailPosition = -1 186 | } 187 | state.bottom -= removed 188 | return removed 189 | } 190 | } 191 | 192 | // Sticky headers are always attached after content. 193 | private object StickyHlm : BaseHlm { 194 | override fun onLayout(helper: LayoutHelper, section: SectionState, layoutState: LayoutState) { 195 | val state = layoutState as HeaderLayoutState 196 | var y = -state.overdraw 197 | 198 | helper.getHeader(section)?.use { header -> 199 | header.addToRecyclerView() 200 | header.measure() 201 | header.layout(0, y, header.measuredWidth, y + header.measuredHeight) 202 | 203 | if (state.headPosition <= 0) { 204 | if (helper.isPreLayout && header.isRemoved) helper.addIgnoredHeight(header.height) 205 | state.disappearedOrRemovedHeight += header.disappearedHeight 206 | y += header.height 207 | helper.filledArea += header.height 208 | state.mode = ADDED 209 | state.headPosition = 0 210 | state.tailPosition = 0 211 | } 212 | 213 | helper.insetStickyStart(header.measuredHeight) { 214 | section.layout(helper, section.leftGutter { 0 }, y, 215 | helper.layoutWidth - section.rightGutter { 0 }) 216 | state.disappearedOrRemovedHeight += section.disappearedHeight 217 | y += section.height 218 | helper.filledArea += section.height 219 | state.headPosition = 0 220 | state.tailPosition = 0 221 | } 222 | 223 | if (state.mode == ABSENT) { 224 | 225 | } 226 | 227 | // Detect and adjust positioning to sticky or not. 228 | var bottom = y + header.measuredHeight 229 | val limit = helper.layoutLimit - helper.stickyStartInset 230 | val floatOffset = if (bottom > limit) limit - bottom else 0 231 | bottom += floatOffset 232 | 233 | header.layout(0, bottom - header.measuredHeight, header.measuredWidth, bottom, helper.numViews) 234 | 235 | // 100% floating header has 0 height. 236 | // val floatAdjustedHeight = Math.max(0, header.height + floatOffset) 237 | if (helper.isPreLayout && header.isRemoved) helper.addIgnoredHeight(header.height) 238 | helper.filledArea += header.height 239 | state.mode = FLOATING 240 | if (state.headPosition < 0) state.headPosition = 1 241 | state.tailPosition = 1 242 | y += header.height 243 | if (y < helper.layoutLimit) { 244 | state.mode = ADDED 245 | } 246 | } 247 | 248 | state.bottom = y 249 | } 250 | 251 | override fun onFillTop(dy: Int, helper: LayoutHelper, section: SectionState, layoutState: LayoutState): Int { 252 | TODO("not implemented") 253 | } 254 | 255 | override fun onFillBottom(dy: Int, helper: LayoutHelper, section: SectionState, layoutState: LayoutState): Int { 256 | TODO("not implemented") 257 | } 258 | 259 | override fun onTrimTop(scrolled: Int, helper: LayoutHelper, 260 | section: SectionState, 261 | layoutState: LayoutState): Int { 262 | TODO("not implemented") 263 | } 264 | 265 | override fun onTrimBottom(scrolled: Int, helper: LayoutHelper, 266 | section: SectionState, 267 | layoutState: LayoutState): Int { 268 | TODO("not implemented") 269 | } 270 | } 271 | 272 | private object GutterHlm : BaseHlm { 273 | override fun isAtTop(section: SectionState, layoutState: LayoutState) = section.atTop 274 | 275 | override fun onLayout(helper: LayoutHelper, section: SectionState, layoutState: LayoutState) { 276 | TODO("not implemented") 277 | } 278 | 279 | override fun onFillTop(dy: Int, helper: LayoutHelper, section: SectionState, layoutState: LayoutState): Int { 280 | TODO("not implemented") 281 | } 282 | 283 | override fun onFillBottom(dy: Int, helper: LayoutHelper, section: SectionState, layoutState: LayoutState): Int { 284 | TODO("not implemented") 285 | } 286 | 287 | override fun onTrimTop(scrolled: Int, helper: LayoutHelper, 288 | section: SectionState, 289 | layoutState: LayoutState): Int { 290 | TODO("not implemented") 291 | } 292 | 293 | override fun onTrimBottom(scrolled: Int, helper: LayoutHelper, 294 | section: SectionState, 295 | layoutState: LayoutState): Int { 296 | TODO("not implemented") 297 | } 298 | } -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/internal/layout/padding.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.internal.layout 2 | 3 | import com.tonicartos.superslim.LayoutHelper 4 | import com.tonicartos.superslim.SectionLayoutManager 5 | import com.tonicartos.superslim.internal.SectionState 6 | import com.tonicartos.superslim.internal.SectionState.LayoutState 7 | import com.tonicartos.superslim.internal.SectionState.PaddingLayoutState 8 | import com.tonicartos.superslim.internal.SectionState.PaddingLayoutState.Companion.BOTTOM_ADDED 9 | import com.tonicartos.superslim.internal.SectionState.PaddingLayoutState.Companion.TOP_ADDED 10 | 11 | internal object PaddingLayoutManager : SectionLayoutManager { 12 | override fun isAtTop(section: SectionState, layoutState: LayoutState): Boolean { 13 | val state = layoutState as PaddingLayoutState 14 | return state.overdraw == 0 && state.paddingTop > 0 || section.atTop 15 | } 16 | 17 | override fun onLayout(helper: LayoutHelper, section: SectionState, layoutState: LayoutState) { 18 | val state = layoutState as PaddingLayoutState 19 | state.paddingTop = helper.paddingTop 20 | state.paddingBottom = helper.paddingBottom 21 | 22 | if (state.paddingTop > 0) { 23 | if (state.onScreen && state flagUnset TOP_ADDED && state.overdraw > 0) { 24 | // Must be in a layout pass with requested position. 25 | state set TOP_ADDED 26 | } else if (!state.onScreen) { 27 | state.overdraw = 0 28 | state set TOP_ADDED 29 | } 30 | } 31 | state.onScreen = true 32 | 33 | var y = if (state flagSet TOP_ADDED) state.paddingTop - state.overdraw else 0 34 | 35 | section.layout(helper, section.leftGutter { 0 }, y, helper.layoutWidth - section.rightGutter { 0 }) 36 | state.disappearedOrRemovedHeight += section.disappearedHeight 37 | y += section.height 38 | helper.filledArea += section.height 39 | 40 | if (state.paddingBottom > 0 && y < helper.layoutLimit) { 41 | state set BOTTOM_ADDED 42 | y += helper.paddingBottom 43 | } 44 | 45 | state.bottom = y 46 | } 47 | 48 | override fun onFillTop(dy: Int, helper: LayoutHelper, section: SectionState, layoutState: LayoutState): Int { 49 | val state = layoutState as PaddingLayoutState 50 | 51 | var toFill = dy - state.overdraw 52 | 53 | if (state.paddingBottom > 0 && !state.onScreen) { 54 | state.paddingTop = helper.paddingTop 55 | state.paddingBottom = helper.paddingBottom 56 | 57 | // Add bottom padding. 58 | val filled = state.paddingBottom 59 | state.overdraw += filled 60 | toFill -= filled 61 | state set BOTTOM_ADDED 62 | } 63 | state.onScreen = true 64 | 65 | // Add content. 66 | state.overdraw += section.fillTop(Math.max(0, toFill), section.leftGutter { 0 }, -state.overdraw, 67 | helper.layoutWidth - section.rightGutter { 0 }, helper) 68 | 69 | if (state.paddingTop > 0 && state flagUnset TOP_ADDED && state.overdraw < dy) { 70 | // Add top padding. 71 | state.overdraw += state.paddingTop 72 | state set TOP_ADDED 73 | } 74 | 75 | val filled = Math.min(dy, state.overdraw) 76 | state.overdraw -= filled 77 | state.bottom += filled 78 | return filled 79 | } 80 | 81 | override fun onFillBottom(dy: Int, helper: LayoutHelper, section: SectionState, layoutState: LayoutState): Int { 82 | val state = layoutState as PaddingLayoutState 83 | var filled = 0 84 | 85 | if (state.paddingTop > 0 && !state.onScreen) { 86 | state.paddingBottom = helper.paddingBottom 87 | state.paddingTop = helper.paddingTop 88 | 89 | // Add top padding. 90 | filled += state.paddingTop 91 | state.bottom = state.paddingTop 92 | state.overdraw = 0 93 | state set TOP_ADDED 94 | } 95 | state.onScreen = true 96 | 97 | val y = if (state flagSet TOP_ADDED) state.paddingTop - state.overdraw else 0 98 | 99 | // Add content 100 | val before = section.height 101 | filled += section.fillBottom(dy, section.leftGutter { 0 }, y, helper.layoutWidth - section.rightGutter { 0 }, 102 | helper) 103 | state.bottom += section.height - before 104 | 105 | if (state.paddingBottom > 0 && filled < dy) { 106 | if (state flagUnset BOTTOM_ADDED) { 107 | // Add bottom padding. 108 | filled += state.paddingBottom 109 | state.bottom += state.paddingBottom 110 | state set BOTTOM_ADDED 111 | } else { 112 | filled += Math.max(0, state.bottom - helper.layoutLimit) 113 | } 114 | } 115 | 116 | return Math.min(dy, filled) 117 | } 118 | 119 | override fun onTrimTop(scrolled: Int, helper: LayoutHelper, section: SectionState, layoutState: LayoutState): Int { 120 | val state = layoutState as PaddingLayoutState 121 | 122 | var removedHeight = 0 123 | var contentTop = 0 124 | 125 | if (state flagSet TOP_ADDED) { 126 | val before = state.overdraw 127 | state.overdraw = Math.min(state.paddingTop, state.overdraw + scrolled) 128 | removedHeight += state.overdraw - before 129 | 130 | // Do padding top. 131 | if (state.overdraw >= state.paddingTop) { 132 | state.overdraw = 0 133 | state unset TOP_ADDED 134 | } else { 135 | contentTop = state.paddingTop - state.overdraw 136 | } 137 | } 138 | 139 | removedHeight += section.trimTop(scrolled, contentTop, helper) 140 | 141 | if (helper.numViews == 0 && state flagSet BOTTOM_ADDED) { 142 | val before = state.overdraw 143 | state.overdraw = Math.min(state.paddingBottom, state.overdraw + (scrolled - removedHeight)) 144 | removedHeight += state.overdraw - before 145 | 146 | // Do padding bottom. 147 | if (state.bottom < 0) { 148 | state.overdraw = 0 149 | state unset BOTTOM_ADDED 150 | state.onScreen = false 151 | } 152 | } 153 | 154 | state.bottom -= removedHeight 155 | return removedHeight 156 | } 157 | 158 | override fun onTrimBottom(scrolled: Int, helper: LayoutHelper, section: SectionState, 159 | layoutState: LayoutState): Int { 160 | val state = layoutState as PaddingLayoutState 161 | var removedHeight = 0 162 | if (state flagSet BOTTOM_ADDED) { 163 | // Do padding bottom. 164 | if (state.bottom - state.paddingBottom > helper.layoutLimit) { 165 | removedHeight += state.paddingBottom 166 | state unset BOTTOM_ADDED 167 | } 168 | } 169 | 170 | val contentTop = if (state flagSet TOP_ADDED) state.paddingTop - state.overdraw else 0 171 | 172 | // Do content. 173 | removedHeight += section.trimBottom(scrolled - removedHeight, contentTop, helper) 174 | 175 | if (state flagSet TOP_ADDED) { 176 | // Do padding top. 177 | if (helper.layoutLimit < 0) { 178 | removedHeight += state.paddingBottom 179 | state unset TOP_ADDED 180 | state.onScreen = false 181 | } 182 | } 183 | state.bottom -= removedHeight 184 | return removedHeight 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/internal/layout_helpers.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.internal 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.View 5 | import com.tonicartos.superslim.LayoutHelper 6 | import com.tonicartos.superslim.SectionConfig 7 | import com.tonicartos.superslim.internal.SectionState.LayoutState 8 | 9 | internal class RootLayoutHelper(val manager: ManagerHelper, val config: ReadWriteLayoutHelper, 10 | val recycler: RecyclerHelper, val state: StateHelper) : 11 | BaseLayoutHelper, ManagerHelper by manager, ReadWriteLayoutHelper by config, RecyclerHelper by recycler, 12 | StateHelper by state { 13 | companion object { 14 | private var helperPool = LayoutHelperPool() 15 | } 16 | 17 | internal fun acquireSubsectionHelper(y: Int, left: Int, right: Int, paddingTop: Int, paddingBottom: Int, 18 | viewsBefore: Int, layoutState: LayoutState, 19 | tellParentViewsChangedBy: (Int) -> Unit, 20 | tellParentAboutTemporaryView: (Int) -> Unit) 21 | = helperPool.acquire(this, left, y, right - left, paddingTop, paddingBottom, viewsBefore, layoutState, 22 | tellParentViewsChangedBy, tellParentAboutTemporaryView) 23 | 24 | inline fun useSubsectionHelper(y: Int, left: Int, right: Int, paddingTop: Int, paddingBottom: Int, 25 | viewsBefore: Int, layoutState: LayoutState, block: (LayoutHelper) -> T): T { 26 | val helper = acquireSubsectionHelper(y, left, right, paddingTop, paddingBottom, viewsBefore, layoutState, 27 | {}, {}) 28 | val r = block(helper) 29 | helper.release() 30 | return r 31 | } 32 | 33 | fun releaseSubsectionHelper(helper: LayoutHelper) { 34 | helperPool.release(helper) 35 | } 36 | 37 | private var layoutLimitExtension = 0 38 | override val layoutLimit get() = config.layoutLimit + layoutLimitExtension 39 | 40 | override fun addIgnoredHeight(ignoredHeight: Int) { 41 | layoutLimitExtension += ignoredHeight 42 | } 43 | 44 | override var stickyStartInset = 0 45 | override var stickyEndInset = 0 46 | 47 | override fun toString(): String = "RootHelper(ignoredHeight = $layoutLimitExtension, layoutLimit = $layoutLimit, layoutWidth = $layoutWidth, \nconfig = $config,\nstate = $state)\n" 48 | .replace("\n", "\n\t") 49 | 50 | private class LayoutHelperPool { 51 | private val pool = arrayListOf() 52 | 53 | fun acquire(root: RootLayoutHelper, x: Int, y: Int, width: Int, paddingTop: Int, paddingBottom: Int, 54 | viewsBefore: Int, layoutState: LayoutState, tellParentViewsChangedBy: (Int) -> Unit, 55 | tellParentAboutTemporaryView: (Int) -> Unit) = 56 | if (pool.isEmpty()) { 57 | LayoutHelper(root, x, y, width, paddingTop, paddingBottom, viewsBefore, layoutState, 58 | tellParentViewsChangedBy, tellParentAboutTemporaryView) 59 | } else { 60 | pool.removeAt(0).reInit(root, x, y, width, paddingTop, paddingBottom, viewsBefore, layoutState, 61 | tellParentViewsChangedBy, tellParentAboutTemporaryView) 62 | } 63 | 64 | fun release(helper: LayoutHelper) { 65 | pool.add(helper) 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * Encapsulates the recycler into the layout helper chain. 72 | */ 73 | internal class RecyclerWrapper : RecyclerHelper { 74 | lateinit var recycler: RecyclerView.Recycler 75 | 76 | fun wrap(recycler: RecyclerView.Recycler): RecyclerWrapper { 77 | this.recycler = recycler 78 | return this 79 | } 80 | 81 | override fun getView(position: Int): View = recycler.getViewForPosition(position) 82 | 83 | override val scrap: List 84 | get() = recycler.scrapList 85 | 86 | override fun removeView(child: View, helper: LayoutHelper) { 87 | helper.removeView(child, recycler) 88 | } 89 | } 90 | 91 | /** 92 | * Encapsulates the recycler view state into the layout helper chain. 93 | */ 94 | internal class StateWrapper : StateHelper { 95 | lateinit var state: RecyclerView.State 96 | 97 | fun wrap(state: RecyclerView.State): StateWrapper { 98 | this.state = state 99 | return this 100 | } 101 | 102 | override val hasTargetScrollPosition: Boolean 103 | get() = state.hasTargetScrollPosition() 104 | 105 | override val targetScrollPosition: Int 106 | get() = state.targetScrollPosition 107 | 108 | override val willRunPredictiveAnimations: Boolean 109 | get() = state.willRunPredictiveAnimations() 110 | 111 | override val isPreLayout: Boolean 112 | get() = state.isPreLayout 113 | 114 | override val itemCount: Int 115 | get() = state.itemCount 116 | 117 | override fun toString(): String = "State(itemCount = $itemCount, isPreLayout = $isPreLayout, willRunPredictiveAnimations = $willRunPredictiveAnimations)" 118 | } 119 | 120 | /**************************************************** 121 | * This is the set of interfaces which collectively 122 | * express the hierarchy of delegates which make up 123 | * the bulk of [LayoutHelper]'s functionality. 124 | ****************************************************/ 125 | 126 | internal interface BaseLayoutHelper : ManagerHelper, StateHelper, RecyclerHelper, ReadWriteLayoutHelper { 127 | fun addIgnoredHeight(ignoredHeight: Int) 128 | } 129 | 130 | internal interface ManagerHelper { 131 | fun addView(child: View) 132 | fun addView(child: View, index: Int) 133 | fun addDisappearingView(child: View) 134 | fun addDisappearingView(child: View, index: Int) 135 | fun addTemporaryView(child: View) 136 | fun addTemporaryView(child: View, index: Int) 137 | 138 | fun removeView(child: View, recycler: RecyclerView.Recycler) 139 | 140 | val supportsPredictiveItemAnimations: Boolean 141 | } 142 | 143 | internal interface StateHelper { 144 | val isPreLayout: Boolean 145 | val willRunPredictiveAnimations: Boolean 146 | val itemCount: Int 147 | val hasTargetScrollPosition: Boolean 148 | val targetScrollPosition: Int 149 | } 150 | 151 | internal interface RecyclerHelper { 152 | fun getView(position: Int): View 153 | val scrap: List 154 | fun removeView(child: View, helper: LayoutHelper) 155 | } 156 | 157 | internal interface ReadWriteLayoutHelper : ReadLayoutHelper, WriteLayoutHelper { 158 | val basePaddingLeft: Int 159 | val basePaddingTop: Int 160 | val basePaddingRight: Int 161 | val basePaddingBottom: Int 162 | 163 | fun attachViewToPosition(position: Int, view: View) 164 | fun detachViewAtPosition(position: Int): View? 165 | 166 | fun getTransformedPaddingLeft(sectionConfig: SectionConfig): Int 167 | fun getTransformedPaddingTop(sectionConfig: SectionConfig): Int 168 | fun getTransformedPaddingRight(sectionConfig: SectionConfig): Int 169 | fun getTransformedPaddingBottom(sectionConfig: SectionConfig): Int 170 | } 171 | 172 | class AttachedView private constructor(private var view: View, private var helper: LayoutHelper) { 173 | companion object { 174 | private val pool = ArrayList() 175 | internal fun wrap(view: View, helper: LayoutHelper) = if (pool.size >= 1) pool.removeAt(0).apply { 176 | this.view = view 177 | this.helper = helper 178 | } else AttachedView(view, helper) 179 | } 180 | 181 | fun done() { 182 | pool.add(this) 183 | } 184 | 185 | val left get() = helper.getLeft(this.view) 186 | val top get() = helper.getTop(this.view) 187 | val right get() = helper.getRight(this.view) 188 | val bottom get() = helper.getBottom(this.view) 189 | val width get() = helper.getMeasuredWidth(this.view) 190 | val height get() = helper.getMeasuredHeight(this.view) 191 | 192 | fun remove() = helper.removeView(this.view, helper) 193 | fun offsetTopAndBottom(offset: Int) = helper.offsetVertical(this.view, offset) 194 | fun offsetLeftAndRight(offset: Int) = helper.offsetHorizontal(this.view, offset) 195 | } 196 | 197 | internal interface ReadLayoutHelper { 198 | fun getLeft(child: View): Int 199 | fun getTop(child: View): Int 200 | fun getRight(child: View): Int 201 | fun getBottom(child: View): Int 202 | fun getMeasuredWidth(child: View): Int 203 | fun getMeasuredHeight(child: View): Int 204 | 205 | fun getAttachedRawView(position: Int): View 206 | 207 | /** 208 | * Width of the layout area. 209 | */ 210 | val layoutWidth: Int 211 | 212 | /** 213 | * Y limit that constrains the layout. This is used to know when to stop laying out items, and is nominally the 214 | * maximum height of the visible layout area. 215 | * 216 | * **Warning**: This value can change, and as such, should not be stored. 217 | */ 218 | val layoutLimit: Int 219 | var stickyStartInset: Int 220 | var stickyEndInset: Int 221 | } 222 | 223 | internal inline fun ReadLayoutHelper.insetStickyStart(inset: Int, block: () -> R): R { 224 | stickyStartInset += inset 225 | val r = block() 226 | stickyStartInset -= inset 227 | return r 228 | } 229 | 230 | internal inline fun ReadLayoutHelper.insetStickyEnd(inset: Int, block: () -> R): R { 231 | stickyEndInset += inset 232 | val r = block() 233 | stickyEndInset -= inset 234 | return r 235 | } 236 | 237 | internal interface WriteLayoutHelper { 238 | fun measure(view: View, usedWidth: Int = 0, usedHeight: Int = 0) 239 | fun layout(view: View, left: Int, top: Int, right: Int, bottom: Int, marginLeft: Int = 0, marginTop: Int = 0, 240 | marginRight: Int = 0, marginBottom: Int = 0) 241 | 242 | fun offsetVertical(view: View, dy: Int) 243 | fun offsetHorizontal(view: View, dx: Int) 244 | fun offsetChildrenVertical(dy: Int) 245 | fun offsetChildrenHorizontal(dx: Int) 246 | } 247 | 248 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/layout.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.View 5 | import com.tonicartos.superslim.internal.AttachedView 6 | import com.tonicartos.superslim.internal.BaseLayoutHelper 7 | import com.tonicartos.superslim.internal.RootLayoutHelper 8 | import com.tonicartos.superslim.internal.SectionState 9 | import com.tonicartos.superslim.internal.SectionState.LayoutState 10 | 11 | interface SectionLayoutManager { 12 | 13 | /** 14 | * Check if the section is at the top. 15 | */ 16 | fun isAtTop(section: T, layoutState: LayoutState): Boolean 17 | 18 | /** 19 | * Layout the section. Layout pass may be pre, post, or normal. 20 | * 21 | * @param helper Layout helper. 22 | * @param section The section to lay out. 23 | * @param layoutState In/out layout state. 24 | */ 25 | fun onLayout(helper: LayoutHelper, section: T, layoutState: LayoutState) 26 | 27 | /** 28 | * Fill distance dy at top of the section. The layout state may already contain some filled distance recorded as 29 | * overdraw. 30 | * 31 | * @param dy Distance to fill. 32 | * @param helper Layout helper. 33 | * @param section Section to fill. 34 | * @param layoutState In/out layout state. 35 | * 36 | * @return How much filled. Valid values are 0 to dy. 37 | */ 38 | fun onFillTop(dy: Int, helper: LayoutHelper, section: T, layoutState: LayoutState): Int 39 | 40 | /** 41 | * Fill distance dy at bottom of the section. The layout state may already contain some filled distance recorded as 42 | * overdraw. 43 | * 44 | * @param dy Distance to fill. 45 | * @param helper Layout helper. 46 | * @param section Section to fill. 47 | * @param layoutState In/out layout state. 48 | * 49 | * @return How much filled. Valid values are 0 to dy. 50 | */ 51 | fun onFillBottom(dy: Int, helper: LayoutHelper, section: T, layoutState: LayoutState): Int 52 | 53 | /** 54 | * Remove views managed by this SectionLayoutManager that are before 0. Remember to update layoutState values accordingly. 55 | * 56 | * @return Height removed from section. 57 | */ 58 | fun onTrimTop(scrolled: Int, helper: LayoutHelper, section: T, layoutState: LayoutState): Int 59 | 60 | /** 61 | * Remove views managed by this SectionLayoutManager that are after [LayoutHelper.layoutLimit]. Remember to update 62 | * state values appropriately. 63 | * 64 | * @return Height removed from section. 65 | */ 66 | fun onTrimBottom(scrolled: Int, helper: LayoutHelper, section: T, layoutState: LayoutState): Int 67 | } 68 | 69 | class LayoutHelper private constructor(private var root: RootLayoutHelper, 70 | private var tellParentViewsChangedBy: (Int) -> Unit, 71 | private var tellParentAboutTemporaryView: (Int) -> Unit) : BaseLayoutHelper { 72 | internal constructor(root: RootLayoutHelper, x: Int, y: Int, width: Int, paddingTop: Int, paddingBottom: Int, 73 | viewsBefore: Int, layoutState: LayoutState, tellParentViewsChanged: (Int) -> Unit, 74 | tellParentAboutTemporaryView: (Int) -> Unit) : 75 | this(root, tellParentViewsChanged, tellParentAboutTemporaryView) { 76 | offset.x = x 77 | offset.y = y 78 | this.width = width 79 | this.paddingTop = paddingTop 80 | this.paddingBottom = paddingBottom 81 | this.viewsBefore = viewsBefore 82 | this.layoutState = layoutState 83 | } 84 | 85 | /************************* 86 | * Init stuff 87 | *************************/ 88 | 89 | internal fun reInit(root: RootLayoutHelper, x: Int, y: Int, width: Int, paddingTop: Int, paddingBottom: Int, 90 | viewsBefore: Int, layoutState: LayoutState, tellParentViewsChangedBy: (Int) -> Unit, 91 | tellParentAboutDisappearedView: (Int) -> Unit) 92 | : LayoutHelper { 93 | this.root = root 94 | offset.x = x 95 | offset.y = y 96 | this.width = width 97 | filledArea = 0 98 | this.paddingTop = paddingTop 99 | this.paddingBottom = paddingBottom 100 | this.viewsBefore = viewsBefore 101 | this.layoutState = layoutState 102 | this.tellParentViewsChangedBy = tellParentViewsChangedBy 103 | this.tellParentAboutTemporaryView = tellParentAboutDisappearedView 104 | return this 105 | } 106 | 107 | private fun acquireSubsectionHelper(y: Int, left: Int, right: Int, paddingTop: Int, paddingBottom: Int, 108 | viewsBefore: Int, layoutState: LayoutState): LayoutHelper 109 | = root.acquireSubsectionHelper(offset.y + y, offset.x + left, offset.x + right, 110 | paddingTop, paddingBottom, viewsBefore, layoutState, 111 | this::viewsChangedBy, this::temporaryViewsChangedBy) 112 | 113 | internal fun release() { 114 | root.releaseSubsectionHelper(this) 115 | } 116 | 117 | internal inline fun useSubsectionHelper(y: Int, left: Int, right: Int, paddingTop: Int, paddingBottom: Int, 118 | viewsBefore: Int, layoutState: LayoutState, 119 | block: (LayoutHelper) -> T): T { 120 | val helper = acquireSubsectionHelper(y, left, right, paddingTop, paddingBottom, viewsBefore, layoutState) 121 | val r = block(helper) 122 | helper.release() 123 | return r 124 | } 125 | 126 | /************************* 127 | * layout stuff 128 | *************************/ 129 | 130 | private var offset = Offset() 131 | private var width = 0 132 | 133 | internal var paddingTop = 0 134 | private set 135 | internal var paddingBottom = 0 136 | private set 137 | 138 | internal var viewsBefore = 0 139 | private set 140 | 141 | private lateinit var layoutState: LayoutState 142 | 143 | var numViews get() = layoutState.numViews 144 | private set(value) { 145 | viewsChangedBy(value - layoutState.numViews) 146 | } 147 | 148 | var numTemporaryViews get() = layoutState.numTemporaryViews 149 | private set(value) { 150 | temporaryViewsChangedBy(value - layoutState.numTemporaryViews) 151 | } 152 | 153 | internal fun viewsChangedBy(delta: Int) { 154 | layoutState.numViews += delta 155 | tellParentViewsChangedBy(delta) 156 | } 157 | 158 | private fun temporaryViewsChangedBy(delta: Int) { 159 | layoutState.numViews += delta 160 | layoutState.numTemporaryViews += delta 161 | tellParentAboutTemporaryView(delta) 162 | } 163 | 164 | override val layoutWidth get() = width 165 | override val layoutLimit get() = root.layoutLimit - offset.y 166 | 167 | override fun getLeft(child: View): Int = root.getLeft(child) - offset.x 168 | override fun getRight(child: View): Int = root.getRight(child) - offset.x 169 | override fun getTop(child: View): Int = root.getTop(child) - offset.y 170 | override fun getBottom(child: View): Int = root.getBottom(child) - offset.y 171 | 172 | override fun layout(view: View, left: Int, top: Int, right: Int, bottom: Int, 173 | marginLeft: Int, marginTop: Int, marginRight: Int, marginBottom: Int) { 174 | root.layout(view, offset.x + left, offset.y + top, offset.x + right, offset.y + bottom, 175 | marginLeft, marginTop, marginRight, marginBottom) 176 | } 177 | 178 | override fun measure(view: View, usedWidth: Int, usedHeight: Int) { 179 | root.measure(view, usedWidth + root.layoutWidth - width, usedHeight + offset.y) 180 | } 181 | 182 | var filledArea: Int = 0 183 | 184 | private val willCheckForDisappearedItems get() = !isPreLayout && willRunPredictiveAnimations && supportsPredictiveItemAnimations 185 | 186 | internal fun getHeader(section: SectionState): Child? { 187 | return if (filledArea >= layoutLimit && willCheckForDisappearedItems && section.hasDisappearedItemsToLayOut) { 188 | section.getDisappearingHeader(this) 189 | } else { 190 | section.getHeader(this) 191 | } 192 | } 193 | 194 | internal fun getFooter(section: SectionState): Child? { 195 | return if (filledArea >= layoutLimit && willCheckForDisappearedItems && section.hasDisappearedItemsToLayOut) { 196 | section.getDisappearingFooter(this) 197 | } else { 198 | section.getFooter(this) 199 | } 200 | } 201 | 202 | fun getUnfinishedChild(position: Int, section: SectionState) = section.getNonFinalChildAt(this, position) 203 | 204 | fun getChild(currentPosition: Int, section: SectionState): Child? { 205 | if (currentPosition < section.numChildren) { 206 | if (filledArea >= layoutLimit && willCheckForDisappearedItems && section.hasDisappearedItemsToLayOut) { 207 | return section.getDisappearingChildAt(this, currentPosition) 208 | } else { 209 | return section.getChildAt(this, currentPosition) 210 | } 211 | } 212 | return null 213 | } 214 | 215 | fun moreToLayout(currentPosition: Int, section: SectionState): Boolean { 216 | if (currentPosition < section.numChildren) { 217 | if (filledArea >= layoutLimit) { 218 | return willCheckForDisappearedItems && section.hasDisappearedItemsToLayOut 219 | } 220 | return true 221 | } 222 | return false 223 | } 224 | 225 | private val SectionState.hasDisappearedItemsToLayOut: Boolean get() { 226 | return scrap.any { it in this } 227 | } 228 | 229 | internal fun scrapHasPosition(position: Int): Boolean { 230 | return scrap.any { it.layoutPosition == position } 231 | } 232 | 233 | override fun getView(position: Int) = root.getView(position) 234 | 235 | override fun removeView(child: View, recycler: RecyclerView.Recycler) { 236 | root.removeView(child, recycler) 237 | numViews -= 1 238 | } 239 | 240 | override fun addView(child: View) { 241 | addView(child, -1) 242 | } 243 | 244 | override fun addView(child: View, index: Int) { 245 | val dest = if (index == -1) numViews else index 246 | root.addView(child, viewsBefore + dest) 247 | numViews += 1 248 | } 249 | 250 | override fun addDisappearingView(child: View) { 251 | addDisappearingView(child, -1) 252 | } 253 | 254 | override fun addDisappearingView(child: View, index: Int) { 255 | val dest = if (index == -1) numViews else index 256 | root.addDisappearingView(child, viewsBefore + dest) 257 | numTemporaryViews += 1 258 | } 259 | 260 | override fun addTemporaryView(child: View) { 261 | addTemporaryView(child, -1) 262 | } 263 | 264 | override fun addTemporaryView(child: View, index: Int) { 265 | val dest = if (index == -1) numViews else index 266 | root.addTemporaryView(child, viewsBefore + dest) 267 | numTemporaryViews += 1 268 | } 269 | 270 | override fun getAttachedRawView(position: Int) = root.getAttachedRawView(viewsBefore + position) 271 | 272 | fun getAttachedViewAt(position: Int) = AttachedView.wrap(getAttachedRawView(position), this) 273 | inline fun getAttachedViewAt(position: Int, block: (AttachedView) -> R): R 274 | = getAttachedViewAt(position).let { view -> block(view).also { view.done() } } 275 | 276 | override fun attachViewToPosition(position: Int, view: View) { 277 | numViews += 1 278 | root.attachViewToPosition(viewsBefore + position, view) 279 | } 280 | 281 | override fun detachViewAtPosition(position: Int): View? { 282 | numViews -= 1 283 | return root.detachViewAtPosition(viewsBefore + position) 284 | } 285 | 286 | override fun toString(): String = "SubsectionHelper($offset, width = $width, limit = $layoutLimit, views before = $viewsBefore, root = \n$root)".replace( 287 | "\n", "\n\t") 288 | 289 | private data class Offset(var x: Int = 0, var y: Int = 0) 290 | 291 | /******************************************************** 292 | * Delegated stuff 293 | *******************************************************/ 294 | override fun addIgnoredHeight(ignoredHeight: Int) { 295 | root.addIgnoredHeight(ignoredHeight) 296 | layoutState.disappearedOrRemovedHeight += ignoredHeight 297 | } 298 | 299 | override val supportsPredictiveItemAnimations get() = root.supportsPredictiveItemAnimations 300 | override val isPreLayout: Boolean get() = root.isPreLayout 301 | override val willRunPredictiveAnimations: Boolean get() = root.willRunPredictiveAnimations 302 | override val itemCount get() = root.itemCount 303 | override val hasTargetScrollPosition get() = root.hasTargetScrollPosition 304 | override val targetScrollPosition get() = root.targetScrollPosition 305 | override val scrap get() = root.scrap 306 | override val basePaddingLeft get() = root.basePaddingLeft 307 | override val basePaddingTop get() = root.basePaddingTop 308 | override val basePaddingRight get() = root.basePaddingRight 309 | override val basePaddingBottom get() = root.basePaddingBottom 310 | override fun getTransformedPaddingLeft(sectionConfig: SectionConfig) = root.getTransformedPaddingLeft(sectionConfig) 311 | override fun getTransformedPaddingTop(sectionConfig: SectionConfig) = root.getTransformedPaddingTop(sectionConfig) 312 | override fun getTransformedPaddingRight(sectionConfig: SectionConfig) 313 | = root.getTransformedPaddingRight(sectionConfig) 314 | 315 | override fun getTransformedPaddingBottom(sectionConfig: SectionConfig) 316 | = root.getTransformedPaddingBottom(sectionConfig) 317 | 318 | override fun getMeasuredWidth(child: View): Int = root.getMeasuredWidth(child) 319 | override fun getMeasuredHeight(child: View): Int = root.getMeasuredHeight(child) 320 | override var stickyStartInset get() = root.stickyStartInset 321 | set(value) { 322 | root.stickyStartInset = value 323 | } 324 | override var stickyEndInset get() = root.stickyEndInset 325 | set(value) { 326 | root.stickyEndInset = value 327 | } 328 | 329 | override fun offsetChildrenVertical(dy: Int) = root.offsetChildrenVertical(dy) 330 | override fun offsetChildrenHorizontal(dx: Int) = root.offsetChildrenHorizontal(dx) 331 | override fun offsetHorizontal(view: View, dx: Int) = root.offsetHorizontal(view, dx) 332 | override fun offsetVertical(view: View, dy: Int) = root.offsetVertical(view, dy) 333 | override fun removeView(child: View, helper: LayoutHelper) = root.removeView(child, helper) 334 | } 335 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/layout/flexbox.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.layout 2 | 3 | import com.tonicartos.superslim.LayoutHelper 4 | import com.tonicartos.superslim.SectionConfig 5 | import com.tonicartos.superslim.SectionLayoutManager 6 | import com.tonicartos.superslim.adapter.FooterStyle 7 | import com.tonicartos.superslim.adapter.HeaderStyle 8 | import com.tonicartos.superslim.internal.SectionState 9 | import com.tonicartos.superslim.internal.SectionState.LayoutState 10 | 11 | class FlexboxSectionConfig(gutterStart: Int = SectionConfig.DEFAULT_GUTTER, 12 | gutterEnd: Int = SectionConfig.DEFAULT_GUTTER, 13 | @HeaderStyle headerStyle: Int = SectionConfig.DEFAULT_HEADER_STYLE, 14 | @FooterStyle footerStyle: Int = SectionConfig.DEFAULT_FOOTER_STYLE, 15 | paddingStart: Int = 0, paddingTop: Int = 0, paddingEnd: Int = 0, paddingBottom: Int = 0) : 16 | SectionConfig(gutterStart, gutterEnd, headerStyle, footerStyle, paddingStart, paddingTop, paddingEnd, 17 | paddingBottom) { 18 | 19 | override fun onMakeSection(oldState: SectionState?): SectionState = FlexboxSectionState(this, oldState) 20 | 21 | override fun onCopy(): FlexboxSectionConfig { 22 | return FlexboxSectionConfig(gutterStart, gutterEnd, headerStyle, footerStyle) 23 | } 24 | } 25 | 26 | private class FlexboxSectionState(configuration: FlexboxSectionConfig, oldState: SectionState? = null) 27 | : SectionState(configuration, oldState) { 28 | override fun isAtTop(layoutState: LayoutState) = FlexboxSlm.isAtTop(this, layoutState) 29 | 30 | override fun doLayout(helper: LayoutHelper, layoutState: LayoutState) 31 | = FlexboxSlm.onLayout(helper, this, layoutState) 32 | 33 | override fun doFillTop(dy: Int, helper: LayoutHelper, layoutState: LayoutState) 34 | = FlexboxSlm.onFillTop(dy, helper, this, layoutState) 35 | 36 | override fun doFillBottom(dy: Int, helper: LayoutHelper, layoutState: LayoutState) 37 | = FlexboxSlm.onFillTop(dy, helper, this, layoutState) 38 | 39 | override fun doTrimTop(scrolled: Int, helper: LayoutHelper, layoutState: LayoutState): Int 40 | = FlexboxSlm.onTrimTop(scrolled, helper, this, layoutState) 41 | 42 | override fun doTrimBottom(scrolled: Int, helper: LayoutHelper, layoutState: LayoutState): Int 43 | = FlexboxSlm.onTrimBottom(scrolled, helper, this, layoutState) 44 | } 45 | 46 | private object FlexboxSlm : SectionLayoutManager { 47 | override fun isAtTop(section: FlexboxSectionState, layoutState: LayoutState): Boolean { 48 | TODO("not implemented") 49 | } 50 | 51 | override fun onLayout(helper: LayoutHelper, section: FlexboxSectionState, layoutState: LayoutState) { 52 | TODO("not implemented") 53 | } 54 | 55 | override fun onFillTop(dy: Int, helper: LayoutHelper, section: FlexboxSectionState, layoutState: LayoutState): Int { 56 | TODO("not implemented") 57 | } 58 | 59 | /** 60 | * Fill revealed area where content has been scrolled up the screen by dy. 61 | */ 62 | override fun onFillBottom(dy: Int, helper: LayoutHelper, section: FlexboxSectionState, 63 | layoutState: LayoutState): Int { 64 | TODO("not implemented") 65 | } 66 | 67 | override fun onTrimTop(scrolled: Int, helper: LayoutHelper, 68 | section: FlexboxSectionState, 69 | layoutState: LayoutState): Int { 70 | TODO("not implemented") 71 | } 72 | 73 | override fun onTrimBottom(scrolled: Int, helper: LayoutHelper, 74 | section: FlexboxSectionState, 75 | layoutState: LayoutState): Int { 76 | TODO("not implemented") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/layout/grid.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.layout 2 | 3 | import com.tonicartos.superslim.* 4 | import com.tonicartos.superslim.adapter.FooterStyle 5 | import com.tonicartos.superslim.adapter.HeaderStyle 6 | import com.tonicartos.superslim.internal.SectionState 7 | import com.tonicartos.superslim.internal.SectionState.LayoutState 8 | 9 | class GridSectionConfig(gutterStart: Int = SectionConfig.DEFAULT_GUTTER, gutterEnd: Int = SectionConfig.DEFAULT_GUTTER, 10 | @HeaderStyle headerStyle: Int = SectionConfig.DEFAULT_HEADER_STYLE, 11 | @FooterStyle footerStyle: Int = SectionConfig.DEFAULT_FOOTER_STYLE, 12 | paddingStart: Int = 0, paddingTop: Int = 0, paddingEnd: Int = 0, paddingBottom: Int = 0) : 13 | SectionConfig(gutterStart, gutterEnd, headerStyle, footerStyle, paddingStart, paddingTop, paddingEnd, 14 | paddingBottom), 15 | ColumnsSectionConfigurationMixin by ColumnsConfiguration() { 16 | override fun onMakeSection(oldState: SectionState?): SectionState = GridSectionState(this, oldState) 17 | 18 | override fun onCopy(): GridSectionConfig { 19 | val copy = GridSectionConfig(gutterStart, gutterEnd, headerStyle, footerStyle) 20 | copy.numColumns = numColumns 21 | copy.columnWidth = columnWidth 22 | return copy 23 | } 24 | } 25 | 26 | private class GridSectionState(var configuration: GridSectionConfig, oldState: SectionState? = null) : 27 | SectionState(configuration, oldState), ColumnsSectionStateMixin by ColumnsState(configuration) { 28 | override fun isAtTop(layoutState: LayoutState) = GridSlm.isAtTop(this, layoutState) 29 | 30 | override fun doLayout(helper: LayoutHelper, layoutState: LayoutState) { 31 | resolveColumns(helper) 32 | 33 | GridSlm.onLayout(helper, this, layoutState) 34 | } 35 | 36 | override fun doFillTop(dy: Int, helper: LayoutHelper, layoutState: LayoutState): Int { 37 | TODO("not implemented") 38 | } 39 | 40 | override fun doFillBottom(dy: Int, helper: LayoutHelper, layoutState: LayoutState): Int { 41 | TODO("not implemented") 42 | } 43 | 44 | override fun doTrimTop(scrolled: Int, helper: LayoutHelper, 45 | layoutState: LayoutState): Int { 46 | TODO("not implemented") 47 | } 48 | 49 | override fun doTrimBottom(scrolled: Int, helper: LayoutHelper, 50 | layoutState: LayoutState): Int { 51 | TODO("not implemented") 52 | } 53 | } 54 | 55 | private object GridSlm : SectionLayoutManager { 56 | override fun isAtTop(section: GridSectionState, layoutState: LayoutState): Boolean { 57 | TODO("not implemented") 58 | } 59 | 60 | override fun onLayout(helper: LayoutHelper, section: GridSectionState, layoutState: LayoutState) { 61 | TODO("not implemented") 62 | } 63 | 64 | override fun onFillTop(dy: Int, helper: LayoutHelper, section: GridSectionState, layoutState: LayoutState): Int { 65 | TODO("not implemented") 66 | } 67 | 68 | override fun onFillBottom(dy: Int, helper: LayoutHelper, section: GridSectionState, layoutState: LayoutState): Int { 69 | TODO("not implemented") 70 | } 71 | 72 | override fun onTrimTop(scrolled: Int, helper: LayoutHelper, 73 | section: GridSectionState, 74 | layoutState: LayoutState): Int { 75 | TODO("not implemented") 76 | } 77 | 78 | override fun onTrimBottom(scrolled: Int, helper: LayoutHelper, 79 | section: GridSectionState, 80 | layoutState: LayoutState): Int { 81 | TODO("not implemented") 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/layout/linear.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.layout 2 | 3 | import com.tonicartos.superslim.LayoutHelper 4 | import com.tonicartos.superslim.SectionConfig 5 | import com.tonicartos.superslim.SectionLayoutManager 6 | import com.tonicartos.superslim.adapter.FooterStyle 7 | import com.tonicartos.superslim.adapter.HeaderStyle 8 | import com.tonicartos.superslim.internal.SectionState 9 | import com.tonicartos.superslim.internal.SectionState.LayoutState 10 | import com.tonicartos.superslim.use 11 | 12 | class LinearSectionConfig(gutterStart: Int = SectionConfig.DEFAULT_GUTTER, 13 | gutterEnd: Int = SectionConfig.DEFAULT_GUTTER, 14 | @HeaderStyle headerStyle: Int = SectionConfig.DEFAULT_HEADER_STYLE, 15 | @FooterStyle footerStyle: Int = SectionConfig.DEFAULT_FOOTER_STYLE) : 16 | SectionConfig(gutterStart, gutterEnd, headerStyle, footerStyle, 0, 0, 0, 0) { 17 | override fun onMakeSection(oldState: SectionState?): SectionState = LinearSectionState(this, oldState) 18 | 19 | override fun onCopy(): LinearSectionConfig { 20 | return LinearSectionConfig(gutterStart, gutterEnd, headerStyle, footerStyle) 21 | } 22 | } 23 | 24 | internal class LinearSectionState(configuration: LinearSectionConfig, oldState: SectionState? = null) 25 | : SectionState(configuration, oldState) { 26 | override fun isAtTop(layoutState: LayoutState) 27 | = LinearSlm.isAtTop(this, layoutState) 28 | 29 | override fun doLayout(helper: LayoutHelper, layoutState: LayoutState) 30 | = LinearSlm.onLayout(helper, this, layoutState) 31 | 32 | override fun doFillTop(dy: Int, helper: LayoutHelper, layoutState: LayoutState) 33 | = LinearSlm.onFillTop(dy, helper, this, layoutState) 34 | 35 | override fun doFillBottom(dy: Int, helper: LayoutHelper, layoutState: LayoutState) 36 | = LinearSlm.onFillBottom(dy, helper, this, layoutState) 37 | 38 | override fun doTrimTop(scrolled: Int, helper: LayoutHelper, layoutState: LayoutState) 39 | = LinearSlm.onTrimTop(scrolled, helper, this, layoutState) 40 | 41 | override fun doTrimBottom(scrolled: Int, helper: LayoutHelper, layoutState: LayoutState) 42 | = LinearSlm.onTrimBottom(scrolled, helper, this, layoutState) 43 | } 44 | 45 | internal object LinearSlm : SectionLayoutManager { 46 | override fun isAtTop(section: LinearSectionState, layoutState: LayoutState) 47 | = layoutState.overdraw == 0 && layoutState.headPosition <= 0 && 48 | section.isChildAtTop(layoutState.headPosition) 49 | 50 | override fun onLayout(helper: LayoutHelper, section: LinearSectionState, layoutState: LayoutState) { 51 | if (layoutState.headPosition < 0) layoutState.headPosition = 0 52 | layoutState.tailPosition = -1 53 | var currentPosition = layoutState.headPosition 54 | var y = -layoutState.overdraw 55 | 56 | while (helper.moreToLayout(currentPosition, section)) { 57 | helper.getChild(currentPosition, section)?.use { child -> 58 | child.addToRecyclerView() 59 | child.measure() 60 | child.layout(0, y, child.measuredWidth, y + child.measuredHeight, helper.numViews) 61 | if (helper.isPreLayout && child.isRemoved) { 62 | helper.addIgnoredHeight(child.height) 63 | } 64 | layoutState.disappearedOrRemovedHeight += child.disappearedHeight 65 | y += child.height 66 | helper.filledArea += child.height 67 | if (child.disappearedHeight < child.height) layoutState.tailPosition = currentPosition 68 | currentPosition += 1 69 | } ?: break 70 | } 71 | layoutState.bottom = y 72 | if (layoutState.tailPosition == -1) layoutState.headPosition = -1 73 | // Log.d("Linear", "${section} --- $layoutState") 74 | } 75 | 76 | override fun onFillTop(dy: Int, helper: LayoutHelper, section: LinearSectionState, layoutState: LayoutState): Int { 77 | if (layoutState.headPosition < 0) { 78 | layoutState.headPosition = section.numChildren 79 | layoutState.tailPosition = section.numChildren - 1 80 | } 81 | 82 | var dyRemaining = dy 83 | var currentPos = layoutState.headPosition 84 | var y = -layoutState.overdraw 85 | 86 | // Fill leading children 87 | if (0 <= currentPos && currentPos < section.numChildren) { 88 | helper.getUnfinishedChild(currentPos, section)?.use { child -> 89 | child.measure() 90 | // Different from fillBottom because overscroll hides the excess from section height. 91 | child.fillTop(dyRemaining, 0, y - child.measuredHeight, child.measuredWidth, y).let { 92 | y -= it 93 | dyRemaining -= it 94 | } 95 | } 96 | } 97 | 98 | // Fill using overdraw 99 | dyRemaining -= layoutState.overdraw 100 | 101 | // Fill remaining dy with remaining content. 102 | while (dyRemaining > 0 && currentPos > 0) { 103 | currentPos -= 1 104 | helper.getChild(currentPos, section)?.use { child -> 105 | child.addToRecyclerView(0) 106 | child.measure() 107 | child.fillTop(dyRemaining, 0, y - child.measuredHeight, child.measuredWidth, y).let { 108 | y -= it 109 | dyRemaining -= it 110 | } 111 | } 112 | } 113 | 114 | val filled = Math.min(dy, dy - dyRemaining) // Cap filled distance at dy. Any left over is overdraw. 115 | layoutState.overdraw = Math.max(0, -dyRemaining) // If dyRemaining is -ve, then overdraw happened. 116 | layoutState.bottom += filled // Section got taller by the filled amount. 117 | layoutState.headPosition = currentPos 118 | return filled 119 | } 120 | 121 | /** 122 | * Fill revealed area where content has been scrolled up the screen by dy. 123 | */ 124 | override fun onFillBottom(dy: Int, helper: LayoutHelper, section: LinearSectionState, 125 | layoutState: LayoutState): Int { 126 | if (layoutState.headPosition < 0) { 127 | layoutState.headPosition = 0 128 | } 129 | 130 | var y = layoutState.bottom 131 | // Must fill section children first. 132 | var filled = 0 133 | if (layoutState.tailPosition >= 0) { 134 | // Must handle case where not just the trailing child needs updating. 135 | var stepBackHeight = 0 136 | var stepBackViews = 0 137 | for (position in layoutState.tailPosition downTo layoutState.headPosition) { 138 | helper.getUnfinishedChild(position, section)?.use { child -> 139 | child.measure() 140 | 141 | val before = child.height 142 | val fill = child.fillBottom(dy, 0, y - stepBackHeight - child.height, child.measuredWidth, 0, 143 | helper.numViews - stepBackViews - child.numViews) 144 | if (position == layoutState.tailPosition) { 145 | y += child.height - before 146 | filled += fill 147 | } 148 | 149 | stepBackHeight += child.height 150 | stepBackViews += child.numViews 151 | } 152 | if (y - stepBackHeight <= helper.layoutLimit - helper.stickyEndInset) break 153 | } 154 | } 155 | 156 | // Check to see if we have to handle excess. 157 | val excess = if (layoutState.tailPosition >= 0 && filled == 0) { 158 | Math.max(0, layoutState.bottom - helper.layoutLimit) 159 | } else { 160 | 0 161 | } 162 | 163 | // Fill in remaining space 164 | while (filled + excess < dy && layoutState.tailPosition + 1 < section.numChildren) { 165 | helper.getChild(layoutState.tailPosition + 1, section)?.use { child -> 166 | child.addToRecyclerView() 167 | child.measure() 168 | filled += child.fillBottom(dy - filled, 0, y, child.measuredWidth, y + child.measuredHeight, 169 | helper.numViews) 170 | layoutState.tailPosition += 1 171 | y += child.height 172 | } 173 | } 174 | 175 | filled += excess 176 | 177 | layoutState.bottom = y 178 | 179 | return Math.min(dy, filled) 180 | } 181 | 182 | override fun onTrimTop(scrolled: Int, helper: LayoutHelper, section: LinearSectionState, 183 | layoutState: LayoutState): Int { 184 | var removedHeight = 0 185 | while (layoutState.headPosition <= layoutState.tailPosition) { 186 | var childRemoved = false 187 | helper.getUnfinishedChild(layoutState.headPosition, section)?.use { child -> 188 | removedHeight += child.trimTop(scrolled, 0, helper) 189 | childRemoved = child.numViews == 0 190 | // Don't adjust overdraw because section children don't report drawing into the area. 191 | } ?: helper.getAttachedViewAt(0) { child -> 192 | if (child.bottom < 0) { 193 | child.remove() 194 | removedHeight += Math.max(0, child.height - layoutState.overdraw) 195 | layoutState.overdraw = Math.max(0, layoutState.overdraw - child.height) 196 | childRemoved = true 197 | } else if (child.top < 0) { 198 | val before = layoutState.overdraw 199 | layoutState.overdraw = -child.top 200 | removedHeight += layoutState.overdraw - before 201 | } 202 | } 203 | 204 | if (childRemoved) { 205 | layoutState.headPosition += 1 206 | } else { 207 | break 208 | } 209 | } 210 | 211 | if (helper.numViews == 0) { 212 | layoutState.headPosition = -1 213 | layoutState.tailPosition = -1 214 | } 215 | layoutState.bottom -= removedHeight 216 | return removedHeight 217 | } 218 | 219 | override fun onTrimBottom(scrolled: Int, helper: LayoutHelper, section: LinearSectionState, 220 | layoutState: LayoutState): Int { 221 | var removedHeight = 0 222 | var y = layoutState.bottom 223 | // Trim child sections back to before the sticky edge. Remove child views after the limit. Track tail position. 224 | var stepBackViews = 0 225 | for (position in layoutState.tailPosition downTo layoutState.headPosition) { 226 | helper.getUnfinishedChild(position, section)?.use { child -> 227 | y -= child.height 228 | removedHeight += child.trimBottom(scrolled, y, helper, helper.numViews - stepBackViews - child.numViews) 229 | stepBackViews += child.numViews 230 | if (child.numViews == 0) layoutState.tailPosition -= 1 231 | } ?: helper.getAttachedViewAt(helper.numViews - 1) { child -> 232 | y -= child.height 233 | stepBackViews += 1 234 | if (child.top > helper.layoutLimit) { 235 | removedHeight += child.height 236 | child.remove() 237 | layoutState.tailPosition -= 1 238 | stepBackViews -= 1 239 | } 240 | } 241 | // Go until marker before sticky edge. 242 | if (y < helper.layoutLimit - helper.stickyEndInset) break 243 | } 244 | 245 | if (helper.numViews == 0) { 246 | layoutState.headPosition = -1 247 | layoutState.tailPosition = -1 248 | } 249 | layoutState.bottom -= removedHeight 250 | return removedHeight 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/layout/staggered_grid.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim.layout 2 | 3 | import com.tonicartos.superslim.* 4 | import com.tonicartos.superslim.adapter.FooterStyle 5 | import com.tonicartos.superslim.adapter.HeaderStyle 6 | import com.tonicartos.superslim.internal.SectionState 7 | import com.tonicartos.superslim.internal.SectionState.LayoutState 8 | 9 | class StaggeredGridSectionConfig(gutterStart: Int = SectionConfig.DEFAULT_GUTTER, 10 | gutterEnd: Int = SectionConfig.DEFAULT_GUTTER, 11 | @HeaderStyle headerStyle: Int = SectionConfig.DEFAULT_HEADER_STYLE, 12 | @FooterStyle footerStyle: Int = SectionConfig.DEFAULT_FOOTER_STYLE, 13 | paddingStart: Int = 0, paddingTop: Int = 0, paddingEnd: Int = 0, 14 | paddingBottom: Int = 0) : 15 | SectionConfig(gutterStart, gutterEnd, headerStyle, footerStyle, paddingStart, paddingTop, paddingEnd, 16 | paddingBottom), 17 | ColumnsSectionConfigurationMixin by ColumnsConfiguration() { 18 | override fun onMakeSection(oldState: SectionState?): SectionState = StaggeredGridSection(this, oldState) 19 | 20 | override fun onCopy(): StaggeredGridSectionConfig { 21 | val copy = StaggeredGridSectionConfig(gutterStart, gutterEnd, headerStyle, footerStyle) 22 | copy.numColumns = numColumns 23 | copy.columnWidth = columnWidth 24 | return copy 25 | } 26 | } 27 | 28 | private class StaggeredGridSection(var configuration: StaggeredGridSectionConfig, oldState: SectionState? = null) : 29 | SectionState(configuration, oldState), ColumnsSectionStateMixin by ColumnsState(configuration) { 30 | override fun isAtTop(layoutState: LayoutState): Boolean { 31 | return StaggeredGridSlm.isAtTop(this, layoutState) 32 | } 33 | 34 | override fun doLayout(helper: LayoutHelper, layoutState: LayoutState) { 35 | resolveColumns(helper) 36 | 37 | StaggeredGridSlm.onLayout(helper, this, layoutState) 38 | } 39 | 40 | override fun doFillTop(dy: Int, helper: LayoutHelper, layoutState: LayoutState): Int { 41 | TODO("not implemented") 42 | } 43 | 44 | override fun doFillBottom(dy: Int, helper: LayoutHelper, layoutState: LayoutState): Int { 45 | TODO("not implemented") 46 | } 47 | 48 | override fun doTrimTop(scrolled: Int, helper: LayoutHelper, 49 | layoutState: LayoutState): Int { 50 | TODO("not implemented") 51 | } 52 | 53 | override fun doTrimBottom(scrolled: Int, helper: LayoutHelper, 54 | layoutState: LayoutState): Int { 55 | TODO("not implemented") 56 | } 57 | } 58 | 59 | private object StaggeredGridSlm : SectionLayoutManager { 60 | override fun isAtTop(section: StaggeredGridSection, layoutState: LayoutState): Boolean { 61 | TODO("not implemented") 62 | } 63 | 64 | override fun onLayout(helper: LayoutHelper, section: StaggeredGridSection, layoutState: LayoutState) { 65 | TODO("not implemented") 66 | } 67 | 68 | override fun onFillTop(dy: Int, helper: LayoutHelper, section: StaggeredGridSection, 69 | layoutState: LayoutState): Int { 70 | TODO("not implemented") 71 | } 72 | 73 | override fun onFillBottom(dy: Int, helper: LayoutHelper, section: StaggeredGridSection, 74 | layoutState: LayoutState): Int { 75 | TODO("not implemented") 76 | } 77 | 78 | override fun onTrimTop(scrolled: Int, helper: LayoutHelper, 79 | section: StaggeredGridSection, 80 | layoutState: LayoutState): Int { 81 | TODO("not implemented") 82 | } 83 | 84 | override fun onTrimBottom(scrolled: Int, helper: LayoutHelper, 85 | section: StaggeredGridSection, 86 | layoutState: LayoutState): Int { 87 | TODO("not implemented") 88 | } 89 | } 90 | 91 | 92 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/tonicartos/superslim/section_data_mixins.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim 2 | 3 | interface ColumnsSectionConfigurationMixin { 4 | var numColumns: Int 5 | var columnWidth: Int 6 | } 7 | 8 | class ColumnsConfiguration : ColumnsSectionConfigurationMixin { 9 | override var numColumns = 0 10 | get() = field 11 | set(value) { 12 | field = value 13 | columnWidth = -1 14 | } 15 | 16 | override var columnWidth: Int = -1 17 | } 18 | 19 | interface ColumnsSectionStateMixin { 20 | var numColumns: Int 21 | 22 | fun resolveColumns(helper: LayoutHelper) 23 | } 24 | 25 | class ColumnsState(configuration: ColumnsSectionConfigurationMixin) : ColumnsSectionStateMixin { 26 | override var numColumns: Int = 0 27 | private var requestedColumnWidth = -1 28 | private var columnsResolved = false 29 | 30 | init { 31 | numColumns = configuration.numColumns 32 | requestedColumnWidth = configuration.columnWidth 33 | } 34 | 35 | override fun resolveColumns(helper: LayoutHelper) { 36 | if (columnsResolved) { 37 | return 38 | } 39 | 40 | if (requestedColumnWidth != -1) { 41 | numColumns = helper.layoutWidth / requestedColumnWidth 42 | } 43 | 44 | if (numColumns == 0) { 45 | numColumns = 1 46 | } 47 | 48 | columnsResolved = true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SuperSLiM 3 | 4 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/tonicartos/superslim/grid_layout.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim 2 | 3 | /** 4 | * 5 | */ 6 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/tonicartos/superslim/header_layout.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim 2 | 3 | 4 | class HeaderTests { 5 | //TODO: mock helper 6 | //TODO: mock section state 7 | //TODO: mock layout state 8 | } 9 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/tonicartos/superslim/linear_layout.kt: -------------------------------------------------------------------------------- 1 | package com.tonicartos.superslim 2 | 3 | class LinearSlmTests { 4 | } 5 | -------------------------------------------------------------------------------- /library/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':dbexample', ':library' 2 | --------------------------------------------------------------------------------