├── .gitignore ├── .idea ├── caches │ └── build_file_checksums.ser ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── bullseyedevs │ │ └── tableview │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── bullseyedevs │ │ │ └── tableview │ │ │ ├── MainActivity.java │ │ │ ├── adapter │ │ │ └── ClubAdapter.java │ │ │ ├── model │ │ │ └── Club.java │ │ │ └── util │ │ │ └── FixedGridLayoutManager.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── row_club.xml │ │ └── row_club_colorful.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── searchable.xml │ └── test │ └── java │ └── com │ └── bullseyedevs │ └── tableview │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | -------------------------------------------------------------------------------- /.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brkckr/TableView/960bfd6bd4ef9af026e023f55c6476a6d49cc81e/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TableView 2 | A RecyclerView that looks like a TableView with a fixed header that can scroll horizontally and vertically at the same time. 3 | 4 | ![TableView Preview](https://media.giphy.com/media/cY9n8HTNHLJqxFzOsg/giphy.gif) 5 | 6 | --- 7 | 8 | ## Features 9 | - Both horizontal and vertical scrollable 10 | - Searchable 11 | 12 | --- 13 | 14 | ## License 15 | 16 | This project is licensed under the terms of the **MIT** license. 17 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | defaultConfig { 6 | applicationId "com.bullseyedevs.tableview" 7 | minSdkVersion 19 8 | targetSdkVersion 27 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'com.android.support:appcompat-v7:27.1.1' 24 | implementation 'com.android.support.constraint:constraint-layout:1.1.0' 25 | testImplementation 'junit:junit:4.12' 26 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 27 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 28 | 29 | implementation 'com.android.support:recyclerview-v7:27.1.1' 30 | 31 | implementation 'com.github.bumptech.glide:glide:4.7.1' 32 | } 33 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/bullseyedevs/tableview/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.bullseyedevs.tableview; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest 19 | { 20 | @Test 21 | public void useAppContext() 22 | { 23 | // Context of the app under test. 24 | Context appContext = InstrumentationRegistry.getTargetContext(); 25 | 26 | assertEquals("com.bullseyedevs.tableview", appContext.getPackageName()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/bullseyedevs/tableview/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.bullseyedevs.tableview; 2 | 3 | import android.app.SearchManager; 4 | import android.content.Context; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.os.Bundle; 7 | import android.support.v7.widget.DividerItemDecoration; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.support.v7.widget.SearchView; 10 | import android.view.Menu; 11 | import android.view.MenuItem; 12 | import android.widget.HorizontalScrollView; 13 | 14 | import com.bullseyedevs.tableview.adapter.ClubAdapter; 15 | import com.bullseyedevs.tableview.model.Club; 16 | import com.bullseyedevs.tableview.util.FixedGridLayoutManager; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | public class MainActivity extends AppCompatActivity 22 | { 23 | 24 | int scrollX = 0; 25 | 26 | List clubList = new ArrayList<>(); 27 | 28 | RecyclerView rvClub; 29 | 30 | HorizontalScrollView headerScroll; 31 | 32 | SearchView searchView; 33 | 34 | ClubAdapter clubAdapter; 35 | 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) 38 | { 39 | super.onCreate(savedInstanceState); 40 | setContentView(R.layout.activity_main); 41 | 42 | initViews(); 43 | 44 | prepareClubData(); 45 | 46 | setUpRecyclerView(); 47 | 48 | rvClub.addOnScrollListener(new RecyclerView.OnScrollListener() 49 | { 50 | @Override 51 | public void onScrolled(RecyclerView recyclerView, int dx, int dy) 52 | { 53 | super.onScrolled(recyclerView, dx, dy); 54 | 55 | scrollX += dx; 56 | 57 | headerScroll.scrollTo(scrollX, 0); 58 | } 59 | 60 | @Override 61 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) 62 | { 63 | super.onScrollStateChanged(recyclerView, newState); 64 | } 65 | }); 66 | } 67 | 68 | @Override 69 | public boolean onCreateOptionsMenu(Menu menu) 70 | { 71 | getMenuInflater().inflate(R.menu.menu_main, menu); 72 | 73 | // Associate searchable configuration with the SearchView 74 | SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); 75 | searchView = (SearchView) menu.findItem(R.id.action_search).getActionView(); 76 | searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); 77 | searchView.setMaxWidth(Integer.MAX_VALUE); 78 | 79 | // listening to search query text change 80 | searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() 81 | { 82 | @Override 83 | public boolean onQueryTextSubmit(String query) 84 | { 85 | // filter recycler view when query submitted 86 | clubAdapter.getFilter().filter(query); 87 | return false; 88 | } 89 | 90 | @Override 91 | public boolean onQueryTextChange(String query) 92 | { 93 | // filter recycler view when text is changed 94 | clubAdapter.getFilter().filter(query); 95 | return false; 96 | } 97 | }); 98 | return true; 99 | } 100 | 101 | @Override 102 | public boolean onOptionsItemSelected(MenuItem item) 103 | { 104 | // Handle action bar item clicks here. The action bar will 105 | // automatically handle clicks on the Home/Up button, so long 106 | // as you specify a parent activity in AndroidManifest.xml. 107 | int id = item.getItemId(); 108 | 109 | //noinspection SimplifiableIfStatement 110 | if (id == R.id.action_search) 111 | { 112 | return true; 113 | } 114 | 115 | return super.onOptionsItemSelected(item); 116 | } 117 | 118 | @Override 119 | public void onBackPressed() 120 | { 121 | // close search view on back button pressed 122 | if (!searchView.isIconified()) 123 | { 124 | searchView.setIconified(true); 125 | return; 126 | } 127 | super.onBackPressed(); 128 | } 129 | 130 | private void initViews() 131 | { 132 | rvClub = findViewById(R.id.rvClub); 133 | headerScroll = findViewById(R.id.headerScroll); 134 | } 135 | 136 | /** 137 | * Prepares dummy data 138 | */ 139 | private void prepareClubData() 140 | { 141 | clubList.add(new Club("Galatasaray", "https://tmssl.akamaized.net/images/wappen/head/141.png", "Istanbul, Turkey", "Ali Sami Yen", "Süper Lig", "Fatih Terim", "Bafetimbi Gomis")); 142 | clubList.add(new Club("Real Madrid", "https://tmssl.akamaized.net//images/wappen/head/418.png", "Madrid, Spain", "Santiago Barnabeu", "La Liga", "Zidane", "Cristiano Ronaldo")); 143 | clubList.add(new Club("Barcelona", "https://tmssl.akamaized.net//images/wappen/head/131.png", "Barcelona, Spain", "Camp Nou", "La Liga", "Ernesto Valverde", "Lionel Messi")); 144 | clubList.add(new Club("Bayern München", "https://tmssl.akamaized.net//images/wappen/head/27.png", "München, Germany", "Allianz Arena", "Bundesliga", "Jupp Heynckes", "Robert Lewandowski")); 145 | clubList.add(new Club("Manchester United", "https://tmssl.akamaized.net//images/wappen/head/985.png", "Manchester, England", "Old Trafford", "Premier League", "Jose Mourinho", "Paul Pogba")); 146 | clubList.add(new Club("Manchester City", "https://tmssl.akamaized.net//images/wappen/head/281.png", "Manchester, England", " Etihad Stadium", "Premier League", "Pep Guardiola", "Kevin de Bruyne")); 147 | clubList.add(new Club("Atletico Madrid", "https://tmssl.akamaized.net//images/wappen/head/13.png", "Madrid, Spain", "Estadio Metropolitano de Madrid ", "La Liga", "Diego Simeone", "Antoine Griezmann")); 148 | clubList.add(new Club("Liverpool", "https://tmssl.akamaized.net//images/wappen/head/31.png", "Liverpool, Spain", "Anfield", "Premier League", "Klopp", "Mo Salah")); 149 | clubList.add(new Club("Juventus", "https://tmssl.akamaized.net//images/wappen/head/506.png", "Turin, Italy", "Allianz Stadium", "Serie A", "Massimiliano Allegri", "Paulo Dybala")); 150 | clubList.add(new Club("Arsenal", "https://tmssl.akamaized.net//images/wappen/head/11.png", "London, England", "Emirates Stadium", "Premier League", "Arsene Wenger", "Mesut Özil")); 151 | clubList.add(new Club("Roma", "https://tmssl.akamaized.net//images/wappen/head/12.png", "Rome, Italy", " Olimpico di Roma", "Serie A", "Eusebio Di Francesco", "Cengiz Ünder")); 152 | clubList.add(new Club("PSG", "https://tmssl.akamaized.net//images/wappen/head/583.png", "Paris, France", "Parc des Princes ", "Ligue 1", "Unai Emery", "Neymar")); 153 | clubList.add(new Club("Chelsea", "https://tmssl.akamaized.net//images/wappen/head/631.png", "London, England", "Stamford Bridge", "Premier League", "Conte", "Eden Hazard")); 154 | clubList.add(new Club("Tottenham", "https://tmssl.akamaized.net//images/wappen/head/148.png", "London, England", "Wembley Stadium ", "Premier League", "Mauricio Pochettino", "Harry Kane")); 155 | } 156 | 157 | /** 158 | * Handles RecyclerView for the action 159 | */ 160 | private void setUpRecyclerView() 161 | { 162 | clubAdapter = new ClubAdapter(MainActivity.this, clubList); 163 | 164 | FixedGridLayoutManager manager = new FixedGridLayoutManager(); 165 | manager.setTotalColumnCount(1); 166 | rvClub.setLayoutManager(manager); 167 | rvClub.setAdapter(clubAdapter); 168 | rvClub.addItemDecoration(new DividerItemDecoration(MainActivity.this, DividerItemDecoration.VERTICAL)); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /app/src/main/java/com/bullseyedevs/tableview/adapter/ClubAdapter.java: -------------------------------------------------------------------------------- 1 | package com.bullseyedevs.tableview.adapter; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.Filterable; 9 | import android.widget.ImageView; 10 | import android.widget.TextView; 11 | import android.widget.Filter; 12 | 13 | import com.bullseyedevs.tableview.R; 14 | import com.bullseyedevs.tableview.model.Club; 15 | import com.bumptech.glide.Glide; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | public class ClubAdapter extends RecyclerView.Adapter implements Filterable 21 | { 22 | private static final int TYPE_ROW = 0; 23 | private static final int TYPE_ROW_COLORFUL = 1; 24 | 25 | private List clubList; 26 | private List filteredClubList; 27 | private Context context; 28 | 29 | public ClubAdapter(Context context, List clubList) 30 | { 31 | this.context = context; 32 | this.clubList = clubList; 33 | this.filteredClubList = clubList; 34 | } 35 | 36 | @Override 37 | public int getItemViewType(int position) 38 | { 39 | if (position % 2 == 0) 40 | { 41 | return TYPE_ROW_COLORFUL; 42 | } 43 | 44 | return TYPE_ROW; 45 | } 46 | 47 | @Override 48 | public ClubViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) 49 | { 50 | if (viewType == TYPE_ROW) 51 | { 52 | View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.row_club, viewGroup, false); 53 | return new ClubViewHolder(view); 54 | } else 55 | { 56 | View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.row_club_colorful, 57 | viewGroup, false); 58 | return new ClubViewHolder(view); 59 | } 60 | } 61 | 62 | @Override 63 | public void onBindViewHolder(ClubViewHolder holder, int position) 64 | { 65 | Club club = filteredClubList.get(position); 66 | 67 | holder.txtName.setText(club.name); 68 | holder.txtLocation.setText(club.location); 69 | holder.txtStadiumName.setText(club.stadiumName); 70 | holder.txtLeagueName.setText(club.leagueName); 71 | holder.txtCoachName.setText(club.coachName); 72 | holder.txtStarPlayerName.setText(club.starPlayerName); 73 | 74 | Glide.with(context).load(club.logoUrl).into(holder.imgLogo); 75 | } 76 | 77 | @Override 78 | public int getItemCount() 79 | { 80 | return filteredClubList.size(); 81 | } 82 | 83 | public class ClubViewHolder extends RecyclerView.ViewHolder 84 | { 85 | public TextView txtName, txtLocation, txtStadiumName, txtLeagueName, txtCoachName, txtStarPlayerName; 86 | public ImageView imgLogo; 87 | 88 | public ClubViewHolder(View view) 89 | { 90 | super(view); 91 | txtName = view.findViewById(R.id.txtName); 92 | txtLocation = view.findViewById(R.id.txtLocation); 93 | txtStadiumName = view.findViewById(R.id.txtStadiumName); 94 | txtLeagueName = view.findViewById(R.id.txtLeagueName); 95 | txtCoachName = view.findViewById(R.id.txtCoachName); 96 | txtStarPlayerName = view.findViewById(R.id.txtStarPlayerName); 97 | 98 | imgLogo = view.findViewById(R.id.imgLogo); 99 | } 100 | } 101 | 102 | @Override 103 | public Filter getFilter() 104 | { 105 | return new Filter() 106 | { 107 | @Override 108 | protected FilterResults performFiltering(CharSequence charSequence) 109 | { 110 | String charString = charSequence.toString(); 111 | if (charString.isEmpty()) 112 | { 113 | filteredClubList = clubList; 114 | } else 115 | { 116 | List filteredList = new ArrayList<>(); 117 | for (Club club : clubList) 118 | { 119 | // name match condition. this might differ depending on your requirement 120 | // here we are looking for name 121 | if (club.name.toLowerCase().contains(charString.toLowerCase()) ) 122 | { 123 | filteredList.add(club); 124 | } 125 | } 126 | 127 | filteredClubList = filteredList; 128 | } 129 | 130 | FilterResults filterResults = new FilterResults(); 131 | filterResults.values = filteredClubList; 132 | return filterResults; 133 | } 134 | 135 | @Override 136 | protected void publishResults(CharSequence charSequence, FilterResults filterResults) 137 | { 138 | filteredClubList = (ArrayList) filterResults.values; 139 | 140 | // refresh the list with filtered data 141 | notifyDataSetChanged(); 142 | } 143 | }; 144 | } 145 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bullseyedevs/tableview/model/Club.java: -------------------------------------------------------------------------------- 1 | package com.bullseyedevs.tableview.model; 2 | 3 | public class Club 4 | { 5 | public String name; 6 | public String logoUrl; 7 | public String location; 8 | public String stadiumName; 9 | public String leagueName; 10 | public String coachName; 11 | public String starPlayerName; 12 | 13 | public Club(String name, String logoUrl, String location, String stadiumName, String leagueName, String coachName, String starPlayerName) 14 | { 15 | this.name = name; 16 | this.logoUrl = logoUrl; 17 | this.location = location; 18 | this.stadiumName = stadiumName; 19 | this.leagueName = leagueName; 20 | this.coachName = coachName; 21 | this.starPlayerName = starPlayerName; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/bullseyedevs/tableview/util/FixedGridLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.bullseyedevs.tableview.util; 2 | 3 | import android.content.Context; 4 | import android.graphics.PointF; 5 | import android.support.v7.widget.LinearSmoothScroller; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.util.AttributeSet; 8 | import android.util.Log; 9 | import android.util.SparseArray; 10 | import android.util.SparseIntArray; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | 14 | import java.util.HashSet; 15 | import java.util.List; 16 | 17 | /** 18 | * A {@link android.support.v7.widget.RecyclerView.LayoutManager} implementation 19 | * that places children in a two-dimensional grid, sized to a fixed column count 20 | * value. User scrolling is possible in both horizontal and vertical directions 21 | * to view the data set. 22 | *

23 | *

The column count is controllable via {@link #setTotalColumnCount(int)}. The layout manager 24 | * will generate the number of rows necessary to accommodate the data set based on 25 | * the fixed column count. 26 | *

27 | *

This manager does make some assumptions to simplify the implementation: 28 | *

    29 | *
  • All child views are assumed to be the same size
  • 30 | *
  • The window of visible views is a constant
  • 31 | *
32 | */ 33 | 34 | /** 35 | * Copy from https://github.com/devunwired/recyclerview-playground 36 | */ 37 | 38 | public class FixedGridLayoutManager extends RecyclerView.LayoutManager 39 | { 40 | 41 | private static final String TAG = FixedGridLayoutManager.class.getSimpleName(); 42 | 43 | private static final int DEFAULT_COUNT = 1; 44 | 45 | /* View Removal Constants */ 46 | private static final int REMOVE_VISIBLE = 0; 47 | private static final int REMOVE_INVISIBLE = 1; 48 | 49 | /* Fill Direction Constants */ 50 | private static final int DIRECTION_NONE = -1; 51 | private static final int DIRECTION_START = 0; 52 | private static final int DIRECTION_END = 1; 53 | private static final int DIRECTION_UP = 2; 54 | private static final int DIRECTION_DOWN = 3; 55 | 56 | /* First (top-left) position visible at any point */ 57 | private int mFirstVisiblePosition; 58 | /* Consistent size applied to all child views */ 59 | private int mDecoratedChildWidth; 60 | private int mDecoratedChildHeight; 61 | /* Number of columns that exist in the grid */ 62 | private int mTotalColumnCount = DEFAULT_COUNT; 63 | /* Metrics for the visible window of our data */ 64 | private int mVisibleColumnCount; 65 | private int mVisibleRowCount; 66 | 67 | /* Used for tracking off-screen change events */ 68 | private int mFirstChangedPosition; 69 | private int mChangedPositionCount; 70 | 71 | /** 72 | * Set the number of columns the layout manager will use. This will 73 | * trigger a layout update. 74 | * 75 | * @param count Number of columns. 76 | */ 77 | public void setTotalColumnCount(int count) 78 | { 79 | mTotalColumnCount = count; 80 | requestLayout(); 81 | } 82 | 83 | /* 84 | * You must return true from this method if you want your 85 | * LayoutManager to support anything beyond "simple" item 86 | * animations. Enabling this causes onLayoutChildren() to 87 | * be called twice on each animated change; once for a 88 | * pre-layout, and again for the real layout. 89 | */ 90 | @Override 91 | public boolean supportsPredictiveItemAnimations() 92 | { 93 | return true; 94 | } 95 | 96 | /* 97 | * Called by RecyclerView when a view removal is triggered. This is called 98 | * before onLayoutChildren() in pre-layout if the views removed are not visible. We 99 | * use it in this case to inform pre-layout that a removal took place. 100 | * 101 | * This method is still called if the views removed were visible, but it will 102 | * happen AFTER pre-layout. 103 | */ 104 | @Override 105 | public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) 106 | { 107 | mFirstChangedPosition = positionStart; 108 | mChangedPositionCount = itemCount; 109 | } 110 | 111 | /* 112 | * This method is your initial call from the framework. You will receive it when you 113 | * need to start laying out the initial set of views. This method will not be called 114 | * repeatedly, so don't rely on it to continually process changes during user 115 | * interaction. 116 | * 117 | * This method will be called when the data set in the adapter changes, so it can be 118 | * used to update a layout based on a new item count. 119 | * 120 | * If predictive animations are enabled, you will see this called twice. First, with 121 | * state.isPreLayout() returning true to lay out children in their initial conditions. 122 | * Then again to lay out children in their final locations. 123 | */ 124 | @Override 125 | public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) 126 | { 127 | //We have nothing to show for an empty data set but clear any existing views 128 | if (getItemCount() == 0) 129 | { 130 | detachAndScrapAttachedViews(recycler); 131 | return; 132 | } 133 | if (getChildCount() == 0 && state.isPreLayout()) 134 | { 135 | //Nothing to do during prelayout when empty 136 | return; 137 | } 138 | 139 | //Clear change tracking state when a real layout occurs 140 | if (!state.isPreLayout()) 141 | { 142 | mFirstChangedPosition = mChangedPositionCount = 0; 143 | } 144 | 145 | if (getChildCount() == 0) 146 | { //First or empty layout 147 | //Scrap measure one child 148 | View scrap = recycler.getViewForPosition(0); 149 | addView(scrap); 150 | measureChildWithMargins(scrap, 0, 0); 151 | 152 | /* 153 | * We make some assumptions in this code based on every child 154 | * view being the same size (i.e. a uniform grid). This allows 155 | * us to compute the following values up front because they 156 | * won't change. 157 | */ 158 | mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap); 159 | mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap); 160 | 161 | detachAndScrapView(scrap, recycler); 162 | } 163 | 164 | //Always update the visible row/column counts 165 | updateWindowSizing(); 166 | 167 | SparseIntArray removedCache = null; 168 | /* 169 | * During pre-layout, we need to take note of any views that are 170 | * being removed in order to handle predictive animations 171 | */ 172 | if (state.isPreLayout()) 173 | { 174 | removedCache = new SparseIntArray(getChildCount()); 175 | for (int i = 0; i < getChildCount(); i++) 176 | { 177 | final View view = getChildAt(i); 178 | LayoutParams lp = (LayoutParams) view.getLayoutParams(); 179 | 180 | if (lp.isItemRemoved()) 181 | { 182 | //Track these view removals as visible 183 | removedCache.put(lp.getViewLayoutPosition(), REMOVE_VISIBLE); 184 | } 185 | } 186 | 187 | //Track view removals that happened out of bounds (i.e. off-screen) 188 | if (removedCache.size() == 0 && mChangedPositionCount > 0) 189 | { 190 | for (int i = mFirstChangedPosition; i < (mFirstChangedPosition + mChangedPositionCount); i++) 191 | { 192 | removedCache.put(i, REMOVE_INVISIBLE); 193 | } 194 | } 195 | } 196 | 197 | 198 | int childLeft; 199 | int childTop; 200 | if (getChildCount() == 0) 201 | { //First or empty layout 202 | //Reset the visible and scroll positions 203 | mFirstVisiblePosition = 0; 204 | childLeft = getPaddingLeft(); 205 | childTop = getPaddingTop(); 206 | } else if (!state.isPreLayout() 207 | && getVisibleChildCount() >= state.getItemCount()) 208 | { 209 | //Data set is too small to scroll fully, just reset position 210 | mFirstVisiblePosition = 0; 211 | childLeft = getPaddingLeft(); 212 | childTop = getPaddingTop(); 213 | } else 214 | { //Adapter data set changes 215 | /* 216 | * Keep the existing initial position, and save off 217 | * the current scrolled offset. 218 | */ 219 | final View topChild = getChildAt(0); 220 | childLeft = getDecoratedLeft(topChild); 221 | childTop = getDecoratedTop(topChild); 222 | 223 | /* 224 | * When data set is too small to scroll vertically, adjust vertical offset 225 | * and shift position to the first row, preserving current column 226 | */ 227 | if (!state.isPreLayout() && getVerticalSpace() > (getTotalRowCount() * mDecoratedChildHeight)) 228 | { 229 | mFirstVisiblePosition = mFirstVisiblePosition % getTotalColumnCount(); 230 | childTop = getPaddingTop(); 231 | 232 | //If the shift overscrolls the column max, back it off 233 | if ((mFirstVisiblePosition + mVisibleColumnCount) > state.getItemCount()) 234 | { 235 | mFirstVisiblePosition = Math.max(state.getItemCount() - mVisibleColumnCount, 0); 236 | childLeft = getPaddingLeft(); 237 | } 238 | } 239 | 240 | /* 241 | * Adjust the visible position if out of bounds in the 242 | * new layout. This occurs when the new item count in an adapter 243 | * is much smaller than it was before, and you are scrolled to 244 | * a location where no items would exist. 245 | */ 246 | int maxFirstRow = getTotalRowCount() - (mVisibleRowCount - 1); 247 | int maxFirstCol = getTotalColumnCount() - (mVisibleColumnCount - 1); 248 | boolean isOutOfRowBounds = getFirstVisibleRow() > maxFirstRow; 249 | boolean isOutOfColBounds = getFirstVisibleColumn() > maxFirstCol; 250 | if (isOutOfRowBounds || isOutOfColBounds) 251 | { 252 | int firstRow; 253 | if (isOutOfRowBounds) 254 | { 255 | firstRow = maxFirstRow; 256 | } else 257 | { 258 | firstRow = getFirstVisibleRow(); 259 | } 260 | int firstCol; 261 | if (isOutOfColBounds) 262 | { 263 | firstCol = maxFirstCol; 264 | } else 265 | { 266 | firstCol = getFirstVisibleColumn(); 267 | } 268 | mFirstVisiblePosition = firstRow * getTotalColumnCount() + firstCol; 269 | 270 | childLeft = getHorizontalSpace() - (mDecoratedChildWidth * mVisibleColumnCount); 271 | childTop = getVerticalSpace() - (mDecoratedChildHeight * mVisibleRowCount); 272 | 273 | //Correct cases where shifting to the bottom-right overscrolls the top-left 274 | // This happens on data sets too small to scroll in a direction. 275 | if (getFirstVisibleRow() == 0) 276 | { 277 | childTop = Math.min(childTop, getPaddingTop()); 278 | } 279 | if (getFirstVisibleColumn() == 0) 280 | { 281 | childLeft = Math.min(childLeft, getPaddingLeft()); 282 | } 283 | } 284 | } 285 | 286 | //Clear all attached views into the recycle bin 287 | detachAndScrapAttachedViews(recycler); 288 | 289 | //Fill the grid for the initial layout of views 290 | fillGrid(DIRECTION_NONE, childLeft, childTop, recycler, state, removedCache); 291 | 292 | //Evaluate any disappearing views that may exist 293 | if (!state.isPreLayout() && !recycler.getScrapList().isEmpty()) 294 | { 295 | final List scrapList = recycler.getScrapList(); 296 | final HashSet disappearingViews = new HashSet(scrapList.size()); 297 | 298 | for (RecyclerView.ViewHolder holder : scrapList) 299 | { 300 | final View child = holder.itemView; 301 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 302 | if (!lp.isItemRemoved()) 303 | { 304 | disappearingViews.add(child); 305 | } 306 | } 307 | 308 | for (View child : disappearingViews) 309 | { 310 | layoutDisappearingView(child); 311 | } 312 | } 313 | } 314 | 315 | @Override 316 | public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) 317 | { 318 | //Completely scrap the existing layout 319 | removeAllViews(); 320 | } 321 | 322 | /* 323 | * Rather than continuously checking how many views we can fit 324 | * based on scroll offsets, we simplify the math by computing the 325 | * visible grid as what will initially fit on screen, plus one. 326 | */ 327 | private void updateWindowSizing() 328 | { 329 | mVisibleColumnCount = (getHorizontalSpace() / mDecoratedChildWidth) + 1; 330 | if (getHorizontalSpace() % mDecoratedChildWidth > 0) 331 | { 332 | mVisibleColumnCount++; 333 | } 334 | 335 | //Allow minimum value for small data sets 336 | if (mVisibleColumnCount > getTotalColumnCount()) 337 | { 338 | mVisibleColumnCount = getTotalColumnCount(); 339 | } 340 | 341 | 342 | mVisibleRowCount = (getVerticalSpace() / mDecoratedChildHeight) + 1; 343 | if (getVerticalSpace() % mDecoratedChildHeight > 0) 344 | { 345 | mVisibleRowCount++; 346 | } 347 | 348 | if (mVisibleRowCount > getTotalRowCount()) 349 | { 350 | mVisibleRowCount = getTotalRowCount(); 351 | } 352 | } 353 | 354 | private void fillGrid(int direction, RecyclerView.Recycler recycler, RecyclerView.State state) 355 | { 356 | fillGrid(direction, 0, 0, recycler, state, null); 357 | } 358 | 359 | private void fillGrid(int direction, int emptyLeft, int emptyTop, 360 | RecyclerView.Recycler recycler, 361 | RecyclerView.State state, 362 | SparseIntArray removedPositions) 363 | { 364 | if (mFirstVisiblePosition < 0) mFirstVisiblePosition = 0; 365 | if (mFirstVisiblePosition >= getItemCount()) mFirstVisiblePosition = (getItemCount() - 1); 366 | 367 | /* 368 | * First, we will detach all existing views from the layout. 369 | * detachView() is a lightweight operation that we can use to 370 | * quickly reorder views without a full add/remove. 371 | */ 372 | SparseArray viewCache = new SparseArray(getChildCount()); 373 | int startLeftOffset = emptyLeft; 374 | int startTopOffset = emptyTop; 375 | if (getChildCount() != 0) 376 | { 377 | final View topView = getChildAt(0); 378 | startLeftOffset = getDecoratedLeft(topView); 379 | startTopOffset = getDecoratedTop(topView); 380 | switch (direction) 381 | { 382 | case DIRECTION_START: 383 | startLeftOffset -= mDecoratedChildWidth; 384 | break; 385 | case DIRECTION_END: 386 | startLeftOffset += mDecoratedChildWidth; 387 | break; 388 | case DIRECTION_UP: 389 | startTopOffset -= mDecoratedChildHeight; 390 | break; 391 | case DIRECTION_DOWN: 392 | startTopOffset += mDecoratedChildHeight; 393 | break; 394 | } 395 | 396 | //Cache all views by their existing position, before updating counts 397 | for (int i = 0; i < getChildCount(); i++) 398 | { 399 | int position = positionOfIndex(i); 400 | final View child = getChildAt(i); 401 | viewCache.put(position, child); 402 | } 403 | 404 | //Temporarily detach all views. 405 | // Views we still need will be added back at the proper index. 406 | for (int i = 0; i < viewCache.size(); i++) 407 | { 408 | detachView(viewCache.valueAt(i)); 409 | } 410 | } 411 | 412 | /* 413 | * Next, we advance the visible position based on the fill direction. 414 | * DIRECTION_NONE doesn't advance the position in any direction. 415 | */ 416 | switch (direction) 417 | { 418 | case DIRECTION_START: 419 | mFirstVisiblePosition--; 420 | break; 421 | case DIRECTION_END: 422 | mFirstVisiblePosition++; 423 | break; 424 | case DIRECTION_UP: 425 | mFirstVisiblePosition -= getTotalColumnCount(); 426 | break; 427 | case DIRECTION_DOWN: 428 | mFirstVisiblePosition += getTotalColumnCount(); 429 | break; 430 | } 431 | 432 | /* 433 | * Next, we supply the grid of items that are deemed visible. 434 | * If these items were previously there, they will simply be 435 | * re-attached. New views that must be created are obtained 436 | * from the Recycler and added. 437 | */ 438 | int leftOffset = startLeftOffset; 439 | int topOffset = startTopOffset; 440 | 441 | for (int i = 0; i < getVisibleChildCount(); i++) 442 | { 443 | int nextPosition = positionOfIndex(i); 444 | 445 | /* 446 | * When a removal happens out of bounds, the pre-layout positions of items 447 | * after the removal are shifted to their final positions ahead of schedule. 448 | * We have to track off-screen removals and shift those positions back 449 | * so we can properly lay out all current (and appearing) views in their 450 | * initial locations. 451 | */ 452 | int offsetPositionDelta = 0; 453 | if (state.isPreLayout()) 454 | { 455 | int offsetPosition = nextPosition; 456 | 457 | for (int offset = 0; offset < removedPositions.size(); offset++) 458 | { 459 | //Look for off-screen removals that are less-than this 460 | if (removedPositions.valueAt(offset) == REMOVE_INVISIBLE 461 | && removedPositions.keyAt(offset) < nextPosition) 462 | { 463 | //Offset position to match 464 | offsetPosition--; 465 | } 466 | } 467 | offsetPositionDelta = nextPosition - offsetPosition; 468 | nextPosition = offsetPosition; 469 | } 470 | 471 | if (nextPosition < 0 || nextPosition >= state.getItemCount()) 472 | { 473 | //Item space beyond the data set, don't attempt to add a view 474 | continue; 475 | } 476 | 477 | //Layout this position 478 | View view = viewCache.get(nextPosition); 479 | if (view == null) 480 | { 481 | /* 482 | * The Recycler will give us either a newly constructed view, 483 | * or a recycled view it has on-hand. In either case, the 484 | * view will already be fully bound to the data by the 485 | * adapter for us. 486 | */ 487 | view = recycler.getViewForPosition(nextPosition); 488 | addView(view); 489 | 490 | /* 491 | * Update the new view's metadata, but only when this is a real 492 | * layout pass. 493 | */ 494 | if (!state.isPreLayout()) 495 | { 496 | LayoutParams lp = (LayoutParams) view.getLayoutParams(); 497 | lp.row = getGlobalRowOfPosition(nextPosition); 498 | lp.column = getGlobalColumnOfPosition(nextPosition); 499 | } 500 | 501 | /* 502 | * It is prudent to measure/layout each new view we 503 | * receive from the Recycler. We don't have to do 504 | * this for views we are just re-arranging. 505 | */ 506 | measureChildWithMargins(view, 0, 0); 507 | layoutDecorated(view, leftOffset, topOffset, 508 | leftOffset + mDecoratedChildWidth, 509 | topOffset + mDecoratedChildHeight); 510 | 511 | } else 512 | { 513 | //Re-attach the cached view at its new index 514 | attachView(view); 515 | viewCache.remove(nextPosition); 516 | } 517 | 518 | if (i % mVisibleColumnCount == (mVisibleColumnCount - 1)) 519 | { 520 | leftOffset = startLeftOffset; 521 | topOffset += mDecoratedChildHeight; 522 | 523 | //During pre-layout, on each column end, apply any additional appearing views 524 | if (state.isPreLayout()) 525 | { 526 | layoutAppearingViews(recycler, view, nextPosition, removedPositions.size(), offsetPositionDelta); 527 | } 528 | } else 529 | { 530 | leftOffset += mDecoratedChildWidth; 531 | } 532 | } 533 | 534 | /* 535 | * Finally, we ask the Recycler to scrap and store any views 536 | * that we did not re-attach. These are views that are not currently 537 | * necessary because they are no longer visible. 538 | */ 539 | for (int i = 0; i < viewCache.size(); i++) 540 | { 541 | final View removingView = viewCache.valueAt(i); 542 | recycler.recycleView(removingView); 543 | } 544 | } 545 | 546 | /* 547 | * You must override this method if you would like to support external calls 548 | * to shift the view to a given adapter position. In our implementation, this 549 | * is the same as doing a fresh layout with the given position as the top-left 550 | * (or first visible), so we simply set that value and trigger onLayoutChildren() 551 | */ 552 | @Override 553 | public void scrollToPosition(int position) 554 | { 555 | if (position >= getItemCount()) 556 | { 557 | Log.e(TAG, "Cannot scroll to " + position + ", item count is " + getItemCount()); 558 | return; 559 | } 560 | 561 | //Set requested position as first visible 562 | mFirstVisiblePosition = position; 563 | //Toss all existing views away 564 | removeAllViews(); 565 | //Trigger a new view layout 566 | requestLayout(); 567 | } 568 | 569 | /* 570 | * You must override this method if you would like to support external calls 571 | * to animate a change to a new adapter position. The framework provides a 572 | * helper scroller implementation (LinearSmoothScroller), which we leverage 573 | * to do the animation calculations. 574 | */ 575 | @Override 576 | public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, final int position) 577 | { 578 | if (position >= getItemCount()) 579 | { 580 | Log.e(TAG, "Cannot scroll to " + position + ", item count is " + getItemCount()); 581 | return; 582 | } 583 | 584 | /* 585 | * LinearSmoothScroller's default behavior is to scroll the contents until 586 | * the child is fully visible. It will snap to the top-left or bottom-right 587 | * of the parent depending on whether the direction of travel was positive 588 | * or negative. 589 | */ 590 | LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) 591 | { 592 | /* 593 | * LinearSmoothScroller, at a minimum, just need to know the vector 594 | * (x/y distance) to travel in order to get from the current positioning 595 | * to the target. 596 | */ 597 | @Override 598 | public PointF computeScrollVectorForPosition(int targetPosition) 599 | { 600 | final int rowOffset = getGlobalRowOfPosition(targetPosition) 601 | - getGlobalRowOfPosition(mFirstVisiblePosition); 602 | final int columnOffset = getGlobalColumnOfPosition(targetPosition) 603 | - getGlobalColumnOfPosition(mFirstVisiblePosition); 604 | 605 | return new PointF(columnOffset * mDecoratedChildWidth, rowOffset * mDecoratedChildHeight); 606 | } 607 | }; 608 | scroller.setTargetPosition(position); 609 | startSmoothScroll(scroller); 610 | } 611 | 612 | /* 613 | * Use this method to tell the RecyclerView if scrolling is even possible 614 | * in the horizontal direction. 615 | */ 616 | @Override 617 | public boolean canScrollHorizontally() 618 | { 619 | //We do allow scrolling 620 | return true; 621 | } 622 | 623 | /* 624 | * This method describes how far RecyclerView thinks the contents should scroll horizontally. 625 | * You are responsible for verifying edge boundaries, and determining if this scroll 626 | * event somehow requires that new views be added or old views get recycled. 627 | */ 628 | @Override 629 | public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) 630 | { 631 | if (getChildCount() == 0) 632 | { 633 | return 0; 634 | } 635 | 636 | //Take leftmost measurements from the top-left child 637 | final View topView = getChildAt(0); 638 | //Take rightmost measurements from the top-right child 639 | final View bottomView = getChildAt(mVisibleColumnCount - 1); 640 | 641 | //Optimize the case where the entire data set is too small to scroll 642 | int viewSpan = getDecoratedRight(bottomView) - getDecoratedLeft(topView); 643 | if (viewSpan < getHorizontalSpace()) 644 | { 645 | //We cannot scroll in either direction 646 | return 0; 647 | } 648 | 649 | int delta; 650 | boolean leftBoundReached = getFirstVisibleColumn() == 0; 651 | boolean rightBoundReached = getLastVisibleColumn() >= getTotalColumnCount(); 652 | if (dx > 0) 653 | { // Contents are scrolling left 654 | //Check right bound 655 | if (rightBoundReached) 656 | { 657 | //If we've reached the last column, enforce limits 658 | int rightOffset = getHorizontalSpace() - getDecoratedRight(bottomView) + getPaddingRight(); 659 | delta = Math.max(-dx, rightOffset); 660 | } else 661 | { 662 | //No limits while the last column isn't visible 663 | delta = -dx; 664 | } 665 | } else 666 | { // Contents are scrolling right 667 | //Check left bound 668 | if (leftBoundReached) 669 | { 670 | int leftOffset = -getDecoratedLeft(topView) + getPaddingLeft(); 671 | delta = Math.min(-dx, leftOffset); 672 | } else 673 | { 674 | delta = -dx; 675 | } 676 | } 677 | 678 | offsetChildrenHorizontal(delta); 679 | 680 | if (dx > 0) 681 | { 682 | if (getDecoratedRight(topView) < 0 && !rightBoundReached) 683 | { 684 | fillGrid(DIRECTION_END, recycler, state); 685 | } else if (!rightBoundReached) 686 | { 687 | fillGrid(DIRECTION_NONE, recycler, state); 688 | } 689 | } else 690 | { 691 | if (getDecoratedLeft(topView) > 0 && !leftBoundReached) 692 | { 693 | fillGrid(DIRECTION_START, recycler, state); 694 | } else if (!leftBoundReached) 695 | { 696 | fillGrid(DIRECTION_NONE, recycler, state); 697 | } 698 | } 699 | 700 | /* 701 | * Return value determines if a boundary has been reached 702 | * (for edge effects and flings). If returned value does not 703 | * match original delta (passed in), RecyclerView will draw 704 | * an edge effect. 705 | */ 706 | return -delta; 707 | } 708 | 709 | /* 710 | * Use this method to tell the RecyclerView if scrolling is even possible 711 | * in the vertical direction. 712 | */ 713 | @Override 714 | public boolean canScrollVertically() 715 | { 716 | //We do allow scrolling 717 | return true; 718 | } 719 | 720 | /* 721 | * This method describes how far RecyclerView thinks the contents should scroll vertically. 722 | * You are responsible for verifying edge boundaries, and determining if this scroll 723 | * event somehow requires that new views be added or old views get recycled. 724 | */ 725 | @Override 726 | public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) 727 | { 728 | if (getChildCount() == 0) 729 | { 730 | return 0; 731 | } 732 | 733 | //Take top measurements from the top-left child 734 | final View topView = getChildAt(0); 735 | //Take bottom measurements from the bottom-right child. 736 | final View bottomView = getChildAt(getChildCount() - 1); 737 | 738 | //Optimize the case where the entire data set is too small to scroll 739 | int viewSpan = getDecoratedBottom(bottomView) - getDecoratedTop(topView); 740 | if (viewSpan < getVerticalSpace()) 741 | { 742 | //We cannot scroll in either direction 743 | return 0; 744 | } 745 | 746 | int delta; 747 | int maxRowCount = getTotalRowCount(); 748 | boolean topBoundReached = getFirstVisibleRow() == 0; 749 | boolean bottomBoundReached = getLastVisibleRow() >= maxRowCount; 750 | if (dy > 0) 751 | { // Contents are scrolling up 752 | //Check against bottom bound 753 | if (bottomBoundReached) 754 | { 755 | //If we've reached the last row, enforce limits 756 | int bottomOffset; 757 | if (rowOfIndex(getChildCount() - 1) >= (maxRowCount - 1)) 758 | { 759 | //We are truly at the bottom, determine how far 760 | bottomOffset = getVerticalSpace() - getDecoratedBottom(bottomView) 761 | + getPaddingBottom(); 762 | } else 763 | { 764 | /* 765 | * Extra space added to account for allowing bottom space in the grid. 766 | * This occurs when the overlap in the last row is not large enough to 767 | * ensure that at least one element in that row isn't fully recycled. 768 | */ 769 | bottomOffset = getVerticalSpace() - (getDecoratedBottom(bottomView) 770 | + mDecoratedChildHeight) + getPaddingBottom(); 771 | } 772 | 773 | delta = Math.max(-dy, bottomOffset); 774 | } else 775 | { 776 | //No limits while the last row isn't visible 777 | delta = -dy; 778 | } 779 | } else 780 | { // Contents are scrolling down 781 | //Check against top bound 782 | if (topBoundReached) 783 | { 784 | int topOffset = -getDecoratedTop(topView) + getPaddingTop(); 785 | 786 | delta = Math.min(-dy, topOffset); 787 | } else 788 | { 789 | delta = -dy; 790 | } 791 | } 792 | 793 | offsetChildrenVertical(delta); 794 | 795 | if (dy > 0) 796 | { 797 | if (getDecoratedBottom(topView) < 0 && !bottomBoundReached) 798 | { 799 | fillGrid(DIRECTION_DOWN, recycler, state); 800 | } else if (!bottomBoundReached) 801 | { 802 | fillGrid(DIRECTION_NONE, recycler, state); 803 | } 804 | } else 805 | { 806 | if (getDecoratedTop(topView) > 0 && !topBoundReached) 807 | { 808 | fillGrid(DIRECTION_UP, recycler, state); 809 | } else if (!topBoundReached) 810 | { 811 | fillGrid(DIRECTION_NONE, recycler, state); 812 | } 813 | } 814 | 815 | /* 816 | * Return value determines if a boundary has been reached 817 | * (for edge effects and flings). If returned value does not 818 | * match original delta (passed in), RecyclerView will draw 819 | * an edge effect. 820 | */ 821 | return -delta; 822 | } 823 | 824 | /* 825 | * This is a helper method used by RecyclerView to determine 826 | * if a specific child view can be returned. 827 | */ 828 | @Override 829 | public View findViewByPosition(int position) 830 | { 831 | for (int i = 0; i < getChildCount(); i++) 832 | { 833 | if (positionOfIndex(i) == position) 834 | { 835 | return getChildAt(i); 836 | } 837 | } 838 | 839 | return null; 840 | } 841 | 842 | /** 843 | * Boilerplate to extend LayoutParams for tracking row/column of attached views 844 | */ 845 | 846 | /* 847 | * Even without extending LayoutParams, we must override this method 848 | * to provide the default layout parameters that each child view 849 | * will receive when added. 850 | */ 851 | @Override 852 | public RecyclerView.LayoutParams generateDefaultLayoutParams() 853 | { 854 | return new LayoutParams( 855 | ViewGroup.LayoutParams.WRAP_CONTENT, 856 | ViewGroup.LayoutParams.WRAP_CONTENT); 857 | } 858 | 859 | @Override 860 | public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) 861 | { 862 | return new LayoutParams(c, attrs); 863 | } 864 | 865 | @Override 866 | public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) 867 | { 868 | if (lp instanceof ViewGroup.MarginLayoutParams) 869 | { 870 | return new LayoutParams((ViewGroup.MarginLayoutParams) lp); 871 | } else 872 | { 873 | return new LayoutParams(lp); 874 | } 875 | } 876 | 877 | @Override 878 | public boolean checkLayoutParams(RecyclerView.LayoutParams lp) 879 | { 880 | return lp instanceof LayoutParams; 881 | } 882 | 883 | public static class LayoutParams extends RecyclerView.LayoutParams 884 | { 885 | 886 | //Current row in the grid 887 | public int row; 888 | //Current column in the grid 889 | public int column; 890 | 891 | public LayoutParams(Context c, AttributeSet attrs) 892 | { 893 | super(c, attrs); 894 | } 895 | 896 | public LayoutParams(int width, int height) 897 | { 898 | super(width, height); 899 | } 900 | 901 | public LayoutParams(ViewGroup.MarginLayoutParams source) 902 | { 903 | super(source); 904 | } 905 | 906 | public LayoutParams(ViewGroup.LayoutParams source) 907 | { 908 | super(source); 909 | } 910 | 911 | public LayoutParams(RecyclerView.LayoutParams source) 912 | { 913 | super(source); 914 | } 915 | } 916 | 917 | /** 918 | * Animation Layout Helpers 919 | */ 920 | 921 | /* Helper to obtain and place extra appearing views */ 922 | private void layoutAppearingViews(RecyclerView.Recycler recycler, View referenceView, int referencePosition, int extraCount, int offset) 923 | { 924 | //Nothing to do... 925 | if (extraCount < 1) return; 926 | 927 | //FIXME: This code currently causes double layout of views that are still visible… 928 | for (int extra = 1; extra <= extraCount; extra++) 929 | { 930 | //Grab the next position after the reference 931 | final int extraPosition = referencePosition + extra; 932 | if (extraPosition < 0 || extraPosition >= getItemCount()) 933 | { 934 | //Can't do anything with this 935 | continue; 936 | } 937 | 938 | /* 939 | * Obtain additional position views that we expect to appear 940 | * as part of the animation. 941 | */ 942 | View appearing = recycler.getViewForPosition(extraPosition); 943 | addView(appearing); 944 | 945 | //Find layout delta from reference position 946 | final int newRow = getGlobalRowOfPosition(extraPosition + offset); 947 | final int rowDelta = newRow - getGlobalRowOfPosition(referencePosition + offset); 948 | final int newCol = getGlobalColumnOfPosition(extraPosition + offset); 949 | final int colDelta = newCol - getGlobalColumnOfPosition(referencePosition + offset); 950 | 951 | layoutTempChildView(appearing, rowDelta, colDelta, referenceView); 952 | } 953 | } 954 | 955 | /* Helper to place a disappearing view */ 956 | private void layoutDisappearingView(View disappearingChild) 957 | { 958 | /* 959 | * LayoutManager has a special method for attaching views that 960 | * will only be around long enough to animate. 961 | */ 962 | addDisappearingView(disappearingChild); 963 | 964 | //Adjust each disappearing view to its proper place 965 | final LayoutParams lp = (LayoutParams) disappearingChild.getLayoutParams(); 966 | 967 | final int newRow = getGlobalRowOfPosition(lp.getViewAdapterPosition()); 968 | final int rowDelta = newRow - lp.row; 969 | final int newCol = getGlobalColumnOfPosition(lp.getViewAdapterPosition()); 970 | final int colDelta = newCol - lp.column; 971 | 972 | layoutTempChildView(disappearingChild, rowDelta, colDelta, disappearingChild); 973 | } 974 | 975 | 976 | /* Helper to lay out appearing/disappearing children */ 977 | private void layoutTempChildView(View child, int rowDelta, int colDelta, View referenceView) 978 | { 979 | //Set the layout position to the global row/column difference from the reference view 980 | int layoutTop = getDecoratedTop(referenceView) + rowDelta * mDecoratedChildHeight; 981 | int layoutLeft = getDecoratedLeft(referenceView) + colDelta * mDecoratedChildWidth; 982 | 983 | measureChildWithMargins(child, 0, 0); 984 | layoutDecorated(child, layoutLeft, layoutTop, 985 | layoutLeft + mDecoratedChildWidth, 986 | layoutTop + mDecoratedChildHeight); 987 | } 988 | 989 | /** 990 | * Private Helpers and Metrics Accessors 991 | */ 992 | 993 | /* Return the overall column index of this position in the global layout */ 994 | private int getGlobalColumnOfPosition(int position) 995 | { 996 | return position % mTotalColumnCount; 997 | } 998 | 999 | /* Return the overall row index of this position in the global layout */ 1000 | private int getGlobalRowOfPosition(int position) 1001 | { 1002 | return position / mTotalColumnCount; 1003 | } 1004 | 1005 | /* 1006 | * Mapping between child view indices and adapter data 1007 | * positions helps fill the proper views during scrolling. 1008 | */ 1009 | private int positionOfIndex(int childIndex) 1010 | { 1011 | int row = childIndex / mVisibleColumnCount; 1012 | int column = childIndex % mVisibleColumnCount; 1013 | 1014 | return mFirstVisiblePosition + (row * getTotalColumnCount()) + column; 1015 | } 1016 | 1017 | private int rowOfIndex(int childIndex) 1018 | { 1019 | int position = positionOfIndex(childIndex); 1020 | 1021 | return position / getTotalColumnCount(); 1022 | } 1023 | 1024 | private int getFirstVisibleColumn() 1025 | { 1026 | return (mFirstVisiblePosition % getTotalColumnCount()); 1027 | } 1028 | 1029 | private int getLastVisibleColumn() 1030 | { 1031 | return getFirstVisibleColumn() + mVisibleColumnCount; 1032 | } 1033 | 1034 | private int getFirstVisibleRow() 1035 | { 1036 | return (mFirstVisiblePosition / getTotalColumnCount()); 1037 | } 1038 | 1039 | private int getLastVisibleRow() 1040 | { 1041 | return getFirstVisibleRow() + mVisibleRowCount; 1042 | } 1043 | 1044 | private int getVisibleChildCount() 1045 | { 1046 | return mVisibleColumnCount * mVisibleRowCount; 1047 | } 1048 | 1049 | private int getTotalColumnCount() 1050 | { 1051 | if (getItemCount() < mTotalColumnCount) 1052 | { 1053 | return getItemCount(); 1054 | } 1055 | 1056 | return mTotalColumnCount; 1057 | } 1058 | 1059 | private int getTotalRowCount() 1060 | { 1061 | if (getItemCount() == 0 || mTotalColumnCount == 0) 1062 | { 1063 | return 0; 1064 | } 1065 | int maxRow = getItemCount() / mTotalColumnCount; 1066 | //Bump the row count if it's not exactly even 1067 | if (getItemCount() % mTotalColumnCount != 0) 1068 | { 1069 | maxRow++; 1070 | } 1071 | 1072 | return maxRow; 1073 | } 1074 | 1075 | private int getHorizontalSpace() 1076 | { 1077 | return getWidth() - getPaddingRight() - getPaddingLeft(); 1078 | } 1079 | 1080 | private int getVerticalSpace() 1081 | { 1082 | return getHeight() - getPaddingBottom() - getPaddingTop(); 1083 | } 1084 | } 1085 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 19 | 20 | 30 | 31 | 41 | 42 | 52 | 53 | 63 | 64 | 74 | 75 | 85 | 86 | 96 | 97 | 98 | 99 | 100 | 101 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /app/src/main/res/layout/row_club.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 23 | 24 | 31 | 32 | 39 | 40 | 47 | 48 | 55 | 56 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/layout/row_club_colorful.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 23 | 24 | 31 | 32 | 39 | 40 | 47 | 48 | 55 | 56 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brkckr/TableView/960bfd6bd4ef9af026e023f55c6476a6d49cc81e/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brkckr/TableView/960bfd6bd4ef9af026e023f55c6476a6d49cc81e/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brkckr/TableView/960bfd6bd4ef9af026e023f55c6476a6d49cc81e/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brkckr/TableView/960bfd6bd4ef9af026e023f55c6476a6d49cc81e/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brkckr/TableView/960bfd6bd4ef9af026e023f55c6476a6d49cc81e/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brkckr/TableView/960bfd6bd4ef9af026e023f55c6476a6d49cc81e/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brkckr/TableView/960bfd6bd4ef9af026e023f55c6476a6d49cc81e/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brkckr/TableView/960bfd6bd4ef9af026e023f55c6476a6d49cc81e/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brkckr/TableView/960bfd6bd4ef9af026e023f55c6476a6d49cc81e/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brkckr/TableView/960bfd6bd4ef9af026e023f55c6476a6d49cc81e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #009688 4 | #00796B 5 | #B2DFDB 6 | #00BCD4 7 | #212121 8 | #757575 9 | #BDBDBD 10 | #E5E6EC 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 80dp 4 | 60dp 5 | 120dp 6 | 5dp 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TableView 3 | 4 | Name 5 | Logo 6 | Location 7 | Stadium 8 | League 9 | Coach 10 | Star 11 | 12 | Search by Club Name 13 | Search 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/xml/searchable.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/test/java/com/bullseyedevs/tableview/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.bullseyedevs.tableview; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest 13 | { 14 | @Test 15 | public void addition_isCorrect() 16 | { 17 | assertEquals(4, 2 + 2); 18 | } 19 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.1.1' 11 | 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brkckr/TableView/960bfd6bd4ef9af026e023f55c6476a6d49cc81e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Apr 15 12:10:15 MSK 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------