├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── lishide │ │ └── scrollrecyclerview │ │ ├── AppBean.java │ │ ├── AppBeanAdapter.java │ │ └── StagGridActivity.java │ └── res │ ├── drawable-hdpi │ └── home_apps_focused.9.png │ ├── drawable │ ├── home_apps_unfocused.xml │ └── selector_home_apps.xml │ ├── layout │ ├── activity_stag_grid.xml │ └── item_grid_apps.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 ├── art └── ScrollRecyclerView_art.gif ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── recyclerview-scroll-lib ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── lishide │ │ └── recyclerview │ │ └── scroll │ │ ├── ScrollRecyclerView.java │ │ ├── SpaceItemDecoration.java │ │ └── listener │ │ ├── OnItemClickListener.java │ │ ├── OnItemKeyListener.java │ │ ├── OnItemLongClickListener.java │ │ └── OnItemSelectedListener.java │ └── res │ └── values │ └── strings.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScrollRecyclerView 2 | RecyclerView 横向 / 纵向滚动网格布局,适用于 Android 平板、Android TV 或其他定制化 Android 设备等,使用遥控器方向导航键控制列表滑动及 item 选择状态。 3 | 4 | ## 效果预览 5 | ![ScrollRecyclerView 效果演示](https://github.com/lishide/ScrollRecyclerView/raw/master/art/ScrollRecyclerView_art.gif "效果预览") 6 | --- 7 | 8 | [![](https://jitpack.io/v/lishide/ScrollRecyclerView.svg)](https://jitpack.io/#lishide/ScrollRecyclerView) 9 | ## 依赖 10 | #### JitPack 引入方法 11 | ##### 1. 在 Project 下的 build.gradle 添加 12 | ```groovy 13 | allprojects { 14 | repositories { 15 | ... 16 | maven { url 'https://jitpack.io' } 17 | } 18 | } 19 | ``` 20 | 21 | ##### 2. 在 Module 下的 build.gradle 添加 22 | ```groovy 23 | dependencies { 24 | compile 'com.github.lishide:ScrollRecyclerView:v1.0.0' 25 | } 26 | ``` 27 | 28 | ## 使用 29 | 30 | * **在 xml 中引用 ScrollRecyclerView** 31 | 32 | ```xml 33 | 37 | ``` 38 | 39 | * **初始化 ScrollRecyclerView,设置布局管理器、间距、适配器、数据等** 40 | 41 | ```java 42 | // 初始化 ScrollRecyclerView 43 | mScrollRecyclerView = (ScrollRecyclerView) findViewById(R.id.scroll_recycler_view); 44 | // 设置动画 45 | mScrollRecyclerView.setItemAnimator(new DefaultItemAnimator()); 46 | // 设置布局管理器:瀑布流式 47 | mScrollRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(3, 48 | StaggeredGridLayoutManager.HORIZONTAL)); 49 | // 根据需要设置间距等 50 | int right = (int) getResources().getDimension(R.dimen.dp_20); 51 | int bottom = (int) getResources().getDimension(R.dimen.dp_20); 52 | RecyclerView.ItemDecoration spacingInPixel = new SpaceItemDecoration(right, bottom); 53 | mScrollRecyclerView.addItemDecoration(spacingInPixel); 54 | ``` 55 | 56 | * **适配器中初始化控件并设置数据** 57 | 58 | 使用的 Adapter 就是正常的 Adapter。为了简单明了,在示例中用的是最普通的一种。当然了,你完全可以使用你常用的或是被封装过的高级 Adapter。 59 | 60 | Adapter 中**比较重要的是**设置 itemView 可以获得焦点,并监听焦点变化。还有要设置 Tag,用来标记 item 的 position,后面有用。 61 | 62 | ```java 63 | holder.itemView.setFocusable(true); 64 | holder.itemView.setTag(position); 65 | holder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() { 66 | @Override 67 | public void onFocusChange(View v, boolean hasFocus) { 68 | 69 | } 70 | }); 71 | ``` 72 | 73 | 会发现在 Adapter 中设置了许多监听器,目前有这四个: 74 | * **item 选定监听(OnItemSelectedListener)** 75 | * **item 点击监听(OnItemClickListener)** 76 | * **item 长按监听(OnItemLongClickListener)** 77 | * **遥控器其他按键监听(OnItemKeyListener)** 78 | 79 | 已将这四个 **Listener** 放在 lib 中,开发者根据需要直接设置和调用即可。 80 | 81 | **要想实现咱们今天主要实现的功能——使用遥控器方向导航键控制列表滑动及 item 选择状态,下面的步骤很重要。** 82 | * 在焦点监听器中,判断获得焦点时调用 `mOnItemSelectedListener.OnItemSelected(v, currentPosition);`,传入 view 和当前 position。 83 | 84 | ```java 85 | if (hasFocus) { 86 | currentPosition = (int) holder.itemView.getTag(); 87 | 88 | mOnItemSelectedListener.OnItemSelected(v, currentPosition); 89 | } 90 | ``` 91 | * **滑动列表** 92 | 93 | 设置 item 选定监听器,然后在监听器中实现列表滑动。 94 | 95 | ```java 96 | adapter.setOnItemSelectedListener(mOnItemSelectedListener); 97 | ``` 98 | ```java 99 | OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() { 100 | @Override 101 | public void OnItemSelected(View view, int position) { 102 | mScrollRecyclerView.smoothHorizontalScrollToNext(position); 103 | } 104 | }; 105 | ``` 106 | 107 | > 实现滑动的功能就是这个`smoothHorizontalScrollToNext`方法了。具体的实现过程推荐下载 demo 看代码,demo 里面的注释非常全了。 108 | 109 | 好了,至此 **使用遥控器方向导航键控制列表滑动及 item 选择状态** 的功能大概完成了。顺便解释一下其他几个 Listener 的作用,OnItemClickListener、OnItemLongClickListener 这两个好理解,在其他的列表点击监听也会遇到过,就不过多说了。需要注意一点的是,item 长按监听要对 view 的点击事件返回值根据需要处理一下,比如 `return false` 会在长按事件结束后再触发一次点击事件,`return true`则只会触发长按事件。如果有特定需要,比如焦点在列表的某个 item 上时,按下了 OK 键,需要跳转到一个新的界面;或者按下 Menu 键做其他业务逻辑处理等等,此时应该设置监听——OnItemKeyListener(遥控器其他按键监听)。 110 | 111 | **ScrollRecyclerView 的用处有一些局限性,手机端应该是用不到(我感觉,因为有触摸屏~),主要适用于 Android 平板、Android TV 或其他定制化 Android 设备等......这里,仍期待得到您的支持!** 112 | 113 | **您在使用过程中,发现 bug 或有好的建议欢迎 issue、email (lishidezy@gmail.com),如果感觉对你有帮助也欢迎点个 star,留下点印记吧。** 114 | 115 | 116 | [1]: https://github.com/lishide/ScrollRecyclerView 117 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 29 5 | buildToolsVersion "29.0.3" 6 | defaultConfig { 7 | applicationId "com.lishide.scrollrecyclerview" 8 | minSdkVersion 15 9 | targetSdkVersion 29 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation fileTree(include: ['*.jar'], dir: 'libs') 24 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 25 | implementation project(':recyclerview-scroll-lib') 26 | } 27 | -------------------------------------------------------------------------------- /app/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 C:\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 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/lishide/scrollrecyclerview/AppBean.java: -------------------------------------------------------------------------------- 1 | package com.lishide.scrollrecyclerview; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | public class AppBean { 6 | private Drawable appIcon; 7 | private String appName; 8 | private int appSize; 9 | private boolean isSd = false; 10 | private boolean isSystem = false; 11 | private String appPackageName; 12 | 13 | public String getApkPath() { 14 | return apkPath; 15 | } 16 | 17 | public void setApkPath(String apkPath) { 18 | this.apkPath = apkPath; 19 | } 20 | 21 | private String apkPath; 22 | 23 | public String getAppPackageName() { 24 | return appPackageName; 25 | } 26 | 27 | public void setAppPackageName(String appPackageName) { 28 | this.appPackageName = appPackageName; 29 | } 30 | 31 | public Drawable getAppIcon() { 32 | return appIcon; 33 | } 34 | 35 | public void setAppIcon(Drawable appIcon) { 36 | this.appIcon = appIcon; 37 | } 38 | 39 | public String getAppName() { 40 | return appName; 41 | } 42 | 43 | public void setAppName(String appName) { 44 | this.appName = appName; 45 | } 46 | 47 | public int getAppSize() { 48 | return appSize; 49 | } 50 | 51 | public void setAppSize(int appSize) { 52 | this.appSize = appSize; 53 | } 54 | 55 | public boolean isSd() { 56 | return isSd; 57 | } 58 | 59 | public void setSd(boolean sd) { 60 | isSd = sd; 61 | } 62 | 63 | public boolean isSystem() { 64 | return isSystem; 65 | } 66 | 67 | public void setSystem(boolean system) { 68 | isSystem = system; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/lishide/scrollrecyclerview/AppBeanAdapter.java: -------------------------------------------------------------------------------- 1 | package com.lishide.scrollrecyclerview; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | import android.view.KeyEvent; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | import android.widget.TextView; 11 | 12 | import androidx.recyclerview.widget.RecyclerView; 13 | 14 | import com.lishide.recyclerview.scroll.listener.OnItemClickListener; 15 | import com.lishide.recyclerview.scroll.listener.OnItemKeyListener; 16 | import com.lishide.recyclerview.scroll.listener.OnItemLongClickListener; 17 | import com.lishide.recyclerview.scroll.listener.OnItemSelectedListener; 18 | 19 | import java.util.List; 20 | 21 | public class AppBeanAdapter extends RecyclerView.Adapter { 22 | private static final String TAG = AppBeanAdapter.class.getSimpleName(); 23 | private Context mContext; 24 | private List mList; 25 | private LayoutInflater inflater; 26 | private int currentPosition; 27 | private OnItemSelectedListener mOnItemSelectedListener; 28 | private OnItemClickListener mOnItemClickListener; 29 | private OnItemLongClickListener mOnItemLongClickListener; 30 | private OnItemKeyListener mOnItemKeyListener; 31 | 32 | public AppBeanAdapter(Context context, List list) { 33 | inflater = LayoutInflater.from(context); 34 | this.mContext = context; 35 | this.mList = list; 36 | } 37 | 38 | @Override 39 | public int getItemCount() { 40 | return mList == null ? 0 : mList.size(); 41 | } 42 | 43 | @Override 44 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 45 | return new ViewHolder(inflater.inflate(R.layout.item_grid_apps, parent, false)); 46 | } 47 | 48 | @Override 49 | public void onBindViewHolder(final ViewHolder holder, int position) { 50 | holder.mImageView.setImageDrawable(mList.get(position).getAppIcon()); 51 | holder.mTextView.setText(mList.get(position).getAppName()); 52 | 53 | // 设置 itemView 可以获得焦点 54 | holder.itemView.setFocusable(true); 55 | holder.itemView.setTag(position); 56 | holder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() { 57 | @Override 58 | public void onFocusChange(View v, boolean hasFocus) { 59 | Log.d(TAG, "焦点监听器被调用了"); 60 | Log.d(TAG, "hasFocus=" + hasFocus); 61 | if (hasFocus) { 62 | currentPosition = (int) holder.itemView.getTag(); 63 | Log.d(TAG, "getTag=" + currentPosition); 64 | Log.i(TAG, "The view hasFocus=" + v + ", holder.itemView=" + holder.itemView); 65 | mOnItemSelectedListener.onItemSelected(v, currentPosition); 66 | } 67 | } 68 | }); 69 | 70 | holder.itemView.setOnClickListener(new View.OnClickListener() { 71 | @Override 72 | public void onClick(View v) { 73 | mOnItemClickListener.onItemClick(v, currentPosition); 74 | } 75 | }); 76 | 77 | holder.itemView.setOnKeyListener(new View.OnKeyListener() { 78 | @Override 79 | public boolean onKey(View v, int keyCode, KeyEvent event) { 80 | mOnItemKeyListener.onItemKey(v, keyCode, event, currentPosition); 81 | return false; 82 | } 83 | }); 84 | 85 | holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { 86 | @Override 87 | public boolean onLongClick(View v) { 88 | mOnItemLongClickListener.onItemLongClick(v, currentPosition); 89 | return true; 90 | } 91 | }); 92 | } 93 | 94 | class ViewHolder extends RecyclerView.ViewHolder { 95 | ImageView mImageView; 96 | TextView mTextView; 97 | 98 | ViewHolder(View itemView) { 99 | super(itemView); 100 | mImageView = (ImageView) itemView.findViewById(R.id.iv_grid_item_icon); 101 | mTextView = (TextView) itemView.findViewById(R.id.tv_grid_item_name); 102 | } 103 | } 104 | 105 | public void setOnItemSelectedListener(OnItemSelectedListener onItemSelectedListener) { 106 | this.mOnItemSelectedListener = onItemSelectedListener; 107 | } 108 | 109 | public void setOnItemClickListener(OnItemClickListener onItemClickListener) { 110 | this.mOnItemClickListener = onItemClickListener; 111 | } 112 | 113 | public void setOnItemLongClickListener(OnItemLongClickListener onItemLongClickListener) { 114 | this.mOnItemLongClickListener = onItemLongClickListener; 115 | } 116 | 117 | public void setOnItemKeyListener(OnItemKeyListener onItemKeyListener) { 118 | this.mOnItemKeyListener = onItemKeyListener; 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /app/src/main/java/com/lishide/scrollrecyclerview/StagGridActivity.java: -------------------------------------------------------------------------------- 1 | package com.lishide.scrollrecyclerview; 2 | 3 | import android.content.pm.ApplicationInfo; 4 | import android.content.pm.PackageInfo; 5 | import android.content.pm.PackageManager; 6 | import android.os.Bundle; 7 | import android.util.Log; 8 | import android.view.KeyEvent; 9 | import android.view.View; 10 | 11 | import androidx.appcompat.app.AppCompatActivity; 12 | import androidx.recyclerview.widget.DefaultItemAnimator; 13 | import androidx.recyclerview.widget.RecyclerView; 14 | import androidx.recyclerview.widget.StaggeredGridLayoutManager; 15 | 16 | import com.lishide.recyclerview.scroll.ScrollRecyclerView; 17 | import com.lishide.recyclerview.scroll.SpaceItemDecoration; 18 | import com.lishide.recyclerview.scroll.listener.OnItemClickListener; 19 | import com.lishide.recyclerview.scroll.listener.OnItemKeyListener; 20 | import com.lishide.recyclerview.scroll.listener.OnItemLongClickListener; 21 | import com.lishide.recyclerview.scroll.listener.OnItemSelectedListener; 22 | 23 | import java.io.File; 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | 27 | /** 28 | * 横向网格布局示例 29 | * 数据源为设备中所有 App 30 | * 演示 item 选定监听、item 点击监听、item 长按监听、遥控器其他按键监听 31 | */ 32 | public class StagGridActivity extends AppCompatActivity { 33 | private static final String TAG = StagGridActivity.class.getSimpleName(); 34 | private ScrollRecyclerView mScrollRecyclerView; 35 | 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_stag_grid); 40 | // 初始化 ScrollRecyclerView 41 | mScrollRecyclerView = (ScrollRecyclerView) findViewById(R.id.scroll_recycler_view); 42 | // 获得数据,数据源为设备中所有 App 43 | List mList = getAllApk(); 44 | // 初始化适配器 45 | AppBeanAdapter adapter = new AppBeanAdapter(this, mList); 46 | // 设置动画 47 | mScrollRecyclerView.setItemAnimator(new DefaultItemAnimator()); 48 | // 设置布局管理器:瀑布流式 49 | mScrollRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(3, 50 | StaggeredGridLayoutManager.HORIZONTAL)); 51 | // 根据需要设置间距等 52 | int right = (int) getResources().getDimension(R.dimen.dp_20); 53 | int bottom = (int) getResources().getDimension(R.dimen.dp_20); 54 | RecyclerView.ItemDecoration spacingInPixel = new SpaceItemDecoration(right, bottom); 55 | mScrollRecyclerView.addItemDecoration(spacingInPixel); 56 | // 关联适配器 57 | mScrollRecyclerView.setAdapter(adapter); 58 | adapter.setOnItemSelectedListener(mOnItemSelectedListener); 59 | adapter.setOnItemClickListener(mOnItemClickListener); 60 | adapter.setOnItemLongClickListener(mOnItemLongClickListener); 61 | adapter.setOnItemKeyListener(mOnItemKeyListener); 62 | } 63 | 64 | public List getAllApk() { 65 | List appBeanList = new ArrayList<>(); 66 | AppBean bean; 67 | PackageManager packageManager = getPackageManager(); 68 | List list = packageManager.getInstalledPackages(0); 69 | for (PackageInfo p : list) { 70 | bean = new AppBean(); 71 | bean.setAppIcon(p.applicationInfo.loadIcon(packageManager)); 72 | bean.setAppName(packageManager.getApplicationLabel(p.applicationInfo).toString()); 73 | bean.setAppPackageName(p.applicationInfo.packageName); 74 | bean.setApkPath(p.applicationInfo.sourceDir); 75 | File file = new File(p.applicationInfo.sourceDir); 76 | bean.setAppSize((int) file.length()); 77 | int flags = p.applicationInfo.flags; 78 | //判断是否是属于系统的apk 79 | if ((flags & ApplicationInfo.FLAG_SYSTEM) != 0) { 80 | bean.setSystem(true); 81 | } else { 82 | bean.setSd(true); 83 | } 84 | appBeanList.add(bean); 85 | } 86 | return appBeanList; 87 | } 88 | 89 | @Override 90 | public boolean onKeyDown(int keyCode, KeyEvent event) { 91 | switch (keyCode) { 92 | case KeyEvent.KEYCODE_DPAD_LEFT: 93 | Log.d(TAG, "按下导航键<-左键->"); 94 | break; 95 | case KeyEvent.KEYCODE_DPAD_RIGHT: 96 | Log.d(TAG, "按下导航键<-右键->"); 97 | break; 98 | case KeyEvent.KEYCODE_DPAD_UP: 99 | Log.d(TAG, "按下导航键<-上键->"); 100 | break; 101 | case KeyEvent.KEYCODE_DPAD_DOWN: 102 | Log.d(TAG, "按下导航键<-下键->"); 103 | break; 104 | default: 105 | break; 106 | } 107 | return super.onKeyDown(keyCode, event); 108 | } 109 | 110 | OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() { 111 | @Override 112 | public void onItemSelected(View view, int position) { 113 | mScrollRecyclerView.smoothHorizontalScrollToNext(position); 114 | } 115 | }; 116 | 117 | OnItemClickListener mOnItemClickListener = new OnItemClickListener() { 118 | @Override 119 | public void onItemClick(View view, int position) { 120 | Log.d(TAG, "<--1--> click position = " + position); 121 | } 122 | }; 123 | 124 | OnItemLongClickListener mOnItemLongClickListener = new OnItemLongClickListener() { 125 | @Override 126 | public void onItemLongClick(View view, int position) { 127 | Log.d(TAG, "<--2--> Long click position = " + position); 128 | } 129 | }; 130 | 131 | OnItemKeyListener mOnItemKeyListener = new OnItemKeyListener() { 132 | @Override 133 | public void onItemKey(View view, int keyCode, KeyEvent event, int position) { 134 | if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 135 | Log.d(TAG, "KEYCODE_DPAD_CENTER"); 136 | } else if (keyCode == KeyEvent.KEYCODE_MENU) { 137 | Log.d(TAG, "KEYCODE_MENU"); 138 | } 139 | } 140 | }; 141 | } 142 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/home_apps_focused.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lishide/ScrollRecyclerView/5a0153d2ec92c1932c96542e9fe0cd5ce6b7e59b/app/src/main/res/drawable-hdpi/home_apps_focused.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/home_apps_unfocused.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/selector_home_apps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_stag_grid.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_grid_apps.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lishide/ScrollRecyclerView/5a0153d2ec92c1932c96542e9fe0cd5ce6b7e59b/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lishide/ScrollRecyclerView/5a0153d2ec92c1932c96542e9fe0cd5ce6b7e59b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lishide/ScrollRecyclerView/5a0153d2ec92c1932c96542e9fe0cd5ce6b7e59b/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lishide/ScrollRecyclerView/5a0153d2ec92c1932c96542e9fe0cd5ce6b7e59b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lishide/ScrollRecyclerView/5a0153d2ec92c1932c96542e9fe0cd5ce6b7e59b/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lishide/ScrollRecyclerView/5a0153d2ec92c1932c96542e9fe0cd5ce6b7e59b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lishide/ScrollRecyclerView/5a0153d2ec92c1932c96542e9fe0cd5ce6b7e59b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lishide/ScrollRecyclerView/5a0153d2ec92c1932c96542e9fe0cd5ce6b7e59b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lishide/ScrollRecyclerView/5a0153d2ec92c1932c96542e9fe0cd5ce6b7e59b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lishide/ScrollRecyclerView/5a0153d2ec92c1932c96542e9fe0cd5ce6b7e59b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | 1dp 7 | 2dp 8 | 3dp 9 | 4dp 10 | 5dp 11 | 8dp 12 | 10dp 13 | 12dp 14 | 15dp 15 | 20dp 16 | 25dp 17 | 30dp 18 | 35dp 19 | 40dp 20 | 45dp 21 | 50dp 22 | 60dp 23 | 80dp 24 | 100dp 25 | 130dp 26 | 27 | 12sp 28 | 13sp 29 | 14sp 30 | 15sp 31 | 16sp 32 | 18sp 33 | 20sp 34 | 22sp 35 | 24sp 36 | 26sp 37 | 28sp 38 | 30sp 39 | 35sp 40 | 40sp 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ScrollRecyclerView 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /art/ScrollRecyclerView_art.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lishide/ScrollRecyclerView/5a0153d2ec92c1932c96542e9fe0cd5ce6b7e59b/art/ScrollRecyclerView_art.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | maven { url 'https://jitpack.io' } 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:4.0.1' 11 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' 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 | maven { url 'https://jitpack.io' } 23 | } 24 | } 25 | 26 | task clean(type: Delete) { 27 | delete rootProject.buildDir 28 | } 29 | -------------------------------------------------------------------------------- /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=-Xmx2048m 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 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lishide/ScrollRecyclerView/5a0153d2ec92c1932c96542e9fe0cd5ce6b7e59b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Aug 08 10:01:14 CST 2020 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-6.1.1-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 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /recyclerview-scroll-lib/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /recyclerview-scroll-lib/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | 4 | android { 5 | compileSdkVersion 29 6 | buildToolsVersion "29.0.3" 7 | 8 | defaultConfig { 9 | minSdkVersion 15 10 | targetSdkVersion 29 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 15 | 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | api fileTree(dir: 'libs', include: ['*.jar']) 27 | api 'androidx.appcompat:appcompat:1.2.0' 28 | api 'androidx.recyclerview:recyclerview:1.2.0-alpha05' 29 | } 30 | -------------------------------------------------------------------------------- /recyclerview-scroll-lib/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 C:\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 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /recyclerview-scroll-lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /recyclerview-scroll-lib/src/main/java/com/lishide/recyclerview/scroll/ScrollRecyclerView.java: -------------------------------------------------------------------------------- 1 | package com.lishide.recyclerview.scroll; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.util.Log; 6 | import android.view.View; 7 | import android.widget.Scroller; 8 | 9 | import androidx.recyclerview.widget.LinearLayoutManager; 10 | import androidx.recyclerview.widget.RecyclerView; 11 | import androidx.recyclerview.widget.StaggeredGridLayoutManager; 12 | 13 | /** 14 | * ScrollRecyclerView——RecyclerView 横向 / 纵向滚动网格布局。 15 | * 适用于 Android 平板、Android TV 或其他定制化 Android 设备等, 16 | * 使用遥控器方向导航键控制列表滑动及 item 选择状态。 17 | * 18 | * @author lishide 19 | * @date 2017/4/12 20 | */ 21 | public class ScrollRecyclerView extends RecyclerView { 22 | private static final String TAG = ScrollRecyclerView.class.getSimpleName(); 23 | /** 24 | * 一个滚动对象 25 | */ 26 | private Scroller mScroller; 27 | private int mLastX = 0; 28 | private int specialLeft, specialRight; 29 | private int childWidth; 30 | 31 | public ScrollRecyclerView(Context context) { 32 | super(context); 33 | init(context); 34 | } 35 | 36 | public ScrollRecyclerView(Context context, AttributeSet attrs) { 37 | super(context, attrs); 38 | init(context); 39 | } 40 | 41 | public ScrollRecyclerView(Context context, AttributeSet attrs, int defStyle) { 42 | super(context, attrs, defStyle); 43 | init(context); 44 | } 45 | 46 | /** 47 | * 一个初始化方法,传入了一个上下文对象,用来初始化滚动对象 48 | * 49 | * @param context 上下文 50 | */ 51 | private void init(Context context) { 52 | mScroller = new Scroller(context); 53 | } 54 | 55 | /** 56 | * 重写计算滚动方法 57 | */ 58 | @Override 59 | public void computeScroll() { 60 | if (mScroller != null && mScroller.computeScrollOffset()) { 61 | // Log.i("computeScroll", "curX:" + mScroller.getCurrX()); 62 | scrollBy(mLastX - mScroller.getCurrX(), 0); 63 | mLastX = mScroller.getCurrX(); 64 | postInvalidate(); 65 | } 66 | } 67 | 68 | /** 69 | * 滚动到目标位置 70 | * 其中 (fx, fy) 表示最终要滚到的目标位置的坐标值,duration 表示期间滚动的耗时。 71 | * 72 | * @param fx 目标位置的X向坐标值 73 | * @param fy 目标位置的Y向坐标值 74 | * @param duration 滚动到目标位置所消耗的时间毫秒值 75 | */ 76 | @SuppressWarnings("unused") 77 | public void smoothScrollTo(int fx, int fy, int duration) { 78 | int dx = 0; 79 | int dy = 0; 80 | // 计算变化的位移量 81 | if (fx != 0) { 82 | dx = fx - mScroller.getFinalX(); 83 | } 84 | if (fy != 0) { 85 | dy = fy - mScroller.getFinalY(); 86 | } 87 | Log.i(TAG, "fx:" + fx + ", getFinalX:" + mScroller.getFinalX() + ", dx:" + dx); 88 | smoothScrollBy(dx, dy, duration); 89 | } 90 | 91 | /** 92 | * 设置滚动的相对偏移 93 | */ 94 | public void smoothScrollBy(int dx, int dy, int duration) { 95 | if (duration > 0) { 96 | mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy, duration); 97 | } else { 98 | // 设置mScroller的滚动偏移量 99 | mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy); 100 | } 101 | // 重绘整个view,重绘过程会调用到computeScroll()方法。 102 | // 这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果 103 | invalidate(); 104 | } 105 | 106 | /** 107 | * 检查自动调节 108 | * 109 | * @param position 要检查的位置 110 | */ 111 | @SuppressWarnings("unused") 112 | public void checkAutoAdjust(int position) { 113 | int childCount = getChildCount(); 114 | // 获取可视范围内的选项的头尾位置 115 | int firstVisibleItemPosition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition(); 116 | int lastVisibleItemPosition = ((LinearLayoutManager) getLayoutManager()).findLastVisibleItemPosition(); 117 | Log.d(TAG, "childCount:" + childCount + ", position:" + position 118 | + ", firstVisibleItemPosition:" + firstVisibleItemPosition 119 | + " lastVisibleItemPosition:" + lastVisibleItemPosition); 120 | if (position == (firstVisibleItemPosition + 1) || position == firstVisibleItemPosition) { 121 | leftScrollBy(position, firstVisibleItemPosition); 122 | } else if (position == (lastVisibleItemPosition - 1) || position == lastVisibleItemPosition) { 123 | rightScrollBy(position, lastVisibleItemPosition); 124 | } 125 | } 126 | 127 | /** 128 | * 当前位置需要向右平移 129 | * 130 | * @param position pos 131 | * @param firstVisibleItemPosition 可见的第一个Item的pos 132 | */ 133 | private void leftScrollBy(int position, int firstVisibleItemPosition) { 134 | View leftChild = getChildAt(0); 135 | if (leftChild != null) { 136 | int startLeft = leftChild.getLeft(); 137 | int endLeft = (position == firstVisibleItemPosition ? leftChild.getWidth() : 0); 138 | Log.d(TAG, "startLeft:" + startLeft + " endLeft" + endLeft); 139 | autoAdjustScroll(startLeft, endLeft); 140 | } 141 | } 142 | 143 | /** 144 | * 当前位置需要向左平移 145 | * 146 | * @param position pos 147 | * @param lastVisibleItemPosition 可见的最后一个Item的pos 148 | */ 149 | private void rightScrollBy(int position, int lastVisibleItemPosition) { 150 | int childCount = getChildCount(); 151 | View rightChild = getChildAt(childCount - 1); 152 | if (rightChild != null) { 153 | int startRight = rightChild.getRight() - getWidth(); 154 | int endRight = (position == lastVisibleItemPosition ? (-1 * rightChild.getWidth()) : 0); 155 | Log.d(TAG, "startRight:" + startRight + " endRight:" + endRight); 156 | autoAdjustScroll(startRight, endRight); 157 | } 158 | } 159 | 160 | /** 161 | * 自动调节滑动 162 | * 163 | * @param start 滑动起始位置 164 | * @param end 滑动结束位置 165 | */ 166 | private void autoAdjustScroll(int start, int end) { 167 | mLastX = start; 168 | mScroller.startScroll(start, 0, end - start, 0); 169 | postInvalidate(); 170 | } 171 | 172 | /** 173 | * 将指定 item 平滑移动到整个 view 的中间位置 174 | * 175 | * @param position 指定的 item 的位置 176 | */ 177 | public void smoothHorizontalScrollToNext(int position) { 178 | Log.d(TAG, "position=" + position); 179 | StaggeredGridLayoutManager manager = (StaggeredGridLayoutManager) getLayoutManager(); 180 | int[] startItems = manager.findFirstVisibleItemPositions(null); 181 | int[] endItems = manager.findLastVisibleItemPositions(null); 182 | if (position == 0) { 183 | int parentWidth = getWidth(); 184 | View firstView = getChildAt(0); 185 | mLastX = 1174; 186 | mScroller.startScroll(mLastX, 0, -500, 0); 187 | postInvalidate(); 188 | 189 | childWidth = firstView.getWidth(); 190 | int preLastPos = endItems[0] - 1; 191 | View specialView = getChildAt(preLastPos); 192 | if (specialView == null) { 193 | return; 194 | } 195 | specialLeft = specialView.getLeft(); 196 | specialRight = parentWidth - specialLeft; 197 | Log.d(TAG, "一屏的倒数第二行位置是:" + preLastPos + ", 父容器宽度:" + parentWidth 198 | + ", 超出位置(极右位置):" + specialLeft + ", 不达位置(极左位置):" + specialRight); 199 | } 200 | 201 | int targetPos = position - startItems[0]; 202 | View targetView = getChildAt(targetPos); 203 | if (targetView == null) { 204 | Log.i(TAG, "TargetView is null!"); 205 | return; 206 | } 207 | int targetLeft = targetView.getLeft(); 208 | int targetRight = targetView.getRight(); 209 | Log.d(TAG, "目标位置:" + targetPos + ", 目标左位置:" + targetLeft + ", 目标右位置:" + targetRight); 210 | 211 | if (targetLeft > specialLeft) { 212 | // 获得焦点的不全显示 item 将自动全显示。 213 | // 因此,到达极右位置只需移动下一个不全显示的偏置距离 214 | mLastX = targetLeft; 215 | mScroller.startScroll(targetLeft, 0, -childWidth / 2, 0); 216 | postInvalidate(); 217 | Log.d(TAG, "<----"); 218 | } else if (targetRight < specialRight) { 219 | // 到达极左位置 220 | mLastX = targetRight; 221 | mScroller.startScroll(targetRight, 0, childWidth / 2, 0); 222 | postInvalidate(); 223 | Log.d(TAG, "---->"); 224 | } 225 | } 226 | 227 | } 228 | -------------------------------------------------------------------------------- /recyclerview-scroll-lib/src/main/java/com/lishide/recyclerview/scroll/SpaceItemDecoration.java: -------------------------------------------------------------------------------- 1 | package com.lishide.recyclerview.scroll; 2 | 3 | import android.graphics.Rect; 4 | import android.view.View; 5 | 6 | import androidx.recyclerview.widget.RecyclerView; 7 | 8 | public class SpaceItemDecoration extends RecyclerView.ItemDecoration { 9 | private int mRight; 10 | private int mBottom; 11 | 12 | public SpaceItemDecoration(int right, int bottom) { 13 | this.mRight = right; 14 | this.mBottom = bottom; 15 | } 16 | 17 | @Override 18 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 19 | outRect.right = mRight; 20 | outRect.bottom = mBottom; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /recyclerview-scroll-lib/src/main/java/com/lishide/recyclerview/scroll/listener/OnItemClickListener.java: -------------------------------------------------------------------------------- 1 | package com.lishide.recyclerview.scroll.listener; 2 | 3 | import android.view.View; 4 | 5 | /** 6 | * item 点击监听 7 | * 8 | * @author lishide 9 | * @date 2017/4/12 10 | */ 11 | public interface OnItemClickListener { 12 | /** 13 | * item 点击 14 | * 15 | * @param view view 16 | * @param position position 17 | */ 18 | void onItemClick(View view, int position); 19 | } 20 | -------------------------------------------------------------------------------- /recyclerview-scroll-lib/src/main/java/com/lishide/recyclerview/scroll/listener/OnItemKeyListener.java: -------------------------------------------------------------------------------- 1 | package com.lishide.recyclerview.scroll.listener; 2 | 3 | import android.view.KeyEvent; 4 | import android.view.View; 5 | 6 | /** 7 | * 遥控器其他按键监听 8 | * 9 | * @author lishide 10 | * @date 2017/4/12 11 | */ 12 | public interface OnItemKeyListener { 13 | /** 14 | * 遥控器其他按键 15 | * 16 | * @param view view 17 | * @param keyCode keyCode 18 | * @param event event 19 | * @param position position 20 | */ 21 | void onItemKey(View view, int keyCode, KeyEvent event, int position); 22 | } 23 | -------------------------------------------------------------------------------- /recyclerview-scroll-lib/src/main/java/com/lishide/recyclerview/scroll/listener/OnItemLongClickListener.java: -------------------------------------------------------------------------------- 1 | package com.lishide.recyclerview.scroll.listener; 2 | 3 | import android.view.View; 4 | 5 | /** 6 | * item 长按监听 7 | * 8 | * @author lishide 9 | * @date 2017/4/12 10 | */ 11 | public interface OnItemLongClickListener { 12 | /** 13 | * item 长按 14 | * 15 | * @param view view 16 | * @param position position 17 | */ 18 | void onItemLongClick(View view, int position); 19 | } 20 | -------------------------------------------------------------------------------- /recyclerview-scroll-lib/src/main/java/com/lishide/recyclerview/scroll/listener/OnItemSelectedListener.java: -------------------------------------------------------------------------------- 1 | package com.lishide.recyclerview.scroll.listener; 2 | 3 | import android.view.View; 4 | 5 | /** 6 | * item 选定监听 7 | * 8 | * @author lishide 9 | * @date 2017/4/12 10 | */ 11 | public interface OnItemSelectedListener { 12 | /** 13 | * item 选定 14 | * 15 | * @param view view 16 | * @param position position 17 | */ 18 | void onItemSelected(View view, int position); 19 | } 20 | -------------------------------------------------------------------------------- /recyclerview-scroll-lib/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | RecyclerView-Scroll-lib 3 | 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':recyclerview-scroll-lib' 2 | --------------------------------------------------------------------------------