├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hirayclay │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── hirayclay │ │ │ ├── Align.java │ │ │ ├── Config.java │ │ │ ├── ItemChangeListener.java │ │ │ ├── MainActivity.java │ │ │ ├── StackAdapter.java │ │ │ ├── StackLayoutManager.java │ │ │ └── VerticalActivity.java │ ├── res │ │ ├── animator │ │ │ └── item_animator.xml │ │ ├── drawable │ │ │ ├── circle_shape.xml │ │ │ ├── xm1.jpg │ │ │ ├── xm2.jpg │ │ │ ├── xm3.jpg │ │ │ ├── xm4.jpg │ │ │ ├── xm5.jpg │ │ │ ├── xm6.jpg │ │ │ ├── xm7.jpg │ │ │ ├── xm8.jpg │ │ │ └── xm9.jpg │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ ├── activity_vertical.xml │ │ │ ├── item_card.xml │ │ │ └── vertical_item_card.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 │ └── res_sub │ │ ├── menu │ │ └── rest.xml │ │ └── values │ │ ├── artical.xml │ │ └── color.xml │ └── test │ └── java │ └── com │ └── hirayclay │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── static ├── README-cn.md ├── VerticallSLM.gif ├── app-hr.apk ├── app-vertical.apk ├── app.apk ├── app_hr.apk ├── art.gif ├── art_new.gif ├── hrreverse.gif ├── stackManager2.gif └── stackmanager3.gif /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | /.idea/vcs.xml 7 | /.idea/misc.xml 8 | /.idea/markdown-navigator 9 | .DS_Store 10 | /build 11 | /captures 12 | .externalNativeBuild 13 | libs/ 14 | .idea/markdown-navigator.xml 15 | 16 | -------------------------------------------------------------------------------- /.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/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [中文](static/README-cn.md) 2 | (因为目前代码组织得非常烂,仅仅是个玩具,如果有Item 增删操作的话最好不要用进项目,因为predictive动画并不支持。当初只是模仿着写,对RV的一整套东西并没有理解很透彻) 3 | 4 | # Why 5 | A long long time ago ,i was inspired by this project [android-pile-layout](https://github.com/xmuSistone/android-pile-layout) ,the author cannot find the appropriate math model with [LayoutManager](https://github.com/HirayClay/StackLayoutManager/blob/master/app/src/main/java/com/hirayclay/StackLayoutManager.java) .Now i have some spare time and try to do the UI with layoutManager,barely ok with the result.
6 | 7 | # Blog 8 | this is the relevant [blog](http://blog.csdn.net/u014296305/article/details/73496017) ,i hope it helps to understanding it
9 | 10 | # Display 11 | ### horizontal 12 | 13 | 14 | ### vertical(only top) 15 | 16 | 17 | ### Demo Apk 18 | [download](static/app-vertical.apk)(vertical support) 19 | 20 | ### Attention 21 | If you don't care about item ADD REMOVE operation, this is what you want, 22 | because predictive animation is not supported yet,otherwise take care! 23 | 24 | # Usage 25 | ```java 26 | 27 | Config config = new Config(); 28 | config.secondaryScale = 0.8f; 29 | config.scaleRatio = 0.5f; 30 | config.maxStackCount = 3; 31 | config.initialStackCount = 2; 32 | config.space = 70; 33 | config.parallex = 1.5f;//parallex factor 34 | config.align= Align.RIGHT 35 | recyclerview.setLayoutManager(new StackLayoutManager(config)); 36 | recyclerview.setAdapter(new StackAdapter(datas)); 37 | 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | buildToolsVersion "27.0.3" 6 | defaultConfig { 7 | applicationId "com.hirayclay" 8 | minSdkVersion 19 9 | targetSdkVersion 27 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | 15 | sourceSets { 16 | main { 17 | res.srcDirs('src/main/res', 'src/main/res_sub') 18 | } 19 | } 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { 31 | exclude group: 'com.android.support', module: 'support-annotations' 32 | }) 33 | implementation 'com.android.support:appcompat-v7:27.1.1' 34 | implementation 'com.android.support:design:27.1.1' 35 | implementation 'com.jakewharton:butterknife:8.8.1' 36 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' 37 | implementation 'com.github.bumptech.glide:glide:3.8.0' 38 | implementation 'com.makeramen:roundedimageview:2.3.0' 39 | implementation 'com.android.support:recyclerview-v7:27.1.1' 40 | implementation 'com.android.support:cardview-v7:27.1.1' 41 | testImplementation 'junit:junit:4.12' 42 | } 43 | -------------------------------------------------------------------------------- /app/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HirayClay/StackLayoutManager/89902db36b316c89f68547a75feb57a7a25edb34/app/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri May 04 14:44:19 CST 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 | -------------------------------------------------------------------------------- /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 E:\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/androidTest/java/com/hirayclay/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.hirayclay; 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 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.hirayclay", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/hirayclay/Align.java: -------------------------------------------------------------------------------- 1 | package com.hirayclay; 2 | 3 | /** 4 | * Created by CJJ on 2017/10/16. 5 | * 6 | * @author CJJ 7 | */ 8 | 9 | enum Align { 10 | 11 | 12 | LEFT(1), 13 | RIGHT(-1), 14 | TOP(1), 15 | BOTTOM(-1); 16 | 17 | int layoutDirection; 18 | 19 | Align(int sign) { 20 | this.layoutDirection = sign; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/hirayclay/Config.java: -------------------------------------------------------------------------------- 1 | package com.hirayclay; 2 | 3 | import android.support.annotation.FloatRange; 4 | import android.support.annotation.IntRange; 5 | 6 | /** 7 | * Created by CJJ on 2017/6/20. 8 | * 9 | * @author CJJ 10 | */ 11 | 12 | public class Config { 13 | 14 | @IntRange(from = 2) 15 | public int space = 60; 16 | public int maxStackCount = 3; 17 | public int initialStackCount = 0; 18 | @FloatRange(from = 0f, to = 1f) 19 | public float secondaryScale; 20 | @FloatRange(from = 0f, to = 1f) 21 | public float scaleRatio; 22 | /** 23 | * the real scroll distance might meet requirement, 24 | * so we multiply a factor fro parallex 25 | */ 26 | @FloatRange(from = 1f,to = 2f) 27 | public float parallex = 1f; 28 | Align align; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/hirayclay/ItemChangeListener.java: -------------------------------------------------------------------------------- 1 | package com.hirayclay; 2 | 3 | import android.view.View; 4 | 5 | /** 6 | * Created by hiray on 2017/12/27. 7 | * 8 | * @author hiray 9 | * notify the observer the item in the base position has changed 10 | */ 11 | 12 | public interface ItemChangeListener { 13 | 14 | /** 15 | * 16 | * @param itemView the new item in the base position 17 | * @param position the item's position in list 18 | */ 19 | void onItemChange(View itemView, int position); 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/hirayclay/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.hirayclay; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.widget.Button; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import butterknife.BindView; 13 | import butterknife.ButterKnife; 14 | import butterknife.OnClick; 15 | import butterknife.Unbinder; 16 | 17 | public class MainActivity extends AppCompatActivity { 18 | private static final String TAG = "MainActivity"; 19 | @BindView(R.id.recyclerview) 20 | RecyclerView recyclerview; 21 | 22 | //horizontal reverse recyclerview 23 | @BindView(R.id.recyclerview1) 24 | RecyclerView hrRecyclerView; 25 | @BindView(R.id.button) 26 | Button button; 27 | private StackLayoutManager layoutManager; 28 | private Unbinder unbinder; 29 | 30 | @Override 31 | protected void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | setContentView(R.layout.activity_main); 34 | unbinder = ButterKnife.bind(this); 35 | resetDefault(); 36 | resetRight(); 37 | } 38 | 39 | 40 | @OnClick(R.id.button) 41 | public void resetDefault() { 42 | List datas = new ArrayList<>(); 43 | for (int i = 0; i < 15; i++) { 44 | datas.add(String.valueOf(i)); 45 | } 46 | 47 | Config config = new Config(); 48 | config.secondaryScale = 0.8f; 49 | config.scaleRatio = 0.4f; 50 | config.maxStackCount = 4; 51 | config.initialStackCount = 2; 52 | config.space = 15; 53 | config.align = Align.LEFT; 54 | recyclerview.setLayoutManager(layoutManager = new StackLayoutManager(config)); 55 | recyclerview.setAdapter(new StackAdapter(datas)); 56 | 57 | } 58 | 59 | @OnClick(R.id.button1) 60 | public void resetRight() { 61 | List datas = new ArrayList<>(); 62 | for (int i = 0; i < 15; i++) { 63 | datas.add(String.valueOf(i)); 64 | } 65 | 66 | Config config = new Config(); 67 | config.secondaryScale = 0.8f; 68 | config.scaleRatio = 0.4f; 69 | config.maxStackCount = 4; 70 | config.initialStackCount = 2; 71 | config.space = getResources().getDimensionPixelOffset(R.dimen.item_space); 72 | 73 | config.align = Align.RIGHT; 74 | hrRecyclerView.setLayoutManager(new StackLayoutManager(config)); 75 | hrRecyclerView.setAdapter(new StackAdapter(datas)); 76 | } 77 | 78 | @OnClick(R.id.button2) 79 | public void viewVertical() { 80 | startActivity(new Intent(this, VerticalActivity.class)); 81 | } 82 | 83 | @OnClick(R.id.scroll_to_specific_item) 84 | public void onScrollToItem() { 85 | layoutManager.scrollToPosition(10); 86 | } 87 | 88 | @Override 89 | protected void onDestroy() { 90 | super.onDestroy(); 91 | unbinder.unbind(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/hirayclay/StackAdapter.java: -------------------------------------------------------------------------------- 1 | package com.hirayclay; 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.ImageView; 9 | import android.widget.TextView; 10 | import android.widget.Toast; 11 | 12 | import com.bumptech.glide.Glide; 13 | 14 | import java.util.Arrays; 15 | import java.util.List; 16 | 17 | /** 18 | * Created by CJJ on 2017/3/7. 19 | */ 20 | 21 | public class StackAdapter extends RecyclerView.Adapter { 22 | 23 | private LayoutInflater inflater; 24 | private List datas; 25 | private Context context; 26 | private List imageUrls = Arrays.asList( 27 | R.drawable.xm2, 28 | R.drawable.xm3, 29 | R.drawable.xm4, 30 | R.drawable.xm5, 31 | R.drawable.xm6, 32 | R.drawable.xm7, 33 | R.drawable.xm1, 34 | R.drawable.xm8, 35 | R.drawable.xm9, 36 | R.drawable.xm1, 37 | R.drawable.xm2, 38 | R.drawable.xm3, 39 | R.drawable.xm4, 40 | R.drawable.xm5, 41 | R.drawable.xm6 42 | ); 43 | private boolean vertical; 44 | 45 | public StackAdapter(List datas) { 46 | this.datas = datas; 47 | } 48 | 49 | @Override 50 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 51 | if (inflater == null) { 52 | context = parent.getContext(); 53 | inflater = LayoutInflater.from(parent.getContext()); 54 | } 55 | if (vertical) 56 | return new ViewHolder(inflater.inflate(R.layout.vertical_item_card, parent, false)); 57 | return new ViewHolder(inflater.inflate(R.layout.item_card, parent, false)); 58 | } 59 | 60 | public StackAdapter vertical() { 61 | this.vertical = true; 62 | return this; 63 | } 64 | 65 | @Override 66 | public void onBindViewHolder(ViewHolder holder, int position) { 67 | Glide.with(context).load(imageUrls.get(position)).into(holder.cover); 68 | holder.index.setText(datas.get(holder.getAdapterPosition())); 69 | } 70 | 71 | @Override 72 | public int getItemCount() { 73 | return datas == null ? 0 : datas.size(); 74 | } 75 | 76 | class ViewHolder extends RecyclerView.ViewHolder { 77 | ImageView cover; 78 | TextView index; 79 | 80 | public ViewHolder(View itemView) { 81 | super(itemView); 82 | cover = (ImageView) itemView.findViewById(R.id.cover); 83 | index = (TextView) itemView.findViewById(R.id.index); 84 | itemView.setOnClickListener(new View.OnClickListener() { 85 | @Override 86 | public void onClick(View v) { 87 | Toast.makeText(context.getApplicationContext(), String.valueOf(getAdapterPosition()), Toast.LENGTH_SHORT) 88 | .show(); 89 | } 90 | }); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/hirayclay/StackLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.hirayclay; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ObjectAnimator; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.util.Log; 8 | import android.view.MotionEvent; 9 | import android.view.VelocityTracker; 10 | import android.view.View; 11 | import android.view.ViewConfiguration; 12 | 13 | import java.lang.reflect.InvocationTargetException; 14 | import java.lang.reflect.Method; 15 | 16 | import static android.support.v7.widget.RecyclerView.NO_POSITION; 17 | import static com.hirayclay.Align.BOTTOM; 18 | import static com.hirayclay.Align.LEFT; 19 | import static com.hirayclay.Align.RIGHT; 20 | import static com.hirayclay.Align.TOP; 21 | 22 | /** 23 | * Created by CJJ on 2017/5/17. 24 | * my thought is simple:we assume the first item in the initial state is the base position , 25 | * we only need to calculate the appropriate position{@link #left(int index)}for the given item 26 | * index with the given offset{@link #mTotalOffset}.After solve this thinking confusion ,this 27 | * layoutManager is easy to implement 28 | * 29 | * @author CJJ 30 | */ 31 | 32 | class StackLayoutManager extends RecyclerView.LayoutManager { 33 | 34 | private static final String TAG = "StackLayoutManager"; 35 | 36 | //the space unit for the stacked item 37 | private int mSpace = 60; 38 | /** 39 | * the offset unit,deciding current position(the sum of {@link #mItemWidth} and {@link #mSpace}) 40 | */ 41 | private int mUnit; 42 | //item width 43 | private int mItemWidth; 44 | private int mItemHeight; 45 | //the counting variable ,record the total offset including parallex 46 | private int mTotalOffset; 47 | //record the total offset without parallex 48 | private int mRealOffset; 49 | private ObjectAnimator animator; 50 | private int animateValue; 51 | private int duration = 300; 52 | private RecyclerView.Recycler recycler; 53 | private int lastAnimateValue; 54 | //the max stacked item count; 55 | private int maxStackCount = 4; 56 | //initial stacked item 57 | private int initialStackCount = 4; 58 | private float secondaryScale = 0.8f; 59 | private float scaleRatio = 0.4f; 60 | private float parallex = 1f; 61 | private int initialOffset; 62 | private boolean initial; 63 | private int mMinVelocityX; 64 | private VelocityTracker mVelocityTracker = VelocityTracker.obtain(); 65 | private int pointerId; 66 | private Align direction = LEFT; 67 | private RecyclerView mRV; 68 | private Method sSetScrollState; 69 | private int mPendingScrollPosition = NO_POSITION; 70 | 71 | StackLayoutManager(Config config) { 72 | this(); 73 | this.maxStackCount = config.maxStackCount; 74 | this.mSpace = config.space; 75 | this.initialStackCount = config.initialStackCount; 76 | this.secondaryScale = config.secondaryScale; 77 | this.scaleRatio = config.scaleRatio; 78 | this.direction = config.align; 79 | this.parallex = config.parallex; 80 | } 81 | 82 | 83 | @SuppressWarnings("unused") 84 | public StackLayoutManager() { 85 | setAutoMeasureEnabled(true); 86 | } 87 | 88 | @Override 89 | public boolean isAutoMeasureEnabled() { 90 | return true; 91 | } 92 | 93 | @Override 94 | public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 95 | if (getItemCount() <= 0) 96 | return; 97 | this.recycler = recycler; 98 | detachAndScrapAttachedViews(recycler); 99 | //got the mUnit basing on the first child,of course we assume that all the item has the same size 100 | View anchorView = recycler.getViewForPosition(0); 101 | measureChildWithMargins(anchorView, 0, 0); 102 | mItemWidth = anchorView.getMeasuredWidth(); 103 | mItemHeight = anchorView.getMeasuredHeight(); 104 | if (canScrollHorizontally()) 105 | mUnit = mItemWidth + mSpace; 106 | else mUnit = mItemHeight + mSpace; 107 | //because this method will be called twice 108 | initialOffset = resolveInitialOffset(); 109 | mMinVelocityX = ViewConfiguration.get(anchorView.getContext()).getScaledMinimumFlingVelocity(); 110 | fill(recycler, 0); 111 | 112 | } 113 | 114 | //we need take direction into account when calc initialOffset 115 | private int resolveInitialOffset() { 116 | int offset = initialStackCount * mUnit; 117 | if (mPendingScrollPosition != NO_POSITION) { 118 | offset = mPendingScrollPosition * mUnit; 119 | mPendingScrollPosition = NO_POSITION; 120 | } 121 | 122 | if (direction == LEFT) 123 | return offset; 124 | if (direction == RIGHT) 125 | return -offset; 126 | if (direction == TOP) 127 | return offset; 128 | else return offset; 129 | } 130 | 131 | @Override 132 | public void onLayoutCompleted(RecyclerView.State state) { 133 | super.onLayoutCompleted(state); 134 | if (getItemCount()<=0) 135 | return; 136 | if (!initial) { 137 | fill(recycler, initialOffset, false); 138 | initial = true; 139 | } 140 | } 141 | 142 | @Override 143 | public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) { 144 | initial = false; 145 | mTotalOffset = mRealOffset = 0; 146 | } 147 | 148 | /** 149 | * the magic function :).all the work including computing ,recycling,and layout is done here 150 | * 151 | * @param recycler ... 152 | */ 153 | private int fill(RecyclerView.Recycler recycler, int dy, boolean apply) { 154 | int delta = direction.layoutDirection * dy; 155 | // multiply the parallex factor 156 | if (apply) 157 | delta = (int) (delta * parallex); 158 | if (direction == LEFT) 159 | return fillFromLeft(recycler, delta); 160 | if (direction == RIGHT) 161 | return fillFromRight(recycler, delta); 162 | if (direction == TOP) 163 | return fillFromTop(recycler, delta); 164 | else return dy;//bottom alignment is not necessary,we don't support that 165 | } 166 | 167 | public int fill(RecyclerView.Recycler recycler, int dy) { 168 | return fill(recycler, dy, true); 169 | } 170 | 171 | private int fillFromTop(RecyclerView.Recycler recycler, int dy) { 172 | if (mTotalOffset + dy < 0 || (mTotalOffset + dy + 0f) / mUnit > getItemCount() - 1) 173 | return 0; 174 | detachAndScrapAttachedViews(recycler); 175 | mTotalOffset += direction.layoutDirection * dy; 176 | int count = getChildCount(); 177 | //removeAndRecycle views 178 | for (int i = 0; i < count; i++) { 179 | View child = getChildAt(i); 180 | if (recycleVertically(child, dy)) 181 | removeAndRecycleView(child, recycler); 182 | } 183 | int currPos = mTotalOffset / mUnit; 184 | int leavingSpace = getHeight() - (left(currPos) + mUnit); 185 | int itemCountAfterBaseItem = leavingSpace / mUnit + 2; 186 | int e = currPos + itemCountAfterBaseItem; 187 | 188 | int start = currPos - maxStackCount >= 0 ? currPos - maxStackCount : 0; 189 | int end = e >= getItemCount() ? getItemCount() - 1 : e; 190 | 191 | int left = getWidth() / 2 - mItemWidth / 2; 192 | //layout views 193 | for (int i = start; i <= end; i++) { 194 | View view = recycler.getViewForPosition(i); 195 | 196 | float scale = scale(i); 197 | float alpha = alpha(i); 198 | 199 | addView(view); 200 | measureChildWithMargins(view, 0, 0); 201 | int top = (int) (left(i) - (1 - scale) * view.getMeasuredHeight() / 2); 202 | int right = view.getMeasuredWidth() + left; 203 | int bottom = view.getMeasuredHeight() + top; 204 | layoutDecoratedWithMargins(view, left, top, right, bottom); 205 | view.setAlpha(alpha); 206 | view.setScaleY(scale); 207 | view.setScaleX(scale); 208 | } 209 | 210 | return dy; 211 | } 212 | 213 | private int fillFromRight(RecyclerView.Recycler recycler, int dy) { 214 | 215 | if (mTotalOffset + dy < 0 || (mTotalOffset + dy + 0f) / mUnit > getItemCount() - 1) 216 | return 0; 217 | detachAndScrapAttachedViews(recycler); 218 | mTotalOffset += dy; 219 | int count = getChildCount(); 220 | //removeAndRecycle views 221 | for (int i = 0; i < count; i++) { 222 | View child = getChildAt(i); 223 | if (recycleHorizontally(child, dy)) 224 | removeAndRecycleView(child, recycler); 225 | } 226 | 227 | 228 | int currPos = mTotalOffset / mUnit; 229 | int leavingSpace = left(currPos); 230 | int itemCountAfterBaseItem = leavingSpace / mUnit + 2; 231 | int e = currPos + itemCountAfterBaseItem; 232 | 233 | int start = currPos - maxStackCount <= 0 ? 0 : currPos - maxStackCount; 234 | int end = e >= getItemCount() ? getItemCount() - 1 : e; 235 | 236 | //layout view 237 | for (int i = start; i <= end; i++) { 238 | View view = recycler.getViewForPosition(i); 239 | 240 | float scale = scale(i); 241 | float alpha = alpha(i); 242 | 243 | addView(view); 244 | measureChildWithMargins(view, 0, 0); 245 | int left = (int) (left(i) - (1 - scale) * view.getMeasuredWidth() / 2); 246 | int top = 0; 247 | int right = left + view.getMeasuredWidth(); 248 | int bottom = view.getMeasuredHeight(); 249 | 250 | layoutDecoratedWithMargins(view, left, top, right, bottom); 251 | view.setAlpha(alpha); 252 | view.setScaleY(scale); 253 | view.setScaleX(scale); 254 | } 255 | 256 | return dy; 257 | } 258 | 259 | private int fillFromLeft(RecyclerView.Recycler recycler, int dy) { 260 | if (mTotalOffset + dy < 0 || (mTotalOffset + dy + 0f) / mUnit > getItemCount() - 1) 261 | return 0; 262 | detachAndScrapAttachedViews(recycler); 263 | mTotalOffset += direction.layoutDirection * dy; 264 | int count = getChildCount(); 265 | //removeAndRecycle views 266 | for (int i = 0; i < count; i++) { 267 | View child = getChildAt(i); 268 | if (recycleHorizontally(child, dy)) 269 | removeAndRecycleView(child, recycler); 270 | } 271 | 272 | 273 | int currPos = mTotalOffset / mUnit; 274 | int leavingSpace = getWidth() - (left(currPos) + mUnit); 275 | int itemCountAfterBaseItem = leavingSpace / mUnit + 2; 276 | int e = currPos + itemCountAfterBaseItem; 277 | 278 | int start = currPos - maxStackCount >= 0 ? currPos - maxStackCount : 0; 279 | int end = e >= getItemCount() ? getItemCount() - 1 : e; 280 | 281 | //layout view 282 | for (int i = start; i <= end; i++) { 283 | View view = recycler.getViewForPosition(i); 284 | 285 | float scale = scale(i); 286 | float alpha = alpha(i); 287 | 288 | addView(view); 289 | measureChildWithMargins(view, 0, 0); 290 | int left = (int) (left(i) - (1 - scale) * view.getMeasuredWidth() / 2); 291 | int top = 0; 292 | int right = left + view.getMeasuredWidth(); 293 | int bottom = top + view.getMeasuredHeight(); 294 | layoutDecoratedWithMargins(view, left, top, right, bottom); 295 | view.setAlpha(alpha); 296 | view.setScaleY(scale); 297 | view.setScaleX(scale); 298 | } 299 | 300 | return dy; 301 | } 302 | 303 | private View.OnTouchListener mTouchListener = new View.OnTouchListener() { 304 | @Override 305 | public boolean onTouch(View v, MotionEvent event) { 306 | mVelocityTracker.addMovement(event); 307 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 308 | if (animator != null && animator.isRunning()) 309 | animator.cancel(); 310 | pointerId = event.getPointerId(0); 311 | 312 | } 313 | if (event.getAction() == MotionEvent.ACTION_UP) { 314 | if (v.isPressed()) v.performClick(); 315 | mVelocityTracker.computeCurrentVelocity(1000, 14000); 316 | float xVelocity = mVelocityTracker.getXVelocity(pointerId); 317 | int o = mTotalOffset % mUnit; 318 | int scrollX; 319 | if (Math.abs(xVelocity) < mMinVelocityX && o != 0) { 320 | if (o >= mUnit / 2) 321 | scrollX = mUnit - o; 322 | else scrollX = -o; 323 | int dur = (int) (Math.abs((scrollX + 0f) / mUnit) * duration); 324 | Log.i(TAG, "onTouch: ======BREW==="); 325 | brewAndStartAnimator(dur, scrollX); 326 | } 327 | } 328 | return false; 329 | } 330 | 331 | }; 332 | 333 | private RecyclerView.OnFlingListener mOnFlingListener = new RecyclerView.OnFlingListener() { 334 | @Override 335 | public boolean onFling(int velocityX, int velocityY) { 336 | int o = mTotalOffset % mUnit; 337 | int s = mUnit - o; 338 | int scrollX; 339 | int vel = absMax(velocityX, velocityY); 340 | if (vel * direction.layoutDirection > 0) { 341 | scrollX = s; 342 | } else 343 | scrollX = -o; 344 | int dur = computeSettleDuration(Math.abs(scrollX), Math.abs(vel)); 345 | brewAndStartAnimator(dur, scrollX); 346 | setScrollStateIdle(); 347 | return true; 348 | } 349 | }; 350 | 351 | private int absMax(int a, int b) { 352 | if (Math.abs(a) > Math.abs(b)) 353 | return a; 354 | else return b; 355 | } 356 | 357 | @Override 358 | public void onAttachedToWindow(RecyclerView view) { 359 | super.onAttachedToWindow(view); 360 | mRV = view; 361 | //check when raise finger and settle to the appropriate item 362 | view.setOnTouchListener(mTouchListener); 363 | 364 | view.setOnFlingListener(mOnFlingListener); 365 | } 366 | 367 | private int computeSettleDuration(int distance, float xvel) { 368 | float sWeight = 0.5f * distance / mUnit; 369 | float velWeight = xvel > 0 ? 0.5f * mMinVelocityX / xvel : 0; 370 | 371 | return (int) ((sWeight + velWeight) * duration); 372 | } 373 | 374 | private void brewAndStartAnimator(int dur, int finalXorY) { 375 | animator = ObjectAnimator.ofInt(StackLayoutManager.this, "animateValue", 0, finalXorY); 376 | animator.setDuration(dur); 377 | animator.start(); 378 | animator.addListener(new AnimatorListenerAdapter() { 379 | @Override 380 | public void onAnimationEnd(Animator animation) { 381 | lastAnimateValue = 0; 382 | } 383 | 384 | @Override 385 | public void onAnimationCancel(Animator animation) { 386 | lastAnimateValue = 0; 387 | } 388 | }); 389 | } 390 | 391 | /******************************precise math method*******************************/ 392 | private float alpha(int position) { 393 | float alpha; 394 | int currPos = mTotalOffset / mUnit; 395 | float n = (mTotalOffset + .0f) / mUnit; 396 | if (position > currPos) 397 | alpha = 1.0f; 398 | else { 399 | //temporary linear map,barely ok 400 | alpha = 1 - (n - position) / maxStackCount; 401 | } 402 | //for precise checking,oh may be kind of dummy 403 | return alpha <= 0.001f ? 0 : alpha; 404 | } 405 | 406 | private float scale(int position) { 407 | switch (direction) { 408 | default: 409 | case LEFT: 410 | case RIGHT: 411 | return scaleDefault(position); 412 | } 413 | } 414 | 415 | private float scaleDefault(int position) { 416 | 417 | float scale; 418 | int currPos = this.mTotalOffset / mUnit; 419 | float n = (mTotalOffset + .0f) / mUnit; 420 | float x = n - currPos; 421 | // position >= currPos+1; 422 | if (position >= currPos) { 423 | if (position == currPos) 424 | scale = 1 - scaleRatio * (n - currPos) / maxStackCount; 425 | else if (position == currPos + 1) 426 | //let the item's (index:position+1) scale be 1 when the item slide 1/2 mUnit, 427 | // this have better visual effect 428 | { 429 | // scale = 0.8f + (0.4f * x >= 0.2f ? 0.2f : 0.4f * x); 430 | scale = secondaryScale + (x > 0.5f ? 1 - secondaryScale : 2 * (1 - secondaryScale) * x); 431 | } else scale = secondaryScale; 432 | } else {//position <= currPos 433 | if (position < currPos - maxStackCount) 434 | scale = 0f; 435 | else { 436 | scale = 1f - scaleRatio * (n - currPos + currPos - position) / maxStackCount; 437 | } 438 | } 439 | return scale; 440 | } 441 | 442 | /** 443 | * @param position the index of the item in the adapter 444 | * @return the accurate left position for the given item 445 | */ 446 | private int left(int position) { 447 | 448 | 449 | int currPos = mTotalOffset / mUnit; 450 | int tail = mTotalOffset % mUnit; 451 | float n = (mTotalOffset + .0f) / mUnit; 452 | float x = n - currPos; 453 | 454 | switch (direction) { 455 | default: 456 | case LEFT: 457 | case TOP: 458 | //from left to right or top to bottom 459 | //these two scenario are actually same 460 | return ltr(position, currPos, tail, x); 461 | case RIGHT: 462 | return rtl(position, currPos, tail, x); 463 | } 464 | } 465 | 466 | /** 467 | * @param position .. 468 | * @param currPos .. 469 | * @param tail .. change 470 | * @param x .. 471 | * @return the left position for given item 472 | */ 473 | private int rtl(int position, int currPos, int tail, float x) { 474 | //虽然是做对称变换,但是必须考虑到scale给 对称变换带来的影响 475 | float scale = scale(position); 476 | int ltr = ltr(position, currPos, tail, x); 477 | return (int) (getWidth() - ltr - (mItemWidth) * scale); 478 | } 479 | 480 | private int ltr(int position, int currPos, int tail, float x) { 481 | int left; 482 | 483 | if (position <= currPos) { 484 | 485 | if (position == currPos) { 486 | left = (int) (mSpace * (maxStackCount - x)); 487 | } else { 488 | left = (int) (mSpace * (maxStackCount - x - (currPos - position))); 489 | 490 | } 491 | } else { 492 | if (position == currPos + 1) 493 | left = mSpace * maxStackCount + mUnit - tail; 494 | else { 495 | float closestBaseItemScale = scale(currPos + 1); 496 | 497 | //调整因为scale导致的left误差 498 | // left = (int) (mSpace * maxStackCount + (position - currPos) * mUnit - tail 499 | // -(position - currPos)*(mItemWidth) * (1 - closestBaseItemScale)); 500 | 501 | int baseStart = (int) (mSpace * maxStackCount + mUnit - tail + closestBaseItemScale * (mUnit - mSpace) + mSpace); 502 | left = (int) (baseStart + (position - currPos - 2) * mUnit - (position - currPos - 2) * (1 - secondaryScale) * (mUnit - mSpace)); 503 | if (BuildConfig.DEBUG) 504 | Log.i(TAG, "ltr: currPos " + currPos 505 | + " pos:" + position 506 | + " left:" + left 507 | + " baseStart" + baseStart 508 | + " currPos+1:" + left(currPos + 1)); 509 | } 510 | left = left <= 0 ? 0 : left; 511 | } 512 | return left; 513 | } 514 | 515 | 516 | @SuppressWarnings("unused") 517 | public void setAnimateValue(int animateValue) { 518 | this.animateValue = animateValue; 519 | int dy = this.animateValue - lastAnimateValue; 520 | fill(recycler, direction.layoutDirection * dy, false); 521 | lastAnimateValue = animateValue; 522 | } 523 | 524 | @SuppressWarnings("unused") 525 | public int getAnimateValue() { 526 | return animateValue; 527 | } 528 | 529 | /** 530 | * should recycle view with the given dy or say check if the 531 | * view is out of the bound after the dy is applied 532 | * 533 | * @param view .. 534 | * @param dy .. 535 | * @return .. 536 | */ 537 | private boolean recycleHorizontally(View view/*int position*/, int dy) { 538 | return view != null && (view.getLeft() - dy < 0 || view.getRight() - dy > getWidth()); 539 | } 540 | 541 | private boolean recycleVertically(View view, int dy) { 542 | return view != null && (view.getTop() - dy < 0 || view.getBottom() - dy > getHeight()); 543 | } 544 | 545 | 546 | @Override 547 | public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { 548 | return fill(recycler, dx); 549 | } 550 | 551 | @Override 552 | public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { 553 | return fill(recycler, dy); 554 | } 555 | 556 | @Override 557 | public boolean canScrollHorizontally() { 558 | return direction == LEFT || direction == RIGHT; 559 | } 560 | 561 | @Override 562 | public boolean canScrollVertically() { 563 | return direction == TOP || direction == BOTTOM; 564 | } 565 | 566 | @Override 567 | public RecyclerView.LayoutParams generateDefaultLayoutParams() { 568 | return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT); 569 | } 570 | 571 | /** 572 | * we need to set scrollstate to {@link RecyclerView#SCROLL_STATE_IDLE} idle 573 | * stop RV from intercepting the touch event which block the item click 574 | */ 575 | private void setScrollStateIdle() { 576 | try { 577 | if (sSetScrollState == null) 578 | sSetScrollState = RecyclerView.class.getDeclaredMethod("setScrollState", int.class); 579 | sSetScrollState.setAccessible(true); 580 | sSetScrollState.invoke(mRV, RecyclerView.SCROLL_STATE_IDLE); 581 | } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 582 | e.printStackTrace(); 583 | } 584 | } 585 | 586 | @Override 587 | public void scrollToPosition(int position) { 588 | if (position > getItemCount() - 1) { 589 | Log.i(TAG, "position is " + position + " but itemCount is " + getItemCount()); 590 | return; 591 | } 592 | int currPosition = mTotalOffset / mUnit; 593 | int distance = (position - currPosition) * mUnit; 594 | int dur = computeSettleDuration(Math.abs(distance), 0); 595 | brewAndStartAnimator(dur, distance); 596 | } 597 | 598 | @Override 599 | public void requestLayout() { 600 | super.requestLayout(); 601 | initial = false; 602 | } 603 | 604 | @SuppressWarnings("unused") 605 | public interface CallBack { 606 | 607 | float scale(int totalOffset, int position); 608 | 609 | float alpha(int totalOffset, int position); 610 | 611 | float left(int totalOffset, int position); 612 | } 613 | } 614 | -------------------------------------------------------------------------------- /app/src/main/java/com/hirayclay/VerticalActivity.java: -------------------------------------------------------------------------------- 1 | package com.hirayclay; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.Menu; 7 | import android.view.MenuItem; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import butterknife.BindView; 13 | import butterknife.ButterKnife; 14 | 15 | public class VerticalActivity extends AppCompatActivity { 16 | 17 | @BindView(R.id.recyclerview_vertical) 18 | RecyclerView verticalRecyclerview; 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | setContentView(R.layout.activity_vertical); 24 | ButterKnife.bind(this); 25 | vr(); 26 | } 27 | 28 | private void vr() { 29 | List datas = new ArrayList<>(); 30 | for (int i = 0; i < 15; i++) { 31 | datas.add(String.valueOf(i)); 32 | } 33 | 34 | Config config = new Config(); 35 | config.secondaryScale = 0.95f; 36 | config.scaleRatio = 0.4f; 37 | config.maxStackCount = 4; 38 | config.initialStackCount = 4; 39 | config.space = 45; 40 | config.parallex = 1.5f; 41 | config.align = Align.TOP; 42 | verticalRecyclerview.setLayoutManager(new StackLayoutManager(config)); 43 | verticalRecyclerview.setAdapter(new StackAdapter(datas).vertical()); 44 | } 45 | 46 | @Override 47 | public boolean onOptionsItemSelected(MenuItem item) { 48 | switch (item.getItemId()) { 49 | case R.id.reset: 50 | vr(); 51 | break; 52 | } 53 | return super.onOptionsItemSelected(item); 54 | } 55 | 56 | @Override 57 | public boolean onCreateOptionsMenu(Menu menu) { 58 | getMenuInflater().inflate(R.menu.rest, menu); 59 | return super.onCreateOptionsMenu(menu); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/res/animator/item_animator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/xm1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HirayClay/StackLayoutManager/89902db36b316c89f68547a75feb57a7a25edb34/app/src/main/res/drawable/xm1.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/xm2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HirayClay/StackLayoutManager/89902db36b316c89f68547a75feb57a7a25edb34/app/src/main/res/drawable/xm2.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/xm3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HirayClay/StackLayoutManager/89902db36b316c89f68547a75feb57a7a25edb34/app/src/main/res/drawable/xm3.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/xm4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HirayClay/StackLayoutManager/89902db36b316c89f68547a75feb57a7a25edb34/app/src/main/res/drawable/xm4.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/xm5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HirayClay/StackLayoutManager/89902db36b316c89f68547a75feb57a7a25edb34/app/src/main/res/drawable/xm5.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/xm6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HirayClay/StackLayoutManager/89902db36b316c89f68547a75feb57a7a25edb34/app/src/main/res/drawable/xm6.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/xm7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HirayClay/StackLayoutManager/89902db36b316c89f68547a75feb57a7a25edb34/app/src/main/res/drawable/xm7.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/xm8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HirayClay/StackLayoutManager/89902db36b316c89f68547a75feb57a7a25edb34/app/src/main/res/drawable/xm8.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/xm9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HirayClay/StackLayoutManager/89902db36b316c89f68547a75feb57a7a25edb34/app/src/main/res/drawable/xm9.jpg -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 |