├── .idea └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── anarchy │ │ └── classifyview │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── anarchy │ │ │ └── classifyview │ │ │ ├── ContentActivity.java │ │ │ ├── MainActivity.java │ │ │ └── sample │ │ │ ├── custom │ │ │ ├── CustomClassifyView.java │ │ │ ├── CustomFragment.java │ │ │ └── MyDragDrawable.java │ │ │ ├── demonstrate │ │ │ ├── DemonstrateFragment.java │ │ │ └── logic │ │ │ │ ├── Book.java │ │ │ │ ├── BookListAdapter.java │ │ │ │ ├── BookListener.java │ │ │ │ ├── NetManager.java │ │ │ │ └── SelectBookListAdapter.java │ │ │ └── normal │ │ │ ├── Bean.java │ │ │ ├── MyAdapter.java │ │ │ └── NormalFragment.java │ └── res │ │ ├── drawable │ │ └── round_shape.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── content_main.xml │ │ ├── demonstrate_main.xml │ │ ├── item.xml │ │ ├── item_book.xml │ │ ├── item_book_inner.xml │ │ ├── item_inner.xml │ │ ├── item_select_list.xml │ │ ├── normal.xml │ │ └── select_book.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── anarchy │ └── classifyview │ └── ExampleUnitTest.java ├── build.gradle ├── classify ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── anarchy │ │ └── classify │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── anarchy │ │ │ └── classify │ │ │ ├── ChangeInfo.java │ │ │ ├── ClassifyDragShadowBuilder.java │ │ │ ├── ClassifyItemAnimator.java │ │ │ ├── ClassifyView.java │ │ │ ├── DragDrawable.java │ │ │ ├── adapter │ │ │ ├── BaseMainAdapter.java │ │ │ ├── BaseSubAdapter.java │ │ │ ├── MainRecyclerViewCallBack.java │ │ │ ├── SubAdapterReference.java │ │ │ └── SubRecyclerViewCallBack.java │ │ │ ├── simple │ │ │ ├── BaseSimpleAdapter.java │ │ │ ├── SimpleAdapter.java │ │ │ └── widget │ │ │ │ ├── BagDrawable.java │ │ │ │ ├── CanMergeView.java │ │ │ │ └── InsertAbleGridView.java │ │ │ └── util │ │ │ └── L.java │ └── res │ │ ├── drawable │ │ └── ic_add_black_24dp.xml │ │ ├── layout │ │ └── simple_item.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── classify_style.xml │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── anarchy │ └── classify │ └── ExampleUnitTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshot └── classifyView.gif └── settings.gradle /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #JitPack 2 | [![](https://jitpack.io/v/tianzhijiexian/DBinding.svg)](https://jitpack.io/#AlphaBoom/ClassifyView/0.2.0) 3 | # ClassifyView 4 | 类似Launcher效果的拖拽合并的RecyclerView 5 | #效果如下 6 | ![image](https://github.com/AlphaBoom/ClassifyView/blob/master/screenshot/classifyView.gif) 7 | #使用配置 8 | **Step one:**Add the JitPack repository to your build file 9 | ``` 10 | allprojects { 11 | repositories { 12 | ... 13 | maven { url "https://jitpack.io" } 14 | } 15 | } 16 | ``` 17 | **Step two:**Add the dependency 18 | ``` 19 | dependencies { 20 | compile 'com.github.AlphaBoom:ClassifyView:0.2.0' 21 | } 22 | ``` 23 | #支持的自定义的属性 24 | ClassifyView attr 25 | 26 | 属性 | 说明 27 | ------------- | ------------- 28 | MainSpanCount | 主层级目录的列数 29 | SubSpanCount | 次级层级目录的列数 30 | ShadowColor | 展开次级目录时阴影的颜色 31 | AnimationDuration | 打开次级目录的动画及合并动画的时间 32 | SubRatio | 次级目录的高度占主层级的高度比例 33 | 34 | InsertAbleGridView(显示合并布局的View) 35 | 36 | 属性 | 说明 37 | ------- | ------- 38 | RowCount | 行数(默认 2) 39 | ColumnCount | 列数(默认 2) 40 | RowGap | 横向每列中的间隙距离 41 | ColumnGap | 纵向每行之间的间隙距离 42 | OutLinePadding | 处于可以合并状态及非合并状态 外围框的距离 43 | OutlineWidth | 外边框的宽度 44 | OutlineColor | 外边框的颜色 45 | InnerPadding | 当内部有多个子View 时 与周围的边距 46 | 47 | #高级自定义 48 | 49 | ##继承ClassifyView 重写以下方法: 50 | 51 | 1. *RecyclerView getMain(Context context, AttributeSet parentAttrs)*
返回主层级使用的 RecyclerView。 52 | 2. *RecyclerView getSub(Context context, AttributeSet parentAttrs)*返回次级层级使用的RecyclerView 53 | 3. *View chooseTarget(View selected, List swapTargets, int curX, int curY)*
当拖拽的View 覆盖到子View时会通过该方法在候选View中选择一个View 为目标View 之后的交互操作都会作用于当前所选择的View 及 这个目标View
@param selected 当前选择的View
@param swapTargets 候选的目标View(候选的目标View 为当前选择的View 能够覆盖到所有View)
@param curX 当前选中View的X轴坐标
@param curY 当前选中View的Y轴坐标 54 | 4. `Drawable getDragDrawable(View view)`
返回用于渲染当前拖动View的显示
@param 当前选中的View
@return 返回Drawable 用于设置拖拽View的背景 55 | 56 | 设置数据方式有两种方式: 57 | 58 | 1. 使用 *ClassifyView.setAdapter(BaseMainAdapter mainAdapter, BaseSubAdapter subAdapter)*用于分别设置主层级及次级层级的适配器 59 | 2. 使用 *setAdapter(BaseSimpleAdapter baseSimpleAdapter)*设置一个混合了主层级及次级层级的适配器,如何自定义可以参考 [SimpleAdapter](https://github.com/AlphaBoom/ClassifyView/blob/master/classify/src/main/java/com/anarchy/classify/simple/SimpleAdapter.java) 60 | 61 | 62 | ##主层级提供的回调 63 | 在BaseAdapter中对于mergeStart等又增加了ViewHolder形式的回调 本质是一样的。 64 | 65 | 回调方法 | 说明 | 是否有默认实现在BaseSubAdapter中 66 | ------ | ----- | ---- 67 | setDragPosition | 设置当前被拖拽的位置 | true,默认效果为隐藏被拖拽的位置 68 | boolean canDragOnLongPress|是否可以长按拖拽该View | false 69 | boolean canDropOVer| 是否可以在对应点放下|true,默认返回true 70 | boolean onMergeStart|第一次处于可合并状态|false 71 | void onMerged|合并结束|false 72 | ChangeInfo onPrepareMerge|当准备进行合并动画时回调,返回的ChangeInfo用于做当前拖拽的View到目标位置的动画|false 73 | void onStartMergeAnimation|开始合并动画的回调|false 74 | void onMergeCancel|当脱离合并状态的回调|false 75 | boolean onMove|当需要触发移动时的回调|false 76 | void moved|移动完成的回调|false 77 | boolean canMergeItem|能否进行合并操作|false 78 | int onLeaveSubRegion|当从次级目录拖动出item到主层级时回调,返回int 为添加到主层级adapter的位置|false 79 | float getVelocity|只对低于这个速度的才判断能否移动(需要配合getCurrentState)|true 80 | int getCurrentState|判断当前处于的状态,返回三个值 Classify.STATE_NONE 无状态,Classify.STATE_MERGE 处于合并状态,Classify.STATE_MOVE 处于移动状态| true 81 | void onItemClick|当item被点击时的回调|false 82 | List explodeItem|用于是否展开次级目录,返回一个List 用于初始化次级目录的数据,对于List size 小于2的不展开次级目录而调用onItemClick|false 83 | 84 | ##次级层级的回调 85 | 次级层级与主层级相似 没有合并的相关回调 单独有两个回调: 86 | 87 | 方法|说明 88 | ---|--- 89 | void initData|用于初始化次级层级数据,初始化的数据来自于主层级的 explodeItem 90 | boolean canDropOver | 对于次层级的item 能否拖动到主层级 91 | 92 | #结语 93 | **当前项目效果展现 使用[SimpleAdapter](https://github.com/AlphaBoom/ClassifyView/blob/master/classify/src/main/java/com/anarchy/classify/simple/SimpleAdapter.java),InsertAbleGridView 是配合SimpleAdapter的控件所写,所以本质是一个有两个RecyclerView的自定义View,支持拖拽item并提供相应回调。其他效果自行书写** 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.anarchy.classifyview" 9 | minSdkVersion 15 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | vectorDrawables.useSupportLibrary true 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | 22 | } 23 | 24 | dependencies { 25 | compile fileTree(include: ['*.jar'], dir: 'libs') 26 | testCompile 'junit:junit:4.12' 27 | compile 'com.android.support:appcompat-v7:23.3.0' 28 | compile 'com.squareup.picasso:picasso:2.5.2' 29 | compile project(':classify') 30 | compile 'com.android.support:design:23.3.0' 31 | } 32 | -------------------------------------------------------------------------------- /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 /Users/anarchy/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/anarchy/classifyview/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classifyview; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/anarchy/classifyview/ContentActivity.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classifyview; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v7.app.AppCompatActivity; 7 | 8 | import com.anarchy.classifyview.sample.demonstrate.DemonstrateFragment; 9 | import com.anarchy.classifyview.sample.normal.NormalFragment; 10 | 11 | /** 12 | *

13 | * Date: 16/6/12 09:40 14 | * Author: zhendong.wu@shoufuyou.com 15 | *

16 | */ 17 | public class ContentActivity extends AppCompatActivity { 18 | private Class[] mClasses = new Class[]{NormalFragment.class, DemonstrateFragment.class}; 19 | @Override 20 | protected void onCreate(@Nullable Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.content_main); 23 | int position = getIntent().getIntExtra(MainActivity.EXTRA_POSITION,0); 24 | try { 25 | getSupportFragmentManager().beginTransaction().add(R.id.container,mClasses[position].newInstance()).commit(); 26 | } catch (InstantiationException e) { 27 | e.printStackTrace(); 28 | } catch (IllegalAccessException e) { 29 | e.printStackTrace(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/anarchy/classifyview/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classifyview; 2 | 3 | import android.content.Intent; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.AdapterView; 12 | import android.widget.ArrayAdapter; 13 | import android.widget.ListView; 14 | 15 | import com.anarchy.classify.ClassifyView; 16 | import com.anarchy.classify.adapter.BaseMainAdapter; 17 | import com.anarchy.classify.adapter.BaseSubAdapter; 18 | import com.anarchy.classify.adapter.SubAdapterReference; 19 | import com.anarchy.classify.util.L; 20 | import com.anarchy.classifyview.sample.normal.NormalFragment; 21 | 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | public class MainActivity extends AppCompatActivity implements AdapterView.OnItemClickListener{ 26 | public static final String EXTRA_POSITION = "com.anarchy.classifyview.MainActivity.EXTRA_POSITION"; 27 | @Override 28 | protected void onCreate(Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | setContentView(R.layout.activity_main); 31 | ListView sampleList = (ListView) findViewById(R.id.sample_list); 32 | sampleList.setAdapter(new ArrayAdapter(this, 33 | android.R.layout.simple_list_item_1,getResources().getStringArray(R.array.list_name))); 34 | sampleList.setOnItemClickListener(this); 35 | } 36 | 37 | @Override 38 | public void onItemClick(AdapterView parent, View view, int position, long id) { 39 | Intent i = new Intent(this,ContentActivity.class); 40 | i.putExtra(EXTRA_POSITION,position); 41 | startActivity(i); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/anarchy/classifyview/sample/custom/CustomClassifyView.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classifyview.sample.custom; 2 | 3 | import android.content.Context; 4 | import android.graphics.drawable.Drawable; 5 | import android.support.annotation.NonNull; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.util.AttributeSet; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | 12 | import com.anarchy.classify.ClassifyItemAnimator; 13 | import com.anarchy.classify.ClassifyView; 14 | 15 | /** 16 | *

17 | * Date: 16/6/13 11:58 18 | * Author: zhendong.wu@shoufuyou.com 19 | *

20 | */ 21 | public class CustomClassifyView extends ClassifyView { 22 | public CustomClassifyView(Context context) { 23 | super(context); 24 | } 25 | 26 | public CustomClassifyView(Context context, AttributeSet attrs) { 27 | super(context, attrs); 28 | } 29 | 30 | public CustomClassifyView(Context context, AttributeSet attrs, int defStyleAttr) { 31 | super(context, attrs, defStyleAttr); 32 | } 33 | 34 | @NonNull 35 | @Override 36 | protected RecyclerView getMain(Context context, AttributeSet parentAttrs) { 37 | return super.getMain(context, parentAttrs); 38 | } 39 | 40 | /** 41 | * 设置次级目录的RecyclerView 的布局为 竖直排列 42 | * @param context 43 | * @param parentAttrs 44 | * @return 45 | */ 46 | @NonNull 47 | @Override 48 | protected RecyclerView getSub(Context context, AttributeSet parentAttrs) { 49 | RecyclerView recyclerView = new RecyclerView(context); 50 | recyclerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 51 | recyclerView.setLayoutManager(new LinearLayoutManager(context)); 52 | recyclerView.setItemAnimator(new ClassifyItemAnimator()); 53 | return recyclerView; 54 | } 55 | 56 | 57 | @Override 58 | protected Drawable getDragDrawable(View view) { 59 | return new MyDragDrawable(view); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/anarchy/classifyview/sample/custom/CustomFragment.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classifyview.sample.custom; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | /** 11 | * Version 1.0 12 | *

13 | * Date: 16/6/13 11:55 14 | * Author: zhendong.wu@shoufuyou.com 15 | *

16 | * Copyright © 2014-2016 Shanghai Xiaotu Network Technology Co., Ltd. 17 | */ 18 | public class CustomFragment extends Fragment { 19 | @Nullable 20 | @Override 21 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 22 | return super.onCreateView(inflater, container, savedInstanceState); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/anarchy/classifyview/sample/custom/MyDragDrawable.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classifyview.sample.custom; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Canvas; 5 | import android.graphics.ColorFilter; 6 | import android.graphics.PixelFormat; 7 | import android.graphics.drawable.Drawable; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | import com.anarchy.classify.simple.widget.CanMergeView; 12 | 13 | /** 14 | *

15 | * Date: 16/6/13 12:00 16 | * Author: zhendong.wu@shoufuyou.com 17 | *

18 | * 只对CanMergeView 做拖拽显示 19 | */ 20 | public class MyDragDrawable extends Drawable { 21 | private View mView; 22 | private Bitmap mCacheBitmap; 23 | 24 | public MyDragDrawable(View view){ 25 | if(view instanceof ViewGroup){ 26 | mView = getCanMergeView((ViewGroup) view); 27 | if(mView == null) mView = view; 28 | }else { 29 | mView = view; 30 | } 31 | mView.setDrawingCacheEnabled(true); 32 | mView.buildDrawingCache(); 33 | mCacheBitmap = mView.getDrawingCache(); 34 | } 35 | 36 | 37 | 38 | private View getCanMergeView(ViewGroup viewGroup){ 39 | for(int i=0;i 34 | * Date: 16/6/12 10:09 35 | * Author: zhendong.wu@shoufuyou.com 36 | *

37 | */ 38 | public class DemonstrateFragment extends Fragment{ 39 | private NetManager mNetManager = new NetManager(); 40 | private List> mBooks = new ArrayList<>(); 41 | private BookListAdapter mAdapter; 42 | @Nullable 43 | @Override 44 | public View onCreateView(LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable Bundle savedInstanceState) { 45 | View view = inflater.inflate(R.layout.demonstrate_main,container,false); 46 | FloatingActionButton button = (FloatingActionButton) view.findViewById(R.id.add_button); 47 | ClassifyView classifyView = (ClassifyView) view.findViewById(R.id.classify_view); 48 | mAdapter = new BookListAdapter(mBooks); 49 | classifyView.setAdapter(mAdapter); 50 | button.setOnClickListener(new View.OnClickListener() { 51 | @Override 52 | public void onClick(View v) { 53 | final AlertDialog.Builder builder = new AlertDialog.Builder(v.getContext()); 54 | View content = LayoutInflater.from(v.getContext()).inflate(R.layout.select_book,null); 55 | RecyclerView recyclerView = (RecyclerView) content.findViewById(R.id.select_list); 56 | final ProgressBar progressBar = (ProgressBar) content.findViewById(R.id.progress_bar); 57 | recyclerView.setLayoutManager(new GridLayoutManager(v.getContext(),2)); 58 | final SelectBookListAdapter selectBookListAdapter = new SelectBookListAdapter(); 59 | recyclerView.setAdapter(selectBookListAdapter); 60 | builder.setView(content); 61 | final AlertDialog dialog = builder.show(); 62 | selectBookListAdapter.setItemClickListener(new SelectBookListAdapter.ItemClickListener() { 63 | @Override 64 | public void onItemClick(View parent, int position,Book book) { 65 | List books = new ArrayList<>(); 66 | books.add(book); 67 | mBooks.add(books); 68 | mAdapter.notifyItemInsert(mBooks.size()-1); 69 | dialog.hide(); 70 | } 71 | }); 72 | progressBar.setVisibility(View.VISIBLE); 73 | mNetManager.getBookList(new BookListener() { 74 | @Override 75 | public void onSuccess(String result) { 76 | Log.d("wzd",result); 77 | progressBar.setVisibility(View.INVISIBLE); 78 | List books = new ArrayList<>(); 79 | try { 80 | JSONObject jsonObject = new JSONObject(result); 81 | JSONArray jsonArray = jsonObject.optJSONArray("list"); 82 | if(jsonArray != null){ 83 | for(int i = 0;i 5 | * Date: 16/6/12 14:02 6 | * Author: zhendong.wu@shoufuyou.com 7 | *

8 | */ 9 | public class Book { 10 | public String imageUrl; 11 | public String name; 12 | public String id; 13 | public String summary; 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/anarchy/classifyview/sample/demonstrate/logic/BookListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classifyview.sample.demonstrate.logic; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.ImageView; 7 | import android.widget.TextView; 8 | 9 | import com.anarchy.classify.simple.SimpleAdapter; 10 | import com.anarchy.classifyview.R; 11 | import com.squareup.picasso.Picasso; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | *

17 | * Date: 16/6/12 14:38 18 | * Author: zhendong.wu@shoufuyou.com 19 | *

20 | */ 21 | public class BookListAdapter extends SimpleAdapter { 22 | 23 | 24 | public BookListAdapter(List> mData) { 25 | super(mData); 26 | } 27 | 28 | 29 | @Override 30 | protected ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 31 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_book,parent,false); 32 | return new ViewHolder(view); 33 | } 34 | 35 | @Override 36 | public View getView(ViewGroup parent, int mainPosition, int subPosition) { 37 | ImageView imageView = (ImageView) LayoutInflater.from(parent.getContext()).inflate(R.layout.item_book_inner,parent,false); 38 | String url = mData.get(mainPosition).get(subPosition).imageUrl; 39 | Picasso.with(parent.getContext()).load(url).into(imageView); 40 | return imageView; 41 | } 42 | 43 | @Override 44 | protected void onBindMainViewHolder(ViewHolder holder, int position) { 45 | List books = mData.get(position); 46 | if(books.size()>1){ 47 | holder.name.setText(""); 48 | }else { 49 | holder.name.setText(books.get(0).name); 50 | } 51 | } 52 | 53 | @Override 54 | protected void onBindSubViewHolder(ViewHolder holder, int mainPosition, int subPosition) { 55 | holder.name.setText(mData.get(mainPosition).get(subPosition).name+""); 56 | } 57 | 58 | public static class ViewHolder extends SimpleAdapter.ViewHolder { 59 | TextView name; 60 | public ViewHolder(View itemView) { 61 | super(itemView); 62 | name = (TextView) itemView.findViewById(R.id.text_name); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/anarchy/classifyview/sample/demonstrate/logic/BookListener.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classifyview.sample.demonstrate.logic; 2 | 3 | /** 4 | *

5 | * Date: 16/6/12 14:03 6 | * Author: zhendong.wu@shoufuyou.com 7 | *

8 | */ 9 | public interface BookListener { 10 | void onSuccess(String result); 11 | void onFailure(Exception e); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/anarchy/classifyview/sample/demonstrate/logic/NetManager.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classifyview.sample.demonstrate.logic; 2 | 3 | import android.net.Uri; 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | import android.os.Message; 7 | import android.text.TextUtils; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.InputStream; 11 | import java.io.InputStreamReader; 12 | import java.net.HttpURLConnection; 13 | import java.net.URL; 14 | 15 | /** 16 | *

17 | * Date: 16/6/12 11:37 18 | * Author: zhendong.wu@shoufuyou.com 19 | *

20 | */ 21 | public class NetManager { 22 | private static final String BASE_URL = "http://www.tngou.net/api/book"; 23 | private static final int SUCCESS = 1; 24 | private static final int FAILURE = 0; 25 | 26 | private String get(String path) throws Exception { 27 | String result = ""; 28 | StringBuilder sb = new StringBuilder(); 29 | URL url = new URL(BASE_URL + path); 30 | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 31 | connection.setRequestMethod("GET"); 32 | if (connection.getResponseCode() == 200) { 33 | InputStream in = connection.getInputStream(); 34 | BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8")); 35 | String strRead; 36 | while ((strRead = reader.readLine()) != null) { 37 | sb.append(strRead); 38 | } 39 | reader.close(); 40 | result = sb.toString(); 41 | } 42 | return result; 43 | } 44 | 45 | 46 | public String getClassify() throws Exception { 47 | return get("/classify"); 48 | } 49 | 50 | public String getList(String page, String rows, String id) throws Exception { 51 | Uri uri = Uri.parse("/list"); 52 | Uri.Builder builder = uri.buildUpon(); 53 | if(!TextUtils.isEmpty(page)) { 54 | builder.appendQueryParameter("page", page); 55 | } 56 | if(!TextUtils.isEmpty(rows)) { 57 | builder.appendQueryParameter("rows", rows); 58 | } 59 | if(!TextUtils.isEmpty(id)) { 60 | builder.appendQueryParameter("id", id); 61 | } 62 | uri = builder.build(); 63 | return get(uri.toString()); 64 | } 65 | 66 | public String getDetail(String id) throws Exception { 67 | Uri uri = Uri.parse("/show"); 68 | uri = uri.buildUpon().appendQueryParameter("id", id).build(); 69 | return get(uri.toString()); 70 | } 71 | 72 | 73 | public void getBookList(final BookListener bookListener) { 74 | final Handler handler = new Handler(Looper.getMainLooper()){ 75 | @Override 76 | public void handleMessage(Message msg) { 77 | int state = msg.what; 78 | if(state == SUCCESS){ 79 | bookListener.onSuccess((String) msg.obj); 80 | }else { 81 | bookListener.onFailure((Exception) msg.obj); 82 | } 83 | } 84 | }; 85 | new Thread(new Runnable() { 86 | @Override 87 | public void run() { 88 | try { 89 | String result = getList(null,40+"",null); 90 | Message message = Message.obtain(); 91 | message.what = SUCCESS; 92 | message.obj = result; 93 | handler.sendMessage(message); 94 | } catch (Exception e) { 95 | Message message = Message.obtain(); 96 | message.what = FAILURE; 97 | message.obj = e; 98 | handler.sendMessage(message); 99 | } 100 | } 101 | }).start(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/anarchy/classifyview/sample/demonstrate/logic/SelectBookListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classifyview.sample.demonstrate.logic; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageView; 8 | import android.widget.TextView; 9 | 10 | import com.anarchy.classifyview.R; 11 | import com.squareup.picasso.Picasso; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | *

17 | * Date: 16/6/12 14:20 18 | * Author: zhendong.wu@shoufuyou.com 19 | *

20 | */ 21 | public class SelectBookListAdapter extends RecyclerView.Adapter { 22 | private List mBookList; 23 | private ItemClickListener mItemClickListener; 24 | 25 | 26 | 27 | public void setItemClickListener(ItemClickListener listener){ 28 | mItemClickListener = listener; 29 | } 30 | 31 | 32 | public void setBookList(List bookList){ 33 | mBookList = bookList; 34 | notifyDataSetChanged(); 35 | } 36 | 37 | @Override 38 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 39 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_select_list,parent,false); 40 | return new ViewHolder(view); 41 | } 42 | 43 | @Override 44 | public void onBindViewHolder(ViewHolder holder, final int position) { 45 | final Book book = mBookList.get(position); 46 | holder.title.setText(book.name); 47 | holder.summary.setText(book.summary); 48 | Picasso.with(holder.itemView.getContext()).load(book.imageUrl).into(holder.cover); 49 | if(mItemClickListener != null){ 50 | holder.itemView.setOnClickListener(new View.OnClickListener() { 51 | @Override 52 | public void onClick(View v) { 53 | if(mItemClickListener != null){ 54 | mItemClickListener.onItemClick(v,position,book); 55 | } 56 | } 57 | }); 58 | } 59 | } 60 | 61 | @Override 62 | public int getItemCount() { 63 | if(mBookList != null) return mBookList.size(); 64 | return 0; 65 | } 66 | 67 | public interface ItemClickListener{ 68 | void onItemClick(View parent,int position,Book book); 69 | } 70 | 71 | 72 | public static class ViewHolder extends RecyclerView.ViewHolder{ 73 | private ImageView cover; 74 | private TextView title; 75 | private TextView summary; 76 | public ViewHolder(View itemView) { 77 | super(itemView); 78 | cover = (ImageView) itemView.findViewById(R.id.book_cover); 79 | title = (TextView) itemView.findViewById(R.id.text_title); 80 | summary = (TextView) itemView.findViewById(R.id.text_summary); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/anarchy/classifyview/sample/normal/Bean.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classifyview.sample.normal; 2 | 3 | /** 4 | *

5 | * Date: 16/6/7 16:41 6 | * Author: zhendong.wu@shoufuyou.com 7 | *

8 | */ 9 | public class Bean { 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/anarchy/classifyview/sample/normal/MyAdapter.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classifyview.sample.normal; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.Toast; 7 | 8 | import com.anarchy.classify.simple.SimpleAdapter; 9 | import com.anarchy.classifyview.R; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | *

15 | * Date: 16/6/7 16:40 16 | * Author: zhendong.wu@shoufuyou.com 17 | *

18 | */ 19 | public class MyAdapter extends SimpleAdapter { 20 | 21 | 22 | public MyAdapter(List> mData) { 23 | super(mData); 24 | } 25 | 26 | 27 | 28 | 29 | @Override 30 | protected ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 31 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item,parent,false); 32 | return new MyAdapter.ViewHolder(view); 33 | } 34 | 35 | @Override 36 | public View getView(ViewGroup parent, int mainPosition, int subPosition) { 37 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_inner,parent,false); 38 | return view; 39 | } 40 | 41 | @Override 42 | protected void onItemClick(View view, int parentIndex, int index) { 43 | Toast.makeText(view.getContext(),"parentIndex: "+parentIndex+"\nindex: "+index,Toast.LENGTH_SHORT).show(); 44 | } 45 | 46 | static class ViewHolder extends SimpleAdapter.ViewHolder { 47 | 48 | public ViewHolder(View itemView) { 49 | super(itemView); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/anarchy/classifyview/sample/normal/NormalFragment.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classifyview.sample.normal; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import com.anarchy.classify.ClassifyView; 11 | import com.anarchy.classifyview.R; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | /** 17 | *

18 | * Date: 16/6/12 09:51 19 | * Author: zhendong.wu@shoufuyou.com 20 | *

21 | */ 22 | public class NormalFragment extends Fragment{ 23 | @Nullable 24 | @Override 25 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 26 | View view = inflater.inflate(R.layout.normal,container,false); 27 | ClassifyView classifyView = (ClassifyView) view.findViewById(R.id.classify_view); 28 | List> data = new ArrayList<>(); 29 | for(int i=0;i<30;i++){ 30 | List inner = new ArrayList<>(); 31 | if(i>10) { 32 | int c = (int) (Math.random() * 15+1); 33 | for(int j=0;j 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/demonstrate_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_book.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_book_inner.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_inner.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_select_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | 21 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/normal.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/select_book.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beasonshu/ClassifyView/3771450ad6782d4e31b5694231194b8d7892ac8f/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beasonshu/ClassifyView/3771450ad6782d4e31b5694231194b8d7892ac8f/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beasonshu/ClassifyView/3771450ad6782d4e31b5694231194b8d7892ac8f/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beasonshu/ClassifyView/3771450ad6782d4e31b5694231194b8d7892ac8f/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beasonshu/ClassifyView/3771450ad6782d4e31b5694231194b8d7892ac8f/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ClassifyView 3 | 4 | 基本效果 5 | 使用演示 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/anarchy/classifyview/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classifyview; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 9 | */ 10 | public class ExampleUnitTest { 11 | @Test 12 | public void addition_isCorrect() throws Exception { 13 | assertEquals(4, 2 + 2); 14 | } 15 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.1.2' 9 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /classify/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /classify/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | 4 | group='com.github.AlphaBoom' 5 | android { 6 | compileSdkVersion 23 7 | buildToolsVersion "23.0.3" 8 | 9 | defaultConfig { 10 | minSdkVersion 15 11 | targetSdkVersion 23 12 | versionCode 1 13 | versionName "1.0" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | compile fileTree(include: ['*.jar'], dir: 'libs') 25 | testCompile 'junit:junit:4.12' 26 | compile 'com.android.support:appcompat-v7:23.3.0' 27 | compile 'com.android.support:recyclerview-v7:23.3.0' 28 | } 29 | -------------------------------------------------------------------------------- /classify/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/anarchy/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /classify/src/androidTest/java/com/anarchy/classify/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /classify/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/ChangeInfo.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify; 2 | 3 | import android.graphics.Point; 4 | 5 | /** 6 | * User: Anarchy 7 | * Email: rsshinide38@163.com 8 | * CreateTime: 六月/09/2016 15:54. 9 | * Description: 10 | */ 11 | public class ChangeInfo { 12 | public int left; 13 | public int top; 14 | public int itemWidth; 15 | public int itemHeight; 16 | public int paddingLeft; 17 | public int paddingTop; 18 | public int paddingBottom; 19 | public int paddingRight; 20 | public int outlinePadding; 21 | @Override 22 | public String toString() { 23 | return "ChangeInfo{" + 24 | "left=" + left + 25 | ", top=" + top + 26 | ", itemWidth=" + itemWidth + 27 | ", itemHeight=" + itemHeight + 28 | '}'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/ClassifyDragShadowBuilder.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Color; 5 | import android.graphics.Point; 6 | import android.graphics.PorterDuff; 7 | import android.view.View; 8 | 9 | import java.lang.ref.WeakReference; 10 | 11 | /** 12 | *只是为了获取drag的触发事件 不绘制拖动的view 13 | */ 14 | public class ClassifyDragShadowBuilder extends View.DragShadowBuilder { 15 | private final WeakReference mView; 16 | public ClassifyDragShadowBuilder(){ 17 | mView = new WeakReference<>(null); 18 | } 19 | public ClassifyDragShadowBuilder(View view){ 20 | mView = new WeakReference<>(view); 21 | } 22 | 23 | @Override 24 | public void onDrawShadow(Canvas canvas) { 25 | //nothing 26 | } 27 | 28 | @Override 29 | public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) { 30 | final View view = mView.get(); 31 | if (view != null) { 32 | shadowSize.set(view.getWidth(), view.getHeight()); 33 | shadowTouchPoint.set(shadowSize.x/2, shadowSize.y/2); 34 | } 35 | } 36 | 37 | public void showShadow(){ 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/ClassifyItemAnimator.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.v4.animation.AnimatorCompatHelper; 5 | import android.support.v4.view.ViewCompat; 6 | import android.support.v4.view.ViewPropertyAnimatorCompat; 7 | import android.support.v4.view.ViewPropertyAnimatorListener; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.support.v7.widget.SimpleItemAnimator; 10 | import android.view.View; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * Version 1.0 17 | *

18 | * Date: 16/6/8 14:12 19 | * Author: zhendong.wu@shoufuyou.com 20 | *

21 | * Copyright © 2014-2016 Shanghai Xiaotu Network Technology Co., Ltd. 22 | */ 23 | public class ClassifyItemAnimator extends SimpleItemAnimator { 24 | private static final boolean DEBUG = false; 25 | 26 | private ArrayList mPendingRemovals = new ArrayList<>(); 27 | private ArrayList mPendingAdditions = new ArrayList<>(); 28 | private ArrayList mPendingMoves = new ArrayList<>(); 29 | private ArrayList mPendingChanges = new ArrayList<>(); 30 | 31 | private ArrayList> mAdditionsList = new ArrayList<>(); 32 | private ArrayList> mMovesList = new ArrayList<>(); 33 | private ArrayList> mChangesList = new ArrayList<>(); 34 | 35 | private ArrayList mAddAnimations = new ArrayList<>(); 36 | private ArrayList mMoveAnimations = new ArrayList<>(); 37 | private ArrayList mRemoveAnimations = new ArrayList<>(); 38 | private ArrayList mChangeAnimations = new ArrayList<>(); 39 | 40 | private static class MoveInfo { 41 | public RecyclerView.ViewHolder holder; 42 | public int fromX, fromY, toX, toY; 43 | 44 | private MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { 45 | this.holder = holder; 46 | this.fromX = fromX; 47 | this.fromY = fromY; 48 | this.toX = toX; 49 | this.toY = toY; 50 | } 51 | } 52 | 53 | private static class ChangeInfo { 54 | public RecyclerView.ViewHolder oldHolder, newHolder; 55 | public int fromX, fromY, toX, toY; 56 | private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) { 57 | this.oldHolder = oldHolder; 58 | this.newHolder = newHolder; 59 | } 60 | 61 | private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, 62 | int fromX, int fromY, int toX, int toY) { 63 | this(oldHolder, newHolder); 64 | this.fromX = fromX; 65 | this.fromY = fromY; 66 | this.toX = toX; 67 | this.toY = toY; 68 | } 69 | 70 | @Override 71 | public String toString() { 72 | return "ChangeInfo{" + 73 | "oldHolder=" + oldHolder + 74 | ", newHolder=" + newHolder + 75 | ", fromX=" + fromX + 76 | ", fromY=" + fromY + 77 | ", toX=" + toX + 78 | ", toY=" + toY + 79 | '}'; 80 | } 81 | } 82 | 83 | @Override 84 | public void runPendingAnimations() { 85 | boolean removalsPending = !mPendingRemovals.isEmpty(); 86 | boolean movesPending = !mPendingMoves.isEmpty(); 87 | boolean changesPending = !mPendingChanges.isEmpty(); 88 | boolean additionsPending = !mPendingAdditions.isEmpty(); 89 | if (!removalsPending && !movesPending && !additionsPending && !changesPending) { 90 | // nothing to animate 91 | return; 92 | } 93 | // First, remove stuff 94 | for (RecyclerView.ViewHolder holder : mPendingRemovals) { 95 | animateRemoveImpl(holder); 96 | } 97 | mPendingRemovals.clear(); 98 | // Next, move stuff 99 | if (movesPending) { 100 | final ArrayList moves = new ArrayList<>(); 101 | moves.addAll(mPendingMoves); 102 | mMovesList.add(moves); 103 | mPendingMoves.clear(); 104 | Runnable mover = new Runnable() { 105 | @Override 106 | public void run() { 107 | for (MoveInfo moveInfo : moves) { 108 | animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, 109 | moveInfo.toX, moveInfo.toY); 110 | } 111 | moves.clear(); 112 | mMovesList.remove(moves); 113 | } 114 | }; 115 | if (removalsPending) { 116 | View view = moves.get(0).holder.itemView; 117 | ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); 118 | } else { 119 | mover.run(); 120 | } 121 | } 122 | // Next, change stuff, to run in parallel with move animations 123 | if (changesPending) { 124 | final ArrayList changes = new ArrayList<>(); 125 | changes.addAll(mPendingChanges); 126 | mChangesList.add(changes); 127 | mPendingChanges.clear(); 128 | Runnable changer = new Runnable() { 129 | @Override 130 | public void run() { 131 | for (ChangeInfo change : changes) { 132 | animateChangeImpl(change); 133 | } 134 | changes.clear(); 135 | mChangesList.remove(changes); 136 | } 137 | }; 138 | if (removalsPending) { 139 | RecyclerView.ViewHolder holder = changes.get(0).oldHolder; 140 | ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); 141 | } else { 142 | changer.run(); 143 | } 144 | } 145 | // Next, add stuff 146 | if (additionsPending) { 147 | final ArrayList additions = new ArrayList<>(); 148 | additions.addAll(mPendingAdditions); 149 | mAdditionsList.add(additions); 150 | mPendingAdditions.clear(); 151 | Runnable adder = new Runnable() { 152 | public void run() { 153 | for (RecyclerView.ViewHolder holder : additions) { 154 | animateAddImpl(holder); 155 | } 156 | additions.clear(); 157 | mAdditionsList.remove(additions); 158 | } 159 | }; 160 | if (removalsPending || movesPending || changesPending) { 161 | long removeDuration = removalsPending ? getRemoveDuration() : 0; 162 | long moveDuration = movesPending ? getMoveDuration() : 0; 163 | long changeDuration = changesPending ? getChangeDuration() : 0; 164 | long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); 165 | View view = additions.get(0).itemView; 166 | ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); 167 | } else { 168 | adder.run(); 169 | } 170 | } 171 | } 172 | 173 | @Override 174 | public boolean animateRemove(final RecyclerView.ViewHolder holder) { 175 | resetAnimation(holder); 176 | mPendingRemovals.add(holder); 177 | return true; 178 | } 179 | 180 | private void animateRemoveImpl(final RecyclerView.ViewHolder holder) { 181 | final View view = holder.itemView; 182 | final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); 183 | mRemoveAnimations.add(holder); 184 | animation.setDuration(getRemoveDuration()) 185 | .alpha(0).setListener(new VpaListenerAdapter() { 186 | @Override 187 | public void onAnimationStart(View view) { 188 | dispatchRemoveStarting(holder); 189 | } 190 | 191 | @Override 192 | public void onAnimationEnd(View view) { 193 | animation.setListener(null); 194 | ViewCompat.setAlpha(view, 1); 195 | dispatchRemoveFinished(holder); 196 | mRemoveAnimations.remove(holder); 197 | dispatchFinishedWhenDone(); 198 | } 199 | }).start(); 200 | } 201 | 202 | @Override 203 | public boolean animateAdd(final RecyclerView.ViewHolder holder) { 204 | resetAnimation(holder); 205 | ViewCompat.setAlpha(holder.itemView, 0); 206 | mPendingAdditions.add(holder); 207 | return true; 208 | } 209 | 210 | private void animateAddImpl(final RecyclerView.ViewHolder holder) { 211 | final View view = holder.itemView; 212 | final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); 213 | mAddAnimations.add(holder); 214 | animation.alpha(1).setDuration(getAddDuration()). 215 | setListener(new VpaListenerAdapter() { 216 | @Override 217 | public void onAnimationStart(View view) { 218 | dispatchAddStarting(holder); 219 | } 220 | @Override 221 | public void onAnimationCancel(View view) { 222 | ViewCompat.setAlpha(view, 1); 223 | } 224 | 225 | @Override 226 | public void onAnimationEnd(View view) { 227 | animation.setListener(null); 228 | dispatchAddFinished(holder); 229 | mAddAnimations.remove(holder); 230 | dispatchFinishedWhenDone(); 231 | } 232 | }).start(); 233 | } 234 | 235 | @Override 236 | public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY, 237 | int toX, int toY) { 238 | final View view = holder.itemView; 239 | fromX += ViewCompat.getTranslationX(holder.itemView); 240 | fromY += ViewCompat.getTranslationY(holder.itemView); 241 | resetAnimation(holder); 242 | int deltaX = toX - fromX; 243 | int deltaY = toY - fromY; 244 | if (deltaX == 0 && deltaY == 0) { 245 | dispatchMoveFinished(holder); 246 | return false; 247 | } 248 | if (deltaX != 0) { 249 | ViewCompat.setTranslationX(view, -deltaX); 250 | } 251 | if (deltaY != 0) { 252 | ViewCompat.setTranslationY(view, -deltaY); 253 | } 254 | mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); 255 | return true; 256 | } 257 | 258 | private void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { 259 | final View view = holder.itemView; 260 | final int deltaX = toX - fromX; 261 | final int deltaY = toY - fromY; 262 | if (deltaX != 0) { 263 | ViewCompat.animate(view).translationX(0); 264 | } 265 | if (deltaY != 0) { 266 | ViewCompat.animate(view).translationY(0); 267 | } 268 | // TODO: make EndActions end listeners instead, since end actions aren't called when 269 | // vpas are canceled (and can't end them. why?) 270 | // need listener functionality in VPACompat for this. Ick. 271 | final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); 272 | mMoveAnimations.add(holder); 273 | animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() { 274 | @Override 275 | public void onAnimationStart(View view) { 276 | dispatchMoveStarting(holder); 277 | } 278 | @Override 279 | public void onAnimationCancel(View view) { 280 | if (deltaX != 0) { 281 | ViewCompat.setTranslationX(view, 0); 282 | } 283 | if (deltaY != 0) { 284 | ViewCompat.setTranslationY(view, 0); 285 | } 286 | } 287 | @Override 288 | public void onAnimationEnd(View view) { 289 | animation.setListener(null); 290 | dispatchMoveFinished(holder); 291 | mMoveAnimations.remove(holder); 292 | dispatchFinishedWhenDone(); 293 | } 294 | }).start(); 295 | } 296 | 297 | @Override 298 | public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, 299 | int fromX, int fromY, int toX, int toY) { 300 | if (oldHolder == newHolder) { 301 | // Don't know how to run change animations when the same view holder is re-used. 302 | // run a move animation to handle position changes. 303 | return animateMove(oldHolder, fromX, fromY, toX, toY); 304 | } 305 | final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView); 306 | final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView); 307 | final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView); 308 | resetAnimation(oldHolder); 309 | int deltaX = (int) (toX - fromX - prevTranslationX); 310 | int deltaY = (int) (toY - fromY - prevTranslationY); 311 | // recover prev translation state after ending animation 312 | ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX); 313 | ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY); 314 | ViewCompat.setAlpha(oldHolder.itemView, prevAlpha); 315 | if (newHolder != null) { 316 | // carry over translation values 317 | resetAnimation(newHolder); 318 | ViewCompat.setTranslationX(newHolder.itemView, -deltaX); 319 | ViewCompat.setTranslationY(newHolder.itemView, -deltaY); 320 | } 321 | mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); 322 | return true; 323 | } 324 | 325 | private void animateChangeImpl(final ChangeInfo changeInfo) { 326 | final RecyclerView.ViewHolder holder = changeInfo.oldHolder; 327 | final View view = holder == null ? null : holder.itemView; 328 | final RecyclerView.ViewHolder newHolder = changeInfo.newHolder; 329 | final View newView = newHolder != null ? newHolder.itemView : null; 330 | if (view != null) { 331 | final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration( 332 | getChangeDuration()); 333 | mChangeAnimations.add(changeInfo.oldHolder); 334 | oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); 335 | oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); 336 | oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { 337 | @Override 338 | public void onAnimationStart(View view) { 339 | dispatchChangeStarting(changeInfo.oldHolder, true); 340 | } 341 | 342 | @Override 343 | public void onAnimationEnd(View view) { 344 | oldViewAnim.setListener(null); 345 | ViewCompat.setAlpha(view, 1); 346 | ViewCompat.setTranslationX(view, 0); 347 | ViewCompat.setTranslationY(view, 0); 348 | dispatchChangeFinished(changeInfo.oldHolder, true); 349 | mChangeAnimations.remove(changeInfo.oldHolder); 350 | dispatchFinishedWhenDone(); 351 | } 352 | }).start(); 353 | } 354 | if (newView != null) { 355 | final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView); 356 | mChangeAnimations.add(changeInfo.newHolder); 357 | newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()) 358 | .setListener(new VpaListenerAdapter() { 359 | @Override 360 | public void onAnimationStart(View view) { 361 | dispatchChangeStarting(changeInfo.newHolder, false); 362 | } 363 | @Override 364 | public void onAnimationEnd(View view) { 365 | newViewAnimation.setListener(null); 366 | ViewCompat.setTranslationX(newView, 0); 367 | ViewCompat.setTranslationY(newView, 0); 368 | dispatchChangeFinished(changeInfo.newHolder, false); 369 | mChangeAnimations.remove(changeInfo.newHolder); 370 | dispatchFinishedWhenDone(); 371 | } 372 | }).start(); 373 | } 374 | } 375 | 376 | private void endChangeAnimation(List infoList, RecyclerView.ViewHolder item) { 377 | for (int i = infoList.size() - 1; i >= 0; i--) { 378 | ChangeInfo changeInfo = infoList.get(i); 379 | if (endChangeAnimationIfNecessary(changeInfo, item)) { 380 | if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { 381 | infoList.remove(changeInfo); 382 | } 383 | } 384 | } 385 | } 386 | 387 | private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { 388 | if (changeInfo.oldHolder != null) { 389 | endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); 390 | } 391 | if (changeInfo.newHolder != null) { 392 | endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); 393 | } 394 | } 395 | private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) { 396 | boolean oldItem = false; 397 | if (changeInfo.newHolder == item) { 398 | changeInfo.newHolder = null; 399 | } else if (changeInfo.oldHolder == item) { 400 | changeInfo.oldHolder = null; 401 | oldItem = true; 402 | } else { 403 | return false; 404 | } 405 | ViewCompat.setAlpha(item.itemView, 1); 406 | ViewCompat.setTranslationX(item.itemView, 0); 407 | ViewCompat.setTranslationY(item.itemView, 0); 408 | dispatchChangeFinished(item, oldItem); 409 | return true; 410 | } 411 | 412 | @Override 413 | public void endAnimation(RecyclerView.ViewHolder item) { 414 | final View view = item.itemView; 415 | // this will trigger end callback which should set properties to their target values. 416 | ViewCompat.animate(view).cancel(); 417 | // TODO if some other animations are chained to end, how do we cancel them as well? 418 | for (int i = mPendingMoves.size() - 1; i >= 0; i--) { 419 | MoveInfo moveInfo = mPendingMoves.get(i); 420 | if (moveInfo.holder == item) { 421 | ViewCompat.setTranslationY(view, 0); 422 | ViewCompat.setTranslationX(view, 0); 423 | dispatchMoveFinished(item); 424 | mPendingMoves.remove(i); 425 | } 426 | } 427 | endChangeAnimation(mPendingChanges, item); 428 | if (mPendingRemovals.remove(item)) { 429 | ViewCompat.setAlpha(view, 1); 430 | dispatchRemoveFinished(item); 431 | } 432 | if (mPendingAdditions.remove(item)) { 433 | ViewCompat.setAlpha(view, 1); 434 | dispatchAddFinished(item); 435 | } 436 | 437 | for (int i = mChangesList.size() - 1; i >= 0; i--) { 438 | ArrayList changes = mChangesList.get(i); 439 | endChangeAnimation(changes, item); 440 | if (changes.isEmpty()) { 441 | mChangesList.remove(i); 442 | } 443 | } 444 | for (int i = mMovesList.size() - 1; i >= 0; i--) { 445 | ArrayList moves = mMovesList.get(i); 446 | for (int j = moves.size() - 1; j >= 0; j--) { 447 | MoveInfo moveInfo = moves.get(j); 448 | if (moveInfo.holder == item) { 449 | ViewCompat.setTranslationY(view, 0); 450 | ViewCompat.setTranslationX(view, 0); 451 | dispatchMoveFinished(item); 452 | moves.remove(j); 453 | if (moves.isEmpty()) { 454 | mMovesList.remove(i); 455 | } 456 | break; 457 | } 458 | } 459 | } 460 | for (int i = mAdditionsList.size() - 1; i >= 0; i--) { 461 | ArrayList additions = mAdditionsList.get(i); 462 | if (additions.remove(item)) { 463 | ViewCompat.setAlpha(view, 1); 464 | dispatchAddFinished(item); 465 | if (additions.isEmpty()) { 466 | mAdditionsList.remove(i); 467 | } 468 | } 469 | } 470 | 471 | // animations should be ended by the cancel above. 472 | //noinspection PointlessBooleanExpression,ConstantConditions 473 | if (mRemoveAnimations.remove(item) && DEBUG) { 474 | throw new IllegalStateException("after animation is cancelled, item should not be in " 475 | + "mRemoveAnimations list"); 476 | } 477 | 478 | //noinspection PointlessBooleanExpression,ConstantConditions 479 | if (mAddAnimations.remove(item) && DEBUG) { 480 | throw new IllegalStateException("after animation is cancelled, item should not be in " 481 | + "mAddAnimations list"); 482 | } 483 | 484 | //noinspection PointlessBooleanExpression,ConstantConditions 485 | if (mChangeAnimations.remove(item) && DEBUG) { 486 | throw new IllegalStateException("after animation is cancelled, item should not be in " 487 | + "mChangeAnimations list"); 488 | } 489 | 490 | //noinspection PointlessBooleanExpression,ConstantConditions 491 | if (mMoveAnimations.remove(item) && DEBUG) { 492 | throw new IllegalStateException("after animation is cancelled, item should not be in " 493 | + "mMoveAnimations list"); 494 | } 495 | dispatchFinishedWhenDone(); 496 | } 497 | 498 | private void resetAnimation(RecyclerView.ViewHolder holder) { 499 | AnimatorCompatHelper.clearInterpolator(holder.itemView); 500 | endAnimation(holder); 501 | } 502 | 503 | @Override 504 | public boolean isRunning() { 505 | return (!mPendingAdditions.isEmpty() || 506 | !mPendingChanges.isEmpty() || 507 | !mPendingMoves.isEmpty() || 508 | !mPendingRemovals.isEmpty() || 509 | !mMoveAnimations.isEmpty() || 510 | !mRemoveAnimations.isEmpty() || 511 | !mAddAnimations.isEmpty() || 512 | !mChangeAnimations.isEmpty() || 513 | !mMovesList.isEmpty() || 514 | !mAdditionsList.isEmpty() || 515 | !mChangesList.isEmpty()); 516 | } 517 | 518 | /** 519 | * Check the state of currently pending and running animations. If there are none 520 | * pending/running, call {@link #dispatchAnimationsFinished()} to notify any 521 | * listeners. 522 | */ 523 | private void dispatchFinishedWhenDone() { 524 | if (!isRunning()) { 525 | dispatchAnimationsFinished(); 526 | } 527 | } 528 | 529 | @Override 530 | public void endAnimations() { 531 | int count = mPendingMoves.size(); 532 | for (int i = count - 1; i >= 0; i--) { 533 | MoveInfo item = mPendingMoves.get(i); 534 | View view = item.holder.itemView; 535 | ViewCompat.setTranslationY(view, 0); 536 | ViewCompat.setTranslationX(view, 0); 537 | dispatchMoveFinished(item.holder); 538 | mPendingMoves.remove(i); 539 | } 540 | count = mPendingRemovals.size(); 541 | for (int i = count - 1; i >= 0; i--) { 542 | RecyclerView.ViewHolder item = mPendingRemovals.get(i); 543 | dispatchRemoveFinished(item); 544 | mPendingRemovals.remove(i); 545 | } 546 | count = mPendingAdditions.size(); 547 | for (int i = count - 1; i >= 0; i--) { 548 | RecyclerView.ViewHolder item = mPendingAdditions.get(i); 549 | View view = item.itemView; 550 | ViewCompat.setAlpha(view, 1); 551 | dispatchAddFinished(item); 552 | mPendingAdditions.remove(i); 553 | } 554 | count = mPendingChanges.size(); 555 | for (int i = count - 1; i >= 0; i--) { 556 | endChangeAnimationIfNecessary(mPendingChanges.get(i)); 557 | } 558 | mPendingChanges.clear(); 559 | if (!isRunning()) { 560 | return; 561 | } 562 | 563 | int listCount = mMovesList.size(); 564 | for (int i = listCount - 1; i >= 0; i--) { 565 | ArrayList moves = mMovesList.get(i); 566 | count = moves.size(); 567 | for (int j = count - 1; j >= 0; j--) { 568 | MoveInfo moveInfo = moves.get(j); 569 | RecyclerView.ViewHolder item = moveInfo.holder; 570 | View view = item.itemView; 571 | ViewCompat.setTranslationY(view, 0); 572 | ViewCompat.setTranslationX(view, 0); 573 | dispatchMoveFinished(moveInfo.holder); 574 | moves.remove(j); 575 | if (moves.isEmpty()) { 576 | mMovesList.remove(moves); 577 | } 578 | } 579 | } 580 | listCount = mAdditionsList.size(); 581 | for (int i = listCount - 1; i >= 0; i--) { 582 | ArrayList additions = mAdditionsList.get(i); 583 | count = additions.size(); 584 | for (int j = count - 1; j >= 0; j--) { 585 | RecyclerView.ViewHolder item = additions.get(j); 586 | View view = item.itemView; 587 | ViewCompat.setAlpha(view, 1); 588 | dispatchAddFinished(item); 589 | additions.remove(j); 590 | if (additions.isEmpty()) { 591 | mAdditionsList.remove(additions); 592 | } 593 | } 594 | } 595 | listCount = mChangesList.size(); 596 | for (int i = listCount - 1; i >= 0; i--) { 597 | ArrayList changes = mChangesList.get(i); 598 | count = changes.size(); 599 | for (int j = count - 1; j >= 0; j--) { 600 | endChangeAnimationIfNecessary(changes.get(j)); 601 | if (changes.isEmpty()) { 602 | mChangesList.remove(changes); 603 | } 604 | } 605 | } 606 | 607 | cancelAll(mRemoveAnimations); 608 | cancelAll(mMoveAnimations); 609 | cancelAll(mAddAnimations); 610 | cancelAll(mChangeAnimations); 611 | 612 | dispatchAnimationsFinished(); 613 | } 614 | 615 | void cancelAll(List viewHolders) { 616 | for (int i = viewHolders.size() - 1; i >= 0; i--) { 617 | ViewCompat.animate(viewHolders.get(i).itemView).cancel(); 618 | } 619 | } 620 | 621 | /** 622 | * {@inheritDoc} 623 | *

624 | * If the payload list is not empty, DefaultItemAnimator returns true. 625 | * When this is the case: 626 | *

636 | */ 637 | @Override 638 | public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, 639 | @NonNull List payloads) { 640 | return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); 641 | } 642 | 643 | private static class VpaListenerAdapter implements ViewPropertyAnimatorListener { 644 | @Override 645 | public void onAnimationStart(View view) {} 646 | 647 | @Override 648 | public void onAnimationEnd(View view) {} 649 | 650 | @Override 651 | public void onAnimationCancel(View view) {} 652 | } 653 | } 654 | -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/ClassifyView.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.AnimatorSet; 6 | import android.animation.ObjectAnimator; 7 | import android.animation.PropertyValuesHolder; 8 | import android.annotation.TargetApi; 9 | import android.content.ClipData; 10 | import android.content.Context; 11 | import android.content.res.TypedArray; 12 | import android.database.Cursor; 13 | import android.graphics.Color; 14 | import android.graphics.drawable.Drawable; 15 | import android.os.Build; 16 | import android.os.SystemClock; 17 | import android.support.annotation.NonNull; 18 | import android.support.v4.view.GestureDetectorCompat; 19 | import android.support.v4.view.MotionEventCompat; 20 | import android.support.v4.view.ViewCompat; 21 | import android.support.v7.widget.GridLayoutManager; 22 | import android.support.v7.widget.LinearLayoutManager; 23 | import android.support.v7.widget.RecyclerView; 24 | import android.util.AttributeSet; 25 | import android.view.DragEvent; 26 | import android.view.GestureDetector; 27 | import android.view.Gravity; 28 | import android.view.MotionEvent; 29 | import android.view.VelocityTracker; 30 | import android.view.View; 31 | import android.view.ViewGroup; 32 | import android.view.animation.AccelerateDecelerateInterpolator; 33 | import android.view.animation.Interpolator; 34 | import android.widget.FrameLayout; 35 | 36 | import com.anarchy.classify.adapter.BaseMainAdapter; 37 | import com.anarchy.classify.adapter.BaseSubAdapter; 38 | import com.anarchy.classify.adapter.MainRecyclerViewCallBack; 39 | import com.anarchy.classify.adapter.SubAdapterReference; 40 | import com.anarchy.classify.adapter.SubRecyclerViewCallBack; 41 | import com.anarchy.classify.simple.BaseSimpleAdapter; 42 | import com.anarchy.classify.util.L; 43 | 44 | import java.util.ArrayList; 45 | import java.util.List; 46 | 47 | /** 48 | *

49 | * Date: 16/6/1 14:16 50 | * Author: zhendong.wu@shoufuyou.com 51 | *

52 | */ 53 | public class ClassifyView extends FrameLayout { 54 | /** 55 | * 不做处理的状态 56 | */ 57 | public static final int STATE_NONE = 0; 58 | /** 59 | * 当前状态为 可移动 60 | */ 61 | public static final int STATE_MOVE = 1; 62 | /** 63 | * 当前状态为 可合并 64 | */ 65 | public static final int STATE_MERGE = 2; 66 | 67 | 68 | private static final int ACTIVE_POINTER_ID_NONE = -1; 69 | private static final String DESCRIPTION = "Long press"; 70 | private static final String MAIN = "main"; 71 | private static final String SUB = "sub"; 72 | 73 | 74 | /** 75 | * 放置主要RecyclerView的容器 76 | */ 77 | private ViewGroup mMainContainer; 78 | /** 79 | * 放置次级RecyclerView的容器 80 | */ 81 | private ViewGroup mSubContainer; 82 | /** 83 | * 被拖动的View 84 | */ 85 | private View mDragView; 86 | 87 | 88 | private View mMainShadowView; 89 | private RecyclerView mMainRecyclerView; 90 | private RecyclerView mSubRecyclerView; 91 | 92 | private int mMainSpanCount; 93 | private int mSubSpanCount; 94 | private GestureDetectorCompat mMainGestureDetector; 95 | private GestureDetectorCompat mSubGestureDetector; 96 | 97 | private RecyclerView.OnItemTouchListener mMainItemTouchListener; 98 | private RecyclerView.OnItemTouchListener mSubItemTouchListener; 99 | 100 | private MainRecyclerViewCallBack mMainCallBack; 101 | private SubRecyclerViewCallBack mSubCallBack; 102 | 103 | private float mSubRatio; 104 | private int mMainActivePointerId = ACTIVE_POINTER_ID_NONE; 105 | private int mSubActivePointerId = ACTIVE_POINTER_ID_NONE; 106 | private int mShadowColor; 107 | private int mAnimationDuration; 108 | 109 | 110 | private int mSelectedStartX; 111 | private int mSelectedStartY; 112 | private float mInitialTouchX; 113 | private float mInitialTouchY; 114 | private float mDx; 115 | private float mDy; 116 | private View mSelected; 117 | private int mSelectedPosition; 118 | /** 119 | * 触发滑动距离 120 | */ 121 | private int mEdgeWidth; 122 | 123 | private boolean inMainRegion; 124 | private boolean inSubRegion; 125 | 126 | private VelocityTracker mVelocityTracker; 127 | 128 | public ClassifyView(Context context) { 129 | super(context); 130 | init(context, null, 0); 131 | } 132 | 133 | public ClassifyView(Context context, AttributeSet attrs) { 134 | super(context, attrs); 135 | init(context, attrs, 0); 136 | } 137 | 138 | public ClassifyView(Context context, AttributeSet attrs, int defStyleAttr) { 139 | super(context, attrs, defStyleAttr); 140 | init(context, attrs, defStyleAttr); 141 | } 142 | 143 | 144 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 145 | public ClassifyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 146 | super(context, attrs, defStyleAttr, defStyleRes); 147 | init(context, attrs, defStyleAttr); 148 | } 149 | 150 | /** 151 | * 初始化容器 152 | */ 153 | private void init(Context context, AttributeSet attrs, int defStyleAttr) { 154 | mMainContainer = new FrameLayout(context); 155 | mSubContainer = new FrameLayout(context); 156 | mMainContainer.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 157 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ClassifyView, defStyleAttr, R.style.DefaultStyle); 158 | mSubRatio = a.getFraction(R.styleable.ClassifyView_SubRatio, 1, 1, 0.7f); 159 | mMainSpanCount = a.getInt(R.styleable.ClassifyView_MainSpanCount, 3); 160 | mSubSpanCount = a.getInt(R.styleable.ClassifyView_SubSpanCount, 3); 161 | mShadowColor = a.getColor(R.styleable.ClassifyView_ShadowColor, 0x83585858); 162 | mAnimationDuration = a.getInt(R.styleable.ClassifyView_AnimationDuration, 200); 163 | mEdgeWidth = a.getDimensionPixelSize(R.styleable.ClassifyView_EdgeWidth, 15); 164 | a.recycle(); 165 | mMainRecyclerView = getMain(context, attrs); 166 | mSubRecyclerView = getSub(context, attrs); 167 | mMainContainer.addView(mMainRecyclerView); 168 | mMainShadowView = new View(context); 169 | mMainShadowView.setBackgroundColor(mShadowColor); 170 | mMainShadowView.setVisibility(View.GONE); 171 | mMainShadowView.setOnClickListener(new OnClickListener() { 172 | @Override 173 | public void onClick(View v) { 174 | if (mHideSubAnim != null && mHideSubAnim.isRunning()) return; 175 | hideSubContainer(); 176 | } 177 | }); 178 | mMainShadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 179 | mMainContainer.addView(mMainShadowView); 180 | mSubContainer.addView(mSubRecyclerView); 181 | mSubContainer.setBackgroundColor(Color.CYAN); 182 | addViewInLayout(mMainContainer, 0, mMainContainer.getLayoutParams()); 183 | mDragView = new View(context); 184 | mDragView.setVisibility(GONE); 185 | addViewInLayout(mDragView, -1, generateDefaultLayoutParams()); 186 | setUpTouchListener(context); 187 | } 188 | 189 | protected 190 | @NonNull 191 | RecyclerView getMain(Context context, AttributeSet parentAttrs) { 192 | RecyclerView recyclerView = new RecyclerView(context); 193 | recyclerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 194 | recyclerView.setLayoutManager(new GridLayoutManager(context, mMainSpanCount)); 195 | recyclerView.setItemAnimator(new ClassifyItemAnimator()); 196 | return recyclerView; 197 | } 198 | 199 | 200 | protected 201 | @NonNull 202 | RecyclerView getSub(Context context, AttributeSet parentAttrs) { 203 | RecyclerView recyclerView = new RecyclerView(context); 204 | recyclerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 205 | recyclerView.setLayoutManager(new GridLayoutManager(context, mSubSpanCount)); 206 | recyclerView.setItemAnimator(new ClassifyItemAnimator()); 207 | return recyclerView; 208 | } 209 | 210 | 211 | public RecyclerView getMainRecyclerView() { 212 | return mMainRecyclerView; 213 | } 214 | 215 | public RecyclerView getSubRecyclerView() { 216 | return mSubRecyclerView; 217 | } 218 | 219 | 220 | private View findChildView(RecyclerView recyclerView, MotionEvent event) { 221 | // first check elevated views, if none, then call RV 222 | final float x = event.getX(); 223 | final float y = event.getY(); 224 | return recyclerView.findChildViewUnder(x, y); 225 | } 226 | 227 | /** 228 | * 设置adapter 229 | * 230 | * @param mainAdapter 231 | * @param subAdapter 232 | */ 233 | public void setAdapter(BaseMainAdapter mainAdapter, BaseSubAdapter subAdapter) { 234 | mMainRecyclerView.setAdapter(mainAdapter); 235 | mMainRecyclerView.addOnItemTouchListener(mMainItemTouchListener); 236 | mMainCallBack = mainAdapter; 237 | mSubRecyclerView.setAdapter(subAdapter); 238 | mSubRecyclerView.addOnItemTouchListener(mSubItemTouchListener); 239 | mSubCallBack = subAdapter; 240 | mMainRecyclerView.setOnDragListener(new MainDragListener()); 241 | mSubRecyclerView.setOnDragListener(new SubDragListener()); 242 | } 243 | 244 | /** 245 | * @param baseSimpleAdapter 246 | */ 247 | public void setAdapter(BaseSimpleAdapter baseSimpleAdapter) { 248 | setAdapter(baseSimpleAdapter.getMainAdapter(), baseSimpleAdapter.getSubAdapter()); 249 | } 250 | 251 | public RecyclerView.LayoutManager getMainLayoutManager() { 252 | return mMainRecyclerView.getLayoutManager(); 253 | } 254 | 255 | public RecyclerView.LayoutManager getSubLayoutManager() { 256 | return mSubRecyclerView.getLayoutManager(); 257 | } 258 | 259 | /** 260 | * 初始化 触摸事件监听 261 | * 262 | * @param context 263 | */ 264 | private void setUpTouchListener(Context context) { 265 | mMainGestureDetector = new GestureDetectorCompat(context, new GestureDetector.SimpleOnGestureListener() { 266 | @Override 267 | public boolean onDown(MotionEvent e) { 268 | return true; 269 | } 270 | 271 | @Override 272 | public boolean onSingleTapConfirmed(MotionEvent e) { 273 | View pressedView = findChildView(mMainRecyclerView, e); 274 | if (pressedView == null) return false; 275 | int position = mMainRecyclerView.getChildAdapterPosition(pressedView); 276 | List list = mMainCallBack.explodeItem(position, pressedView); 277 | if (list == null || list.size() < 2) { 278 | mMainCallBack.onItemClick(position, pressedView); 279 | return true; 280 | } else { 281 | mSubCallBack.initData(position, list); 282 | if (ViewCompat.isAttachedToWindow(mSubContainer)) { 283 | //取消之前进行的动画 284 | if (mShowSubAnim != null && mShowSubAnim.isRunning()) { 285 | mShowSubAnim.cancel(); 286 | } 287 | //确保次级窗口在屏幕外 288 | resetSubContainerPlace(); 289 | showSubContainer(); 290 | } else { 291 | final int height = (int) (getHeight() * mSubRatio); 292 | LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height); 293 | params.gravity = Gravity.BOTTOM; 294 | mSubContainer.setLayoutParams(params); 295 | addView(mSubContainer); 296 | ViewCompat.postOnAnimation(mSubContainer, new Runnable() { 297 | @Override 298 | public void run() { 299 | mSubContainer.setTranslationY(height); 300 | showSubContainer(); 301 | } 302 | }); 303 | } 304 | return true; 305 | } 306 | 307 | } 308 | 309 | @Override 310 | public void onLongPress(MotionEvent e) { 311 | View pressedView = findChildView(mMainRecyclerView, e); 312 | if (pressedView == null) return; 313 | L.d("Main recycler view on long press: x: %1$s + y: %2$s", e.getX(), e.getY()); 314 | int position = mMainRecyclerView.getChildAdapterPosition(pressedView); 315 | 316 | int pointerId = MotionEventCompat.getPointerId(e, 0); 317 | if (pointerId == mMainActivePointerId) { 318 | if (mMainCallBack.canDragOnLongPress(position, pressedView)) { 319 | mSelectedPosition = position; 320 | mSelectedStartX = pressedView.getLeft(); 321 | mSelectedStartY = pressedView.getTop(); 322 | mDx = mDy = 0f; 323 | int index = MotionEventCompat.findPointerIndex(e, mMainActivePointerId); 324 | mInitialTouchX = MotionEventCompat.getX(e, index); 325 | mInitialTouchY = MotionEventCompat.getY(e, index); 326 | L.d("handle event on long press:X: %1$s , Y: %2$s ", mInitialTouchX, mInitialTouchY); 327 | inMainRegion = true; 328 | mSelected = pressedView; 329 | pressedView.startDrag(ClipData.newPlainText(DESCRIPTION, MAIN), 330 | new ClassifyDragShadowBuilder(pressedView), mSelected, 0); 331 | } 332 | } 333 | } 334 | }); 335 | mMainItemTouchListener = new RecyclerView.OnItemTouchListener() { 336 | 337 | @Override 338 | public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { 339 | mMainGestureDetector.onTouchEvent(e); 340 | int action = MotionEventCompat.getActionMasked(e); 341 | switch (action) { 342 | case MotionEvent.ACTION_DOWN: 343 | mMainActivePointerId = MotionEventCompat.getPointerId(e, 0); 344 | break; 345 | case MotionEvent.ACTION_CANCEL: 346 | case MotionEvent.ACTION_UP: 347 | mMainActivePointerId = ACTIVE_POINTER_ID_NONE; 348 | inMergeState = false; 349 | break; 350 | } 351 | return false; 352 | } 353 | 354 | @Override 355 | public void onTouchEvent(RecyclerView rv, MotionEvent e) { 356 | mMainGestureDetector.onTouchEvent(e); 357 | int action = MotionEventCompat.getActionMasked(e); 358 | switch (action) { 359 | case MotionEvent.ACTION_MOVE: 360 | break; 361 | case MotionEvent.ACTION_CANCEL: 362 | case MotionEvent.ACTION_UP: 363 | mMainActivePointerId = ACTIVE_POINTER_ID_NONE; 364 | break; 365 | case MotionEvent.ACTION_POINTER_UP: 366 | int pointerIndex = MotionEventCompat.getActionIndex(e); 367 | int pointerId = MotionEventCompat.getPointerId(e, pointerIndex); 368 | if (pointerId == mSubActivePointerId) { 369 | int newPointerId = pointerIndex == 0 ? 1 : 0; 370 | mMainActivePointerId = MotionEventCompat.getPointerId(e, newPointerId); 371 | } 372 | break; 373 | } 374 | } 375 | 376 | @Override 377 | public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { 378 | 379 | } 380 | }; 381 | mSubGestureDetector = new GestureDetectorCompat(context, new GestureDetector.SimpleOnGestureListener() { 382 | @Override 383 | public boolean onDown(MotionEvent e) { 384 | return true; 385 | } 386 | 387 | @Override 388 | public boolean onSingleTapConfirmed(MotionEvent e) { 389 | View pressedView = findChildView(mSubRecyclerView, e); 390 | if (pressedView == null) return false; 391 | int position = mSubRecyclerView.getChildAdapterPosition(pressedView); 392 | mSubCallBack.onItemClick(position, pressedView); 393 | return true; 394 | } 395 | 396 | @Override 397 | public void onLongPress(MotionEvent e) { 398 | View pressedView = findChildView(mSubRecyclerView, e); 399 | if (pressedView == null) return; 400 | L.d("Sub recycler view on long press: x: %1$s + y: %2$s", e.getX(), e.getY()); 401 | int position = mSubRecyclerView.getChildAdapterPosition(pressedView); 402 | int pointerId = MotionEventCompat.getPointerId(e, 0); 403 | if (pointerId == mSubActivePointerId) { 404 | if (mSubCallBack.canDragOnLongPress(position, pressedView)) { 405 | mSelectedPosition = position; 406 | mSelectedStartX = pressedView.getLeft(); 407 | mSelectedStartY = pressedView.getTop(); 408 | mDx = mDy = 0f; 409 | int index = MotionEventCompat.findPointerIndex(e, mSubActivePointerId); 410 | mInitialTouchX = MotionEventCompat.getX(e, index); 411 | mInitialTouchY = MotionEventCompat.getY(e, index); 412 | inSubRegion = true; 413 | mSelected = pressedView; 414 | pressedView.startDrag(ClipData.newPlainText( 415 | DESCRIPTION, SUB), 416 | getShadowBuilder(pressedView), mSelected, 0); 417 | } 418 | } 419 | } 420 | }); 421 | mSubItemTouchListener = new RecyclerView.OnItemTouchListener() { 422 | @Override 423 | public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { 424 | mSubGestureDetector.onTouchEvent(e); 425 | int action = MotionEventCompat.getActionMasked(e); 426 | switch (action) { 427 | case MotionEvent.ACTION_DOWN: 428 | mSubActivePointerId = MotionEventCompat.getPointerId(e, 0); 429 | mInitialTouchX = e.getX(); 430 | mInitialTouchY = e.getY(); 431 | break; 432 | case MotionEvent.ACTION_CANCEL: 433 | case MotionEvent.ACTION_UP: 434 | mSubActivePointerId = ACTIVE_POINTER_ID_NONE; 435 | break; 436 | } 437 | return false; 438 | } 439 | 440 | @Override 441 | public void onTouchEvent(RecyclerView rv, MotionEvent e) { 442 | mSubGestureDetector.onTouchEvent(e); 443 | int action = MotionEventCompat.getActionMasked(e); 444 | switch (action) { 445 | case MotionEvent.ACTION_MOVE: 446 | break; 447 | case MotionEvent.ACTION_CANCEL: 448 | case MotionEvent.ACTION_UP: 449 | mSubActivePointerId = ACTIVE_POINTER_ID_NONE; 450 | break; 451 | case MotionEvent.ACTION_POINTER_UP: 452 | int pointerIndex = MotionEventCompat.getActionIndex(e); 453 | int pointerId = MotionEventCompat.getPointerId(e, pointerIndex); 454 | if (pointerId == mSubActivePointerId) { 455 | int newPointerId = pointerIndex == 0 ? 1 : 0; 456 | mSubActivePointerId = MotionEventCompat.getPointerId(e, newPointerId); 457 | } 458 | break; 459 | 460 | } 461 | } 462 | 463 | @Override 464 | public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { 465 | 466 | } 467 | }; 468 | } 469 | 470 | private void resetSubContainerPlace() { 471 | int height = mSubContainer.getHeight(); 472 | mSubContainer.setTranslationY(height); 473 | } 474 | 475 | private AnimatorSet mShowSubAnim; 476 | private AnimatorSet mHideSubAnim; 477 | 478 | /** 479 | * 显示次级窗口 480 | */ 481 | private void showSubContainer() { 482 | if (mShowSubAnim != null && mShowSubAnim.isRunning()) return; 483 | mShowSubAnim = new AnimatorSet(); 484 | ObjectAnimator subAnim = ObjectAnimator.ofFloat(mSubContainer, "translationY", 0); 485 | ObjectAnimator shadowAnim = ObjectAnimator.ofFloat(mMainShadowView, "alpha", 0f, 1f); 486 | mShowSubAnim.setDuration(mAnimationDuration); 487 | mShowSubAnim.setInterpolator(new AccelerateDecelerateInterpolator()); 488 | mShowSubAnim.addListener(new AnimatorListenerAdapter() { 489 | @Override 490 | public void onAnimationCancel(Animator animation) { 491 | } 492 | 493 | @Override 494 | public void onAnimationEnd(Animator animation) { 495 | } 496 | 497 | @Override 498 | public void onAnimationStart(Animator animation) { 499 | mMainShadowView.setVisibility(VISIBLE); 500 | } 501 | }); 502 | mShowSubAnim.play(subAnim).with(shadowAnim); 503 | mShowSubAnim.start(); 504 | } 505 | 506 | /** 507 | * 隐藏次级窗口 508 | */ 509 | private void hideSubContainer() { 510 | if (mHideSubAnim != null && mHideSubAnim.isRunning()) return; 511 | int height = mSubContainer.getHeight(); 512 | mHideSubAnim = new AnimatorSet(); 513 | ObjectAnimator subAnim = ObjectAnimator.ofFloat(mSubContainer, "translationY", height); 514 | ObjectAnimator shadowAnim = ObjectAnimator.ofFloat(mMainShadowView, "alpha", 1f, 0f); 515 | mHideSubAnim.setDuration(mAnimationDuration); 516 | mHideSubAnim.setInterpolator(new AccelerateDecelerateInterpolator()); 517 | mHideSubAnim.addListener(new AnimatorListenerAdapter() { 518 | @Override 519 | public void onAnimationCancel(Animator animation) { 520 | mMainShadowView.setVisibility(GONE); 521 | } 522 | 523 | @Override 524 | public void onAnimationEnd(Animator animation) { 525 | mMainShadowView.setVisibility(GONE); 526 | } 527 | 528 | @Override 529 | public void onAnimationStart(Animator animation) { 530 | mMainShadowView.setVisibility(VISIBLE); 531 | } 532 | }); 533 | mHideSubAnim.playTogether(subAnim, shadowAnim); 534 | mHideSubAnim.start(); 535 | 536 | } 537 | 538 | private boolean mergeSuccess = false; 539 | 540 | class MainDragListener implements View.OnDragListener { 541 | @Override 542 | public boolean onDrag(View v, DragEvent event) { 543 | if (mSelected == null) return false; 544 | int action = event.getAction(); 545 | int width = mSelected.getWidth(); 546 | int height = mSelected.getHeight(); 547 | float x = event.getX(); 548 | float y = event.getY(); 549 | float centerX = x - width / 2; 550 | float centerY = y - height / 2; 551 | switch (action) { 552 | case DragEvent.ACTION_DRAG_STARTED: 553 | if (inMainRegion) { 554 | obtainVelocityTracker(); 555 | restoreDragView(); 556 | mDragView.setBackgroundDrawable(getDragDrawable(mSelected)); 557 | mDragView.setVisibility(VISIBLE); 558 | mMainCallBack.setDragPosition(mSelectedPosition); 559 | mDragView.setX(mInitialTouchX - width / 2); 560 | mDragView.setY(mInitialTouchY - height / 2); 561 | mDragView.bringToFront(); 562 | mElevationHelper.floatView(mMainRecyclerView, mDragView); 563 | } 564 | break; 565 | case DragEvent.ACTION_DRAG_LOCATION: 566 | mVelocityTracker.addMovement(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), 567 | MotionEvent.ACTION_MOVE, x, y, 0)); 568 | mDragView.setX(centerX); 569 | mDragView.setY(centerY); 570 | mDx = x - mInitialTouchX; 571 | mDy = y - mInitialTouchY; 572 | moveIfNecessary(mSelected); 573 | removeCallbacks(mScrollRunnable); 574 | mScrollRunnable.run(); 575 | invalidate(); 576 | break; 577 | case DragEvent.ACTION_DRAG_ENDED: 578 | if (mergeSuccess) { 579 | mergeSuccess = false; 580 | break; 581 | } 582 | if (inMainRegion) { 583 | doRecoverAnimation(); 584 | } 585 | releaseVelocityTracker(); 586 | break; 587 | case DragEvent.ACTION_DRAG_EXITED: 588 | break; 589 | case DragEvent.ACTION_DROP: 590 | if (inMergeState) { 591 | inMergeState = false; 592 | if (mLastMergeStartPosition == -1) break; 593 | ChangeInfo changeInfo = mMainCallBack.onPrepareMerge(mMainRecyclerView, mSelectedPosition, mLastMergeStartPosition); 594 | RecyclerView.ViewHolder target = mMainRecyclerView.findViewHolderForAdapterPosition(mLastMergeStartPosition); 595 | if (target == null || changeInfo == null || target.itemView == mSelected) { 596 | mergeSuccess = false; 597 | break; 598 | } 599 | float scaleX = ((float) changeInfo.itemWidth) / ((float) (mSelected.getWidth() - changeInfo.paddingLeft - changeInfo.paddingRight - 2 * changeInfo.outlinePadding)); 600 | float scaleY = ((float) changeInfo.itemHeight) / ((float) (mSelected.getHeight() - changeInfo.paddingTop - changeInfo.paddingBottom - 2 * changeInfo.outlinePadding)); 601 | int targetX = (int) (target.itemView.getLeft() + changeInfo.left + changeInfo.paddingLeft - (changeInfo.paddingLeft + changeInfo.outlinePadding) * scaleX); 602 | int targetY = (int) (target.itemView.getTop() + changeInfo.top + changeInfo.paddingTop - (changeInfo.paddingTop + changeInfo.outlinePadding) * scaleY); 603 | mDragView.setPivotX(0); 604 | mDragView.setPivotY(0); 605 | L.d("targetX:%1$s,targetY:%2$s,scaleX:%3$s,scaleY:%4$s", targetX, targetY, scaleX, scaleY); 606 | mDragView.animate().x(targetX).y(targetY).scaleX(scaleX).scaleY(scaleY).setListener(mMergeAnimListener).setDuration(mAnimationDuration).start(); 607 | mergeSuccess = true; 608 | } 609 | break; 610 | } 611 | return true; 612 | } 613 | } 614 | 615 | private AnimatorListenerAdapter mMergeAnimListener = new AnimatorListenerAdapter() { 616 | @Override 617 | public void onAnimationStart(Animator animation) { 618 | mMainCallBack.onStartMergeAnimation(mMainRecyclerView, mSelectedPosition, mLastMergeStartPosition, mAnimationDuration); 619 | } 620 | 621 | @Override 622 | public void onAnimationEnd(Animator animation) { 623 | mMainCallBack.onMerged(mMainRecyclerView, mSelectedPosition, mLastMergeStartPosition); 624 | restoreToInitial(); 625 | } 626 | 627 | @Override 628 | public void onAnimationCancel(Animator animation) { 629 | mMainCallBack.onMerged(mMainRecyclerView, mSelectedPosition, mLastMergeStartPosition); 630 | restoreToInitial(); 631 | } 632 | }; 633 | 634 | protected Drawable getDragDrawable(View view) { 635 | return new DragDrawable(view); 636 | } 637 | 638 | class SubDragListener implements View.OnDragListener { 639 | @Override 640 | public boolean onDrag(View v, DragEvent event) { 641 | if (mSelected == null) return false; 642 | int action = event.getAction(); 643 | int width = mSelected.getWidth(); 644 | int height = mSelected.getHeight(); 645 | float x = event.getX(); 646 | float y = event.getY(); 647 | float centerX = x - width / 2; 648 | float centerY = y - height / 2; 649 | float marginTop = getHeight() - mSubContainer.getHeight(); 650 | switch (action) { 651 | case DragEvent.ACTION_DRAG_STARTED: 652 | if (inSubRegion) { 653 | obtainVelocityTracker(); 654 | restoreDragView(); 655 | mDragView.setBackgroundDrawable(getDragDrawable(mSelected)); 656 | mDragView.setVisibility(VISIBLE); 657 | mSubCallBack.setDragPosition(mSelectedPosition); 658 | mDragView.setX(mInitialTouchX - width / 2); 659 | mDragView.setY(mInitialTouchY - height / 2 + marginTop); 660 | mDragView.bringToFront(); 661 | mElevationHelper.floatView(mSubRecyclerView, mDragView); 662 | } 663 | break; 664 | case DragEvent.ACTION_DRAG_LOCATION: 665 | mVelocityTracker.addMovement(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), 666 | MotionEvent.ACTION_MOVE, x, y, 0)); 667 | mDragView.setX(centerX); 668 | mDragView.setY(centerY + marginTop); 669 | mDx = x - mInitialTouchX; 670 | mDy = y - mInitialTouchY; 671 | moveIfNecessary(mSelected); 672 | removeCallbacks(mScrollRunnable); 673 | mScrollRunnable.run(); 674 | invalidate(); 675 | break; 676 | case DragEvent.ACTION_DRAG_ENDED: 677 | if (inSubRegion) { 678 | doRecoverAnimation(); 679 | } 680 | releaseVelocityTracker(); 681 | break; 682 | case DragEvent.ACTION_DRAG_EXITED: 683 | if (mSubCallBack.canDragOut(mSelectedPosition)) { 684 | inSubRegion = false; 685 | inMainRegion = true; 686 | hideSubContainer(); 687 | mSelectedPosition = mMainCallBack.onLeaveSubRegion(mSelectedPosition, new SubAdapterReference(mSubCallBack)); 688 | mMainCallBack.setDragPosition(mSelectedPosition); 689 | mSubCallBack.setDragPosition(-1); 690 | } 691 | break; 692 | case DragEvent.ACTION_DROP: 693 | break; 694 | } 695 | return true; 696 | } 697 | } 698 | 699 | /** 700 | * 做恢复到之前状态的动画 701 | */ 702 | private void doRecoverAnimation() { 703 | Animator recoverAnimator = null; 704 | if (inSubRegion) { 705 | RecyclerView.ViewHolder holder = mSubRecyclerView.findViewHolderForAdapterPosition(mSelectedPosition); 706 | if (holder == null) { 707 | PropertyValuesHolder yOffset = PropertyValuesHolder.ofFloat("y", getHeight() + mSelected.getHeight()); 708 | recoverAnimator = ObjectAnimator.ofPropertyValuesHolder(mDragView, yOffset); 709 | } else { 710 | PropertyValuesHolder xOffset = PropertyValuesHolder.ofFloat("x", mSubContainer.getLeft() + holder.itemView.getLeft()); 711 | PropertyValuesHolder yOffset = PropertyValuesHolder.ofFloat("y", mSubContainer.getTop() + holder.itemView.getTop()); 712 | recoverAnimator = ObjectAnimator.ofPropertyValuesHolder(mDragView, xOffset, yOffset); 713 | } 714 | } 715 | 716 | if (inMainRegion) { 717 | RecyclerView.ViewHolder holder = mMainRecyclerView.findViewHolderForAdapterPosition(mSelectedPosition); 718 | if (holder == null) { 719 | PropertyValuesHolder yOffset = PropertyValuesHolder.ofFloat("y", getHeight() + mSelected.getHeight()); 720 | recoverAnimator = ObjectAnimator.ofPropertyValuesHolder(mDragView, yOffset); 721 | } else { 722 | PropertyValuesHolder xOffset = PropertyValuesHolder.ofFloat("x", holder.itemView.getLeft()); 723 | PropertyValuesHolder yOffset = PropertyValuesHolder.ofFloat("y", holder.itemView.getTop()); 724 | recoverAnimator = ObjectAnimator.ofPropertyValuesHolder(mDragView, xOffset, yOffset); 725 | } 726 | } 727 | if (recoverAnimator == null) return; 728 | recoverAnimator.setDuration(mAnimationDuration); 729 | recoverAnimator.setInterpolator(sDragScrollInterpolator); 730 | recoverAnimator.addListener(mRecoverAnimatorListener); 731 | recoverAnimator.start(); 732 | } 733 | 734 | private AnimatorListenerAdapter mRecoverAnimatorListener = new AnimatorListenerAdapter() { 735 | @Override 736 | public void onAnimationEnd(Animator animation) { 737 | restoreToInitial(); 738 | } 739 | }; 740 | 741 | private void restoreToInitial() { 742 | 743 | if (inSubRegion) { 744 | restoreDragView(); 745 | mSubCallBack.setDragPosition(-1); 746 | inSubRegion = false; 747 | } 748 | if (inMainRegion) { 749 | restoreDragView(); 750 | mMainCallBack.setDragPosition(-1); 751 | inMainRegion = false; 752 | } 753 | } 754 | 755 | private void restoreDragView() { 756 | mDragView.setVisibility(GONE); 757 | mDragView.setScaleX(1f); 758 | mDragView.setScaleY(1f); 759 | mDragView.setTranslationX(0f); 760 | mDragView.setTranslationX(0f); 761 | } 762 | 763 | /** 764 | * If user drags the view to the edge, trigger a scroll if necessary. 765 | */ 766 | private boolean scrollIfNecessary() { 767 | RecyclerView recyclerView = null; 768 | if (inMainRegion) { 769 | recyclerView = mMainRecyclerView; 770 | } 771 | if (inSubRegion) { 772 | recyclerView = mSubRecyclerView; 773 | } 774 | if (recyclerView == null) return false; 775 | final long now = System.currentTimeMillis(); 776 | final long scrollDuration = mDragScrollStartTimeInMs 777 | == Long.MIN_VALUE ? 0 : now - mDragScrollStartTimeInMs; 778 | RecyclerView.LayoutManager lm = recyclerView.getLayoutManager(); 779 | 780 | int scrollX = 0; 781 | int scrollY = 0; 782 | if (lm.canScrollHorizontally()) { 783 | int curX = (int) (mInitialTouchX + mDx - mSelected.getWidth() / 2); 784 | final int leftDiff = curX - mEdgeWidth - recyclerView.getPaddingLeft(); 785 | if (mDx < 0 && leftDiff < 0) { 786 | scrollX = leftDiff; 787 | } else if (mDx > 0) { 788 | final int rightDiff = 789 | curX + mSelected.getWidth() + mEdgeWidth - (recyclerView.getWidth() - recyclerView.getPaddingRight()); 790 | if (rightDiff > 0) { 791 | scrollX = rightDiff; 792 | } 793 | } 794 | } 795 | if (lm.canScrollVertically()) { 796 | int curY = (int) (mInitialTouchY + mDy - mSelected.getHeight() / 2); 797 | final int topDiff = curY - mEdgeWidth - recyclerView.getPaddingTop(); 798 | if (mDy < 0 && topDiff < 0) { 799 | scrollY = topDiff; 800 | } else if (mDy > 0) { 801 | final int bottomDiff = curY + mSelected.getHeight() + mEdgeWidth - 802 | (recyclerView.getHeight() - recyclerView.getPaddingBottom()); 803 | if (bottomDiff > 0) { 804 | scrollY = bottomDiff; 805 | } 806 | } 807 | } 808 | if (scrollX != 0) { 809 | scrollX = interpolateOutOfBoundsScroll(recyclerView, 810 | mSelected.getWidth(), scrollX, 811 | recyclerView.getWidth(), scrollDuration); 812 | } 813 | if (scrollY != 0) { 814 | scrollY = interpolateOutOfBoundsScroll(recyclerView, 815 | mSelected.getHeight(), scrollY, 816 | recyclerView.getHeight(), scrollDuration); 817 | } 818 | if (scrollX != 0 || scrollY != 0) { 819 | if (mDragScrollStartTimeInMs == Long.MIN_VALUE) { 820 | mDragScrollStartTimeInMs = now; 821 | } 822 | recyclerView.scrollBy(scrollX, scrollY); 823 | return true; 824 | } 825 | mDragScrollStartTimeInMs = Long.MIN_VALUE; 826 | return false; 827 | } 828 | 829 | 830 | private int interpolateOutOfBoundsScroll(RecyclerView recyclerView, 831 | int viewSize, int viewSizeOutOfBounds, 832 | int totalSize, long msSinceStartScroll) { 833 | final int maxScroll = getMaxDragScroll(recyclerView); 834 | final int absOutOfBounds = Math.abs(viewSizeOutOfBounds); 835 | final int direction = (int) Math.signum(viewSizeOutOfBounds); 836 | // might be negative if other direction 837 | float outOfBoundsRatio = Math.min(1f, 1f * absOutOfBounds / viewSize); 838 | final int cappedScroll = (int) (direction * maxScroll * 839 | sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio)); 840 | final float timeRatio; 841 | if (msSinceStartScroll > DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS) { 842 | timeRatio = 1f; 843 | } else { 844 | timeRatio = (float) msSinceStartScroll / DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS; 845 | } 846 | final int value = (int) (cappedScroll * sDragScrollInterpolator 847 | .getInterpolation(timeRatio)); 848 | if (value == 0) { 849 | return viewSizeOutOfBounds > 0 ? 1 : -1; 850 | } 851 | return value; 852 | } 853 | 854 | private static final long DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000; 855 | private static final Interpolator sDragViewScrollCapInterpolator = new Interpolator() { 856 | public float getInterpolation(float t) { 857 | t -= 1.0f; 858 | return t * t * t * t * t + 1.0f; 859 | } 860 | }; 861 | private static final Interpolator sDragScrollInterpolator = new Interpolator() { 862 | public float getInterpolation(float t) { 863 | return t * t * t * t * t; 864 | } 865 | }; 866 | private int mCachedMaxScrollSpeed = -1; 867 | 868 | private int getMaxDragScroll(RecyclerView recyclerView) { 869 | if (mCachedMaxScrollSpeed == -1) { 870 | mCachedMaxScrollSpeed = recyclerView.getResources().getDimensionPixelSize( 871 | R.dimen.item_touch_helper_max_drag_scroll_per_frame); 872 | } 873 | return mCachedMaxScrollSpeed; 874 | } 875 | 876 | /** 877 | * When user started to drag scroll. Reset when we don't scroll 878 | */ 879 | private long mDragScrollStartTimeInMs; 880 | 881 | 882 | /** 883 | * When user drags a view to the edge, we start scrolling the LayoutManager as long as View 884 | * is partially out of bounds. 885 | */ 886 | private final Runnable mScrollRunnable = new Runnable() { 887 | @Override 888 | public void run() { 889 | if (mSelected != null && scrollIfNecessary()) { 890 | if (mSelected != null) { //it might be lost during scrolling 891 | moveIfNecessary(mSelected); 892 | } 893 | removeCallbacks(mScrollRunnable); 894 | ViewCompat.postOnAnimation(ClassifyView.this, this); 895 | } 896 | } 897 | }; 898 | private boolean inMergeState = false; 899 | private int mLastMergeStartPosition = -1; 900 | 901 | private void moveIfNecessary(View view) { 902 | final int x = (int) (mSelectedStartX + mDx); 903 | final int y = (int) (mSelectedStartY + mDy); 904 | //如果移动范围在自身范围内 905 | if (Math.abs(y - view.getTop()) < view.getHeight() * 0.5f 906 | && Math.abs(x - view.getLeft()) 907 | < view.getWidth() * 0.5f) { 908 | return; 909 | } 910 | List swapTargets = findSwapTargets(view); 911 | if (swapTargets.size() == 0) return; 912 | View target = chooseTarget(view, swapTargets, x, y); 913 | if (target == null) return; 914 | if (inSubRegion) {//次级目录下 没有merge形式 915 | int targetPosition = mSubRecyclerView.getChildAdapterPosition(target); 916 | int state = mSubCallBack.getCurrentState(mSelected, target, x, y, mVelocityTracker, mSelectedPosition, 917 | targetPosition); 918 | if (state == STATE_MOVE) { 919 | if (mSubCallBack.onMove(mSelectedPosition, targetPosition)) { 920 | mSelectedPosition = targetPosition; 921 | RecyclerView.ViewHolder viewHolder = mSubRecyclerView.findViewHolderForAdapterPosition(mSelectedPosition); 922 | if (viewHolder != null) mSelected = viewHolder.itemView; 923 | mSubCallBack.setDragPosition(targetPosition); 924 | mSubCallBack.moved(mSelectedPosition, targetPosition); 925 | } 926 | } 927 | } 928 | if (inMainRegion) {//在主层级下 有merge状况 以及次级目录拖动到主层级的状况 929 | int targetPosition = mMainRecyclerView.getChildAdapterPosition(target); 930 | if(targetPosition != mLastMergeStartPosition) inMergeState = false; 931 | int state = mMainCallBack.getCurrentState(mSelected, target, x, y, mVelocityTracker, mSelectedPosition, 932 | targetPosition); 933 | boolean mergeState = state == STATE_MERGE; 934 | if (mergeState ^ inMergeState) { 935 | if (mergeState) { 936 | if (mMainCallBack.onMergeStart(mMainRecyclerView, mSelectedPosition, targetPosition)) { 937 | inMergeState = true; 938 | mLastMergeStartPosition = targetPosition; 939 | } 940 | } else { 941 | if (mLastMergeStartPosition != -1 && inMergeState) { 942 | mMainCallBack.onMergeCancel(mMainRecyclerView, mSelectedPosition, mLastMergeStartPosition); 943 | mLastMergeStartPosition = -1; 944 | inMergeState = false; 945 | } 946 | } 947 | } 948 | if (state == STATE_MOVE) { 949 | if (inMergeState && mLastMergeStartPosition != -1) { 950 | //makeSure trigger mergeCancel 951 | mMainCallBack.onMergeCancel(mMainRecyclerView, mSelectedPosition, mLastMergeStartPosition); 952 | mLastMergeStartPosition = -1; 953 | inMergeState = false; 954 | } 955 | if (mMainCallBack.onMove(mSelectedPosition, targetPosition)) { 956 | mSelectedPosition = targetPosition; 957 | RecyclerView.ViewHolder viewHolder = mMainRecyclerView.findViewHolderForAdapterPosition(mSelectedPosition); 958 | if (viewHolder != null) mSelected = viewHolder.itemView; 959 | mMainCallBack.setDragPosition(targetPosition); 960 | mMainCallBack.moved(mSelectedPosition, targetPosition); 961 | } 962 | } 963 | } 964 | } 965 | 966 | private List mSwapTargets; 967 | 968 | /** 969 | * 找到当前移动View 有覆盖的view 970 | * 971 | * @return 972 | */ 973 | private List findSwapTargets(View view) { 974 | if (mSwapTargets == null) { 975 | mSwapTargets = new ArrayList<>(); 976 | } else { 977 | mSwapTargets.clear(); 978 | } 979 | int left = Math.round(mSelectedStartX + mDx); 980 | int top = Math.round(mSelectedStartY + mDy); 981 | int right = left + view.getWidth(); 982 | int bottom = top + view.getHeight(); 983 | RecyclerView.LayoutManager lm = null; 984 | RecyclerView recyclerView = null; 985 | if (inMainRegion) { 986 | lm = getMainLayoutManager(); 987 | recyclerView = mMainRecyclerView; 988 | } 989 | if (inSubRegion) { 990 | lm = getSubLayoutManager(); 991 | recyclerView = mSubRecyclerView; 992 | } 993 | if (lm == null || recyclerView == null) return mSwapTargets; 994 | int childCount = lm.getChildCount(); 995 | for (int i = 0; i < childCount; i++) { 996 | View child = lm.getChildAt(i); 997 | if (child == view) { 998 | //本身 999 | continue; 1000 | } 1001 | if (child.getBottom() < top || child.getTop() > bottom || child.getLeft() > right || child.getRight() < left) { 1002 | continue;//没有覆盖到 1003 | } 1004 | int targetPosition = recyclerView.getChildAdapterPosition(child); 1005 | //检验目标位置是否能移动 1006 | if (inMainRegion) { 1007 | if (!mMainCallBack.canDropOVer(mSelectedPosition, targetPosition)) continue; 1008 | } 1009 | if (inSubRegion) { 1010 | if (!mSubCallBack.canDropOver(mSelectedPosition, targetPosition)) continue; 1011 | } 1012 | mSwapTargets.add(child); 1013 | } 1014 | return mSwapTargets; 1015 | } 1016 | 1017 | private void obtainVelocityTracker() { 1018 | if (mVelocityTracker != null) { 1019 | mVelocityTracker.recycle(); 1020 | } 1021 | mVelocityTracker = VelocityTracker.obtain(); 1022 | } 1023 | 1024 | private void releaseVelocityTracker() { 1025 | if (mVelocityTracker != null) { 1026 | mVelocityTracker.recycle(); 1027 | mVelocityTracker = null; 1028 | } 1029 | } 1030 | 1031 | /** 1032 | * 从候选项中找到最有优势的目标 1033 | * 1034 | * @param selected 1035 | * @param swapTargets 1036 | * @param curX 1037 | * @param curY 1038 | * @return 1039 | */ 1040 | protected View chooseTarget(View selected, List swapTargets, int curX, int curY) { 1041 | int right = curX + selected.getWidth(); 1042 | int bottom = curY + selected.getHeight(); 1043 | View winner = null; 1044 | int winnerScore = Integer.MAX_VALUE; 1045 | final int dx = curX - selected.getLeft(); 1046 | final int dy = curY - selected.getTop(); 1047 | final int targetsSize = swapTargets.size(); 1048 | for (int i = 0; i < targetsSize; i++) { 1049 | final View target = swapTargets.get(i); 1050 | final int score = Math.abs(target.getLeft() - curX) + Math.abs(target.getTop() - curY) 1051 | + Math.abs(target.getBottom() - bottom) + Math.abs(target.getRight() - right); 1052 | if (score < winnerScore) { 1053 | winnerScore = score; 1054 | winner = target; 1055 | } 1056 | // if (dx > 0) { 1057 | // int diff = target.getRight() - right; 1058 | // if (diff < 0) { 1059 | // final int score = Math.abs(diff); 1060 | // if (score > winnerScore) { 1061 | // winnerScore = score; 1062 | // winner = target; 1063 | // } 1064 | // } 1065 | // } 1066 | // if (dx < 0) { 1067 | // int diff = target.getLeft() - curX; 1068 | // if (diff > 0) { 1069 | // final int score = Math.abs(diff); 1070 | // if (score > winnerScore) { 1071 | // winnerScore = score; 1072 | // winner = target; 1073 | // } 1074 | // } 1075 | // } 1076 | // if (dy < 0) { 1077 | // int diff = target.getTop() - curY; 1078 | // if (diff > 0) { 1079 | // final int score = Math.abs(diff); 1080 | // if (score > winnerScore) { 1081 | // winnerScore = score; 1082 | // winner = target; 1083 | // } 1084 | // } 1085 | // } 1086 | // 1087 | // if (dy > 0) { 1088 | // int diff = target.getBottom() - bottom; 1089 | // if (diff < 0) { 1090 | // final int score = Math.abs(diff); 1091 | // if (score > winnerScore) { 1092 | // winnerScore = score; 1093 | // winner = target; 1094 | // } 1095 | // } 1096 | // } 1097 | } 1098 | return winner; 1099 | } 1100 | 1101 | /** 1102 | * 获取DragShadowBuilder 用于渲染 被拖动的view 1103 | * 默认使用 {@link ClassifyDragShadowBuilder} 实现 自定义时请重写该方法 1104 | * 1105 | * @param view 被拖动item 的 root view 1106 | * @return 1107 | */ 1108 | protected DragShadowBuilder getShadowBuilder(View view) { 1109 | return new ClassifyDragShadowBuilder(view); 1110 | } 1111 | 1112 | private ElevationHelper mElevationHelper = new ElevationHelper(); 1113 | 1114 | static class ElevationHelper { 1115 | 1116 | 1117 | public void floatView(RecyclerView recyclerView, View dragView) { 1118 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 1119 | float maxElevation = findMaxElevation(recyclerView) + 1f; 1120 | dragView.setElevation(maxElevation); 1121 | } else { 1122 | Drawable drawable = dragView.getBackground(); 1123 | if (drawable instanceof DragDrawable) { 1124 | DragDrawable dragDrawable = (DragDrawable) drawable; 1125 | dragDrawable.showShadow(); 1126 | dragView.setLayerType(View.LAYER_TYPE_SOFTWARE, dragDrawable.getPaint()); 1127 | } 1128 | } 1129 | } 1130 | 1131 | private float findMaxElevation(RecyclerView recyclerView) { 1132 | final int childCount = recyclerView.getChildCount(); 1133 | float max = 0; 1134 | for (int i = 0; i < childCount; i++) { 1135 | final View child = recyclerView.getChildAt(i); 1136 | 1137 | final float elevation = ViewCompat.getElevation(child); 1138 | if (elevation > max) { 1139 | max = elevation; 1140 | } 1141 | } 1142 | return max; 1143 | } 1144 | } 1145 | 1146 | 1147 | } -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/DragDrawable.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.ColorFilter; 7 | import android.graphics.Paint; 8 | import android.graphics.PixelFormat; 9 | import android.graphics.drawable.Drawable; 10 | import android.support.v4.view.ViewCompat; 11 | import android.view.View; 12 | 13 | import com.anarchy.classify.util.L; 14 | 15 | /** 16 | *

17 | * Date: 16/6/2 15:41 18 | * Author: zhendong.wu@shoufuyou.com 19 | *

20 | */ 21 | public class DragDrawable extends Drawable { 22 | final private View mView; 23 | final private Bitmap mBitmap; 24 | private boolean showShadow; 25 | final private Paint mPaint; 26 | public DragDrawable(View view){ 27 | mView = view; 28 | mView.setDrawingCacheEnabled(true); 29 | mView.buildDrawingCache(); 30 | mBitmap = mView.getDrawingCache(); 31 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 32 | mPaint.setColor(Color.BLACK); 33 | mPaint.setStyle(Paint.Style.STROKE); 34 | mPaint.setShadowLayer(5,8,8,0xFF808080); 35 | } 36 | @Override 37 | public void draw(Canvas canvas) { 38 | if(mBitmap == null) { 39 | mView.draw(canvas); 40 | }else { 41 | if(showShadow){ 42 | canvas.drawBitmap(mBitmap, 0, 0, mPaint); 43 | }else { 44 | canvas.drawBitmap(mBitmap, 0, 0, null); 45 | } 46 | } 47 | } 48 | 49 | @Override 50 | public void setAlpha(int alpha) { 51 | //nothing 52 | } 53 | 54 | @Override 55 | public void setColorFilter(ColorFilter colorFilter) {//nothing 56 | } 57 | 58 | @Override 59 | public int getOpacity() { 60 | return PixelFormat.TRANSLUCENT; 61 | } 62 | 63 | @Override 64 | public int getIntrinsicWidth() { 65 | return mView.getWidth(); 66 | } 67 | 68 | @Override 69 | public int getIntrinsicHeight() { 70 | return mView.getHeight(); 71 | } 72 | 73 | public void showShadow(){ 74 | showShadow = true; 75 | invalidateSelf(); 76 | } 77 | 78 | public Paint getPaint(){ 79 | return mPaint; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/adapter/BaseMainAdapter.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify.adapter; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.VelocityTracker; 6 | import android.view.View; 7 | 8 | import com.anarchy.classify.ChangeInfo; 9 | import com.anarchy.classify.ClassifyView; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | *

15 | * Date: 16/6/1 15:33 16 | * Author: zhendong.wu@shoufuyou.com 17 | *

18 | */ 19 | public abstract class BaseMainAdapter extends RecyclerView.Adapter implements MainRecyclerViewCallBack { 20 | private final static int VELOCITY = 5; 21 | private int mSelectedPosition = -1; 22 | 23 | @Override 24 | public void setDragPosition(int position) { 25 | if (position >= getItemCount() || position < -1) return; 26 | if (position == -1 && mSelectedPosition != -1) { 27 | int oldPosition = mSelectedPosition; 28 | mSelectedPosition = position; 29 | notifyItemChanged(oldPosition); 30 | } else { 31 | mSelectedPosition = position; 32 | notifyItemChanged(mSelectedPosition); 33 | } 34 | } 35 | 36 | 37 | @Override 38 | public boolean onMergeStart(RecyclerView parent, int selectedPosition, int targetPosition) { 39 | VH selectedViewHolder = (VH) parent.findViewHolderForAdapterPosition(selectedPosition); 40 | VH targetViewHolder = (VH) parent.findViewHolderForAdapterPosition(targetPosition); 41 | return onMergeStart(selectedViewHolder, targetViewHolder, selectedPosition, targetPosition); 42 | } 43 | 44 | @Override 45 | public void onMergeCancel(RecyclerView parent, int selectedPosition, int targetPosition) { 46 | VH selectedViewHolder = (VH) parent.findViewHolderForAdapterPosition(selectedPosition); 47 | VH targetViewHolder = (VH) parent.findViewHolderForAdapterPosition(targetPosition); 48 | onMergeCancel(selectedViewHolder, targetViewHolder, selectedPosition, targetPosition); 49 | } 50 | 51 | @Override 52 | public void onMerged(RecyclerView parent, int selectedPosition, int targetPosition) { 53 | VH selectedViewHolder = (VH) parent.findViewHolderForAdapterPosition(selectedPosition); 54 | VH targetViewHolder = (VH) parent.findViewHolderForAdapterPosition(targetPosition); 55 | onMerged(selectedViewHolder, targetViewHolder, selectedPosition, targetPosition); 56 | } 57 | 58 | @Override 59 | public ChangeInfo onPrepareMerge(RecyclerView parent, int selectedPosition, int targetPosition) { 60 | VH selectedViewHolder = (VH) parent.findViewHolderForAdapterPosition(selectedPosition); 61 | VH targetViewHolder = (VH) parent.findViewHolderForAdapterPosition(targetPosition); 62 | return onPrePareMerge(selectedViewHolder, targetViewHolder, selectedPosition, targetPosition); 63 | } 64 | 65 | @Override 66 | public void onStartMergeAnimation(RecyclerView parent, int selectedPosition, int targetPosition,int duration) { 67 | VH selectedViewHolder = (VH) parent.findViewHolderForAdapterPosition(selectedPosition); 68 | VH targetViewHolder = (VH) parent.findViewHolderForAdapterPosition(targetPosition); 69 | onStartMergeAnimation(selectedViewHolder, targetViewHolder, selectedPosition, targetPosition,duration); 70 | } 71 | 72 | public abstract boolean onMergeStart(VH selectedViewHolder, VH targetViewHolder, int selectedPosition, int targetPosition); 73 | 74 | public abstract void onMergeCancel(VH selectedViewHolder, VH targetViewHolder, int selectedPosition, int targetPosition); 75 | 76 | public abstract void onMerged(VH selectedViewHolder, VH targetViewHolder, int selectedPosition, int targetPosition); 77 | 78 | public abstract ChangeInfo onPrePareMerge(VH selectedViewHolder, VH targetViewHolder, int selectedPosition, int targetPosition); 79 | 80 | public abstract void onStartMergeAnimation(VH selectedViewHolder, VH targetViewHolder, int selectedPosition, int targetPosition,int duration); 81 | 82 | @Override 83 | public void onBindViewHolder(VH holder, int position, List payloads) { 84 | if (position == mSelectedPosition) { 85 | holder.itemView.setVisibility(View.INVISIBLE); 86 | } else { 87 | holder.itemView.setVisibility(View.VISIBLE); 88 | } 89 | super.onBindViewHolder(holder, position, payloads); 90 | } 91 | 92 | @Override 93 | public boolean canDropOVer(int selectedPosition, int targetPosition) { 94 | return true; 95 | } 96 | 97 | 98 | @Override 99 | public int getCurrentState(View selectedView, View targetView, int x, int y, 100 | VelocityTracker velocityTracker, int selectedPosition, 101 | int targetPosition) { 102 | if (velocityTracker == null) return ClassifyView.STATE_NONE; 103 | int left = x; 104 | int top = y; 105 | int right = left + selectedView.getWidth(); 106 | int bottom = top + selectedView.getHeight(); 107 | if (canMergeItem(selectedPosition, targetPosition)) { 108 | if ((Math.abs(left - targetView.getLeft()) + Math.abs(right - targetView.getRight()) + 109 | Math.abs(top - targetView.getTop()) + Math.abs(bottom - targetView.getBottom())) 110 | < (targetView.getWidth() + targetView.getHeight() 111 | ) / 3) { 112 | return ClassifyView.STATE_MERGE; 113 | } 114 | } 115 | if ((Math.abs(left - targetView.getLeft()) + Math.abs(right - targetView.getRight()) + 116 | Math.abs(top - targetView.getTop()) + Math.abs(bottom - targetView.getBottom())) 117 | < (targetView.getWidth() + targetView.getHeight() 118 | ) / 2) { 119 | velocityTracker.computeCurrentVelocity(100); 120 | float xVelocity = velocityTracker.getXVelocity(); 121 | float yVelocity = velocityTracker.getYVelocity(); 122 | float limit = getVelocity(targetView.getContext()); 123 | if (xVelocity < limit && yVelocity < limit) { 124 | return ClassifyView.STATE_MOVE; 125 | } 126 | } 127 | return ClassifyView.STATE_NONE; 128 | } 129 | 130 | @Override 131 | public float getVelocity(Context context) { 132 | float density = context.getResources().getDisplayMetrics().density; 133 | return density * VELOCITY + .5f; 134 | } 135 | 136 | @Override 137 | public void moved(int selectedPosition, int targetPosition) { 138 | 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/adapter/BaseSubAdapter.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify.adapter; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.VelocityTracker; 6 | import android.view.View; 7 | 8 | 9 | import com.anarchy.classify.ClassifyView; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * Version 1.0 15 | *

16 | * Date: 16/6/1 15:34 17 | * Author: zhendong.wu@shoufuyou.com 18 | *

19 | * Copyright © 2014-2016 Shanghai Xiaotu Network Technology Co., Ltd. 20 | */ 21 | public abstract class BaseSubAdapter extends RecyclerView.Adapter implements SubRecyclerViewCallBack { 22 | private final static int VELOCITY = 5; 23 | @Override 24 | public boolean canDragOnLongPress(int position, View pressedView) { 25 | return true; 26 | } 27 | 28 | private int mSelectedPosition = -1; 29 | @Override 30 | public void setDragPosition(int position) { 31 | if(position >= getItemCount()||position<-1) return; 32 | if(position == -1 && mSelectedPosition != -1){ 33 | // int oldPosition = mSelectedPosition; 34 | mSelectedPosition = position; 35 | notifyDataSetChanged(); 36 | // notifyItemChanged(oldPosition); 37 | }else { 38 | mSelectedPosition = position; 39 | notifyItemChanged(mSelectedPosition); 40 | } 41 | } 42 | 43 | @Override 44 | public void onBindViewHolder(VH holder, int position, List payloads) { 45 | if(position == mSelectedPosition){ 46 | holder.itemView.setVisibility(View.INVISIBLE); 47 | }else { 48 | holder.itemView.setVisibility(View.VISIBLE); 49 | } 50 | super.onBindViewHolder(holder, position, payloads); 51 | } 52 | 53 | @Override 54 | public boolean canDropOver(int selectedPosition, int targetPosition) { 55 | return true; 56 | } 57 | 58 | @Override 59 | public boolean canDragOut(int selectedPosition) { 60 | return true; 61 | } 62 | 63 | @Override 64 | public void moved(int selectedPosition, int targetPosition) { 65 | 66 | } 67 | @Override 68 | public int getCurrentState(View selectedView, View targetView, int x, int y, 69 | VelocityTracker velocityTracker, int selectedPosition, 70 | int targetPosition) { 71 | if(velocityTracker == null) return ClassifyView.STATE_NONE; 72 | int left = x; 73 | int top = y; 74 | int right = left + selectedView.getWidth(); 75 | int bottom = top + selectedView.getHeight(); 76 | if((Math.abs(left - targetView.getLeft())+Math.abs(right - targetView.getRight())+ 77 | Math.abs(top - targetView.getTop())+ Math.abs(bottom - targetView.getBottom())) 78 | <(targetView.getWidth()+targetView.getHeight() 79 | )/2){ 80 | velocityTracker.computeCurrentVelocity(100); 81 | float xVelocity = velocityTracker.getXVelocity(); 82 | float yVelocity = velocityTracker.getYVelocity(); 83 | float limit = getVelocity(targetView.getContext()); 84 | if(xVelocity < limit && yVelocity < limit){ 85 | return ClassifyView.STATE_MOVE; 86 | } 87 | } 88 | return ClassifyView.STATE_NONE; 89 | } 90 | 91 | @Override 92 | public float getVelocity(Context context) { 93 | float density = context.getResources().getDisplayMetrics().density; 94 | return density*VELOCITY + .5f; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/adapter/MainRecyclerViewCallBack.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify.adapter; 2 | 3 | import android.content.Context; 4 | import android.graphics.Point; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.VelocityTracker; 7 | import android.view.View; 8 | 9 | import com.anarchy.classify.ChangeInfo; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * Version 1.0 15 | *

16 | * Date: 16/6/1 15:10 17 | * Author: zhendong.wu@shoufuyou.com 18 | *

19 | * Copyright © 2014-2016 Shanghai Xiaotu Network Technology Co., Ltd. 20 | */ 21 | public interface MainRecyclerViewCallBack { 22 | void setDragPosition(int position); 23 | boolean canDragOnLongPress(int position, View pressedView); 24 | boolean canDropOVer(int selectedPosition,int targetPosition); 25 | boolean onMergeStart(RecyclerView parent,int selectedPosition, int targetPosition); 26 | void onMerged(RecyclerView parent, int selectedPosition, int targetPosition); 27 | ChangeInfo onPrepareMerge(RecyclerView parent, int selectedPosition, int targetPosition); 28 | void onStartMergeAnimation(RecyclerView parent,int selectedPosition,int targetPosition,int duration); 29 | void onMergeCancel(RecyclerView parent,int selectedPosition,int targetPosition); 30 | boolean onMove(int selectedPosition,int targetPosition); 31 | void moved(int selectedPosition,int targetPosition); 32 | boolean canMergeItem(int selectedPosition, int targetPosition); 33 | 34 | /** 35 | * 当次级目录移出范围时添加到 主目录 36 | * @param selectedPosition 37 | * @param subAdapterReference 38 | * @return 添加到主目录的位置 39 | */ 40 | int onLeaveSubRegion(int selectedPosition,SubAdapterReference subAdapterReference); 41 | 42 | /** 43 | * 返回判断移动需要的速度范围 44 | * 单位默认100 如果你没有重写 {@link #getCurrentState(View, View, int, int, VelocityTracker, int, int)}这个方法 45 | * @param context 46 | * @return 47 | */ 48 | float getVelocity(Context context); 49 | /** 50 | * 返回当前的状态 是移动 还是在merge范围中 51 | * @param selectedView 52 | * @param targetView 53 | * @param x 54 | * @param y 55 | * @return 56 | */ 57 | int getCurrentState(View selectedView, View targetView, int x, int y, VelocityTracker velocityTracker, int selectedPosition, int targetPosition); 58 | /** 59 | * 60 | * @param position 61 | * @param pressedView 62 | */ 63 | void onItemClick(int position,View pressedView); 64 | 65 | /** 66 | * 是否要展开这个view 67 | * @param position 68 | * @param pressedView 69 | * @return 如果返回空 或者 长度小于 2 则不会展开 之后会调用 {@link #onItemClick(int, View)} 70 | * 通知这是一个点击item的事件,其他情况会根据返回的List 通知 subAdapter 进行数据更新并打开显示subview的窗口 71 | */ 72 | List explodeItem(int position, View pressedView); 73 | } 74 | -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/adapter/SubAdapterReference.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify.adapter; 2 | 3 | /** 4 | *

5 | * Date: 16/6/6 17:17 6 | * Author: zhendong.wu@shoufuyou.com 7 | *

8 | */ 9 | public class SubAdapterReference { 10 | private final SubRecyclerViewCallBack mSubRecyclerViewCallBack; 11 | 12 | public SubAdapterReference(SubRecyclerViewCallBack subRecyclerViewCallBack) { 13 | mSubRecyclerViewCallBack = subRecyclerViewCallBack; 14 | } 15 | 16 | public T getAdapter() { 17 | return (T) mSubRecyclerViewCallBack; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/adapter/SubRecyclerViewCallBack.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify.adapter; 2 | 3 | import android.content.Context; 4 | import android.view.VelocityTracker; 5 | import android.view.View; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | *

11 | * Date: 16/6/1 15:11 12 | * Author: zhendong.wu@shoufuyou.com 13 | *

14 | */ 15 | public interface SubRecyclerViewCallBack { 16 | boolean canDragOnLongPress(int position, View pressedView); 17 | /** 18 | * 19 | * @param position 20 | * @param pressedView 21 | */ 22 | void onItemClick(int position,View pressedView); 23 | 24 | /** 25 | * 下面进行数据初始化 和 显示 26 | * @param data 27 | */ 28 | void initData(int parentIndex,List data); 29 | 30 | void setDragPosition(int position); 31 | 32 | 33 | boolean canDropOver(int selectedPosition,int targetPosition); 34 | 35 | boolean onMove(int selectedPosition,int targetPosition); 36 | void moved(int selectedPosition,int targetPosition); 37 | 38 | /** 39 | * 是否支持移出次级目录 40 | * @param selectedPosition 41 | * @return 42 | */ 43 | boolean canDragOut(int selectedPosition); 44 | 45 | /** 46 | * 返回判断移动需要的速度范围 47 | * 单位默认100 如果你没有重写 {@link #getCurrentState(View, View, int, int, VelocityTracker, int, int)}这个方法 48 | * @param context 49 | * @return 50 | */ 51 | float getVelocity(Context context); 52 | /** 53 | * 返回当前的状态 是移动 还是在merge范围中 54 | * @param selectedView 55 | * @param targetView 56 | * @param x 57 | * @param y 58 | * @return 59 | */ 60 | int getCurrentState(View selectedView, View targetView, int x, int y, VelocityTracker velocityTracker, int selectedPosition, int targetPosition); 61 | 62 | } 63 | -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/simple/BaseSimpleAdapter.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify.simple; 2 | 3 | import com.anarchy.classify.adapter.BaseMainAdapter; 4 | import com.anarchy.classify.adapter.BaseSubAdapter; 5 | 6 | /** 7 | * Version 1.0 8 | *

9 | * Date: 16/6/7 12:00 10 | * Author: zhendong.wu@shoufuyou.com 11 | *

12 | * Copyright © 2014-2016 Shanghai Xiaotu Network Technology Co., Ltd. 13 | */ 14 | public interface BaseSimpleAdapter { 15 | BaseMainAdapter getMainAdapter(); 16 | BaseSubAdapter getSubAdapter(); 17 | } 18 | -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/simple/SimpleAdapter.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify.simple; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import com.anarchy.classify.ChangeInfo; 9 | import com.anarchy.classify.R; 10 | import com.anarchy.classify.adapter.BaseMainAdapter; 11 | import com.anarchy.classify.adapter.BaseSubAdapter; 12 | import com.anarchy.classify.adapter.SubAdapterReference; 13 | import com.anarchy.classify.simple.widget.CanMergeView; 14 | import com.anarchy.classify.util.L; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | /** 20 | *

21 | * Date: 16/6/7 11:55 22 | * Author: zhendong.wu@shoufuyou.com 23 | *

24 | */ 25 | public abstract class SimpleAdapter implements BaseSimpleAdapter { 26 | protected List> mData; 27 | private SimpleMainAdapter mSimpleMainAdapter; 28 | private SimpleSubAdapter mSimpleSubAdapter; 29 | 30 | public SimpleAdapter(List> data) { 31 | mData = data; 32 | mSimpleMainAdapter = new SimpleMainAdapter(this, mData); 33 | mSimpleSubAdapter = new SimpleSubAdapter(this); 34 | } 35 | 36 | @Override 37 | public BaseMainAdapter getMainAdapter() { 38 | return mSimpleMainAdapter; 39 | } 40 | 41 | @Override 42 | public BaseSubAdapter getSubAdapter() { 43 | return mSimpleSubAdapter; 44 | } 45 | 46 | protected VH onCreateViewHolder(ViewGroup parent, int viewType) { 47 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.simple_item, parent, false); 48 | return (VH) new ViewHolder(view); 49 | } 50 | 51 | protected void onBindMainViewHolder(VH holder, int position) { 52 | } 53 | 54 | protected void onBindSubViewHolder(VH holder, int mainPosition,int subPosition) { 55 | } 56 | 57 | 58 | public void notifyItemInsert(int position){ 59 | mSimpleMainAdapter.notifyItemInserted(position); 60 | } 61 | 62 | public void notifyItemChanged(int position){ 63 | mSimpleMainAdapter.notifyItemChanged(position); 64 | } 65 | 66 | public void notifyItemRangeChanged(int position,int count){ 67 | mSimpleMainAdapter.notifyItemRangeChanged(position,count); 68 | } 69 | 70 | public void notifyItemRangeInsert(int position,int count){ 71 | mSimpleMainAdapter.notifyItemRangeInserted(position,count); 72 | } 73 | 74 | 75 | public void notifyDataSetChanged(){ 76 | mSimpleMainAdapter.notifyDataSetChanged(); 77 | } 78 | /** 79 | * @param parentIndex 80 | * @param index if -1 in main region 81 | */ 82 | protected void onItemClick(View view, int parentIndex, int index) { 83 | } 84 | 85 | /** 86 | * 显示一个item的布局 87 | * 88 | * @return 89 | */ 90 | public abstract View getView(ViewGroup parent, int mainPosition, int subPosition); 91 | 92 | class SimpleMainAdapter extends BaseMainAdapter { 93 | private List> mData; 94 | private SimpleAdapter mSimpleAdapter; 95 | 96 | public SimpleMainAdapter(SimpleAdapter simpleAdapter, List> data) { 97 | mData = data; 98 | mSimpleAdapter = simpleAdapter; 99 | } 100 | 101 | @Override 102 | public VH onCreateViewHolder(ViewGroup parent, int viewType) { 103 | VH vh = mSimpleAdapter.onCreateViewHolder(parent, viewType); 104 | CanMergeView canMergeView = vh.getCanMergeView(); 105 | if (canMergeView != null) { 106 | canMergeView.setAdapter(mSimpleAdapter); 107 | } 108 | return vh; 109 | } 110 | 111 | @Override 112 | public void onBindViewHolder(VH holder, int position) { 113 | CanMergeView canMergeView = holder.getCanMergeView(); 114 | if (canMergeView != null) { 115 | canMergeView.initMain(position, mData.get(position)); 116 | } 117 | mSimpleAdapter.onBindMainViewHolder(holder, position); 118 | } 119 | 120 | @Override 121 | public int getItemCount() { 122 | return mData.size(); 123 | } 124 | 125 | @Override 126 | public boolean canDragOnLongPress(int position, View pressedView) { 127 | return true; 128 | } 129 | 130 | 131 | 132 | @Override 133 | public boolean onMergeStart(VH selectedViewHolder, VH targetViewHolder, 134 | int selectedPosition, int targetPosition) { 135 | L.d("on mergeStart:(%1$s,%2$s)",selectedPosition,targetPosition); 136 | CanMergeView canMergeView = targetViewHolder.getCanMergeView(); 137 | if (canMergeView != null) { 138 | canMergeView.onMergeStart(); 139 | } 140 | return true; 141 | } 142 | 143 | @Override 144 | public void onMergeCancel(VH selectedViewHolder, VH targetViewHolder, 145 | int selectedPosition, int targetPosition) { 146 | L.d("on mergeCancel:(%1$s,%2$s)",selectedPosition,targetPosition); 147 | CanMergeView canMergeView = targetViewHolder.getCanMergeView(); 148 | if (canMergeView != null) { 149 | canMergeView.onMergeCancel(); 150 | } 151 | } 152 | 153 | @Override 154 | public void onMerged(VH selectedViewHolder, VH targetViewHolder, 155 | int selectedPosition, int targetPosition) { 156 | L.d("on Merged:(%1$s,%2$s)",selectedPosition,targetPosition); 157 | CanMergeView canMergeView = targetViewHolder.getCanMergeView(); 158 | if (canMergeView != null) { 159 | canMergeView.onMerged(); 160 | } 161 | mData.get(targetPosition).add(mData.get(selectedPosition).get(0)); 162 | mData.remove(selectedPosition); 163 | notifyItemRemoved(selectedPosition); 164 | if(selectedPosition < targetPosition) { 165 | notifyItemChanged(targetPosition-1); 166 | }else { 167 | notifyItemChanged(targetPosition); 168 | } 169 | } 170 | 171 | @Override 172 | public ChangeInfo onPrePareMerge(VH selectedViewHolder, VH targetViewHolder, int selectedPosition, int targetPosition) { 173 | if(targetViewHolder == null || selectedViewHolder == null) return null; 174 | CanMergeView canMergeView = targetViewHolder.getCanMergeView(); 175 | if (canMergeView != null) { 176 | ChangeInfo info = canMergeView.prepareMerge(); 177 | info.paddingLeft = selectedViewHolder.getPaddingLeft(); 178 | info.paddingRight = selectedViewHolder.getPaddingRight(); 179 | info.paddingTop = selectedViewHolder.getPaddingTop(); 180 | info.paddingBottom = selectedViewHolder.getPaddingBottom(); 181 | info.outlinePadding = canMergeView.getOutlinePadding(); 182 | return info; 183 | } 184 | return null; 185 | } 186 | 187 | @Override 188 | public void onStartMergeAnimation(VH selectedViewHolder, VH targetViewHolder, int selectedPosition, int targetPosition,int duration) { 189 | CanMergeView canMergeView = targetViewHolder.getCanMergeView(); 190 | if (canMergeView != null) { 191 | canMergeView.startMergeAnimation(duration); 192 | } 193 | } 194 | 195 | 196 | @Override 197 | public boolean onMove(int selectedPosition, int targetPosition) { 198 | notifyItemMoved(selectedPosition, targetPosition); 199 | List list = mData.remove(selectedPosition); 200 | mData.add(targetPosition, list); 201 | return true; 202 | } 203 | 204 | @Override 205 | public boolean canMergeItem(int selectedPosition, int targetPosition) { 206 | List currentSelected = mData.get(selectedPosition); 207 | return currentSelected.size() < 2; 208 | } 209 | 210 | 211 | @Override 212 | public int onLeaveSubRegion(int selectedPosition, SubAdapterReference subAdapterReference) { 213 | SimpleSubAdapter simpleSubAdapter = subAdapterReference.getAdapter(); 214 | T t = simpleSubAdapter.getData().remove(selectedPosition); 215 | List list = new ArrayList<>(); 216 | list.add(t); 217 | mData.add(list); 218 | int parentIndex = simpleSubAdapter.getParentIndex(); 219 | if (parentIndex != -1) notifyItemChanged(parentIndex); 220 | return mData.size() - 1; 221 | } 222 | 223 | @Override 224 | public void onItemClick(int position, View pressedView) { 225 | mSimpleAdapter.onItemClick(pressedView, position, -1); 226 | } 227 | 228 | @Override 229 | public List explodeItem(int position, View pressedView) { 230 | if (position < mData.size()) 231 | return mData.get(position); 232 | return null; 233 | } 234 | } 235 | 236 | class SimpleSubAdapter extends BaseSubAdapter { 237 | private List mData; 238 | private int parentIndex = -1; 239 | private SimpleAdapter mSimpleAdapter; 240 | 241 | public SimpleSubAdapter(SimpleAdapter simpleAdapter) { 242 | mSimpleAdapter = simpleAdapter; 243 | } 244 | 245 | @Override 246 | public VH onCreateViewHolder(ViewGroup parent, int viewType) { 247 | VH vh = mSimpleAdapter.onCreateViewHolder(parent, viewType); 248 | CanMergeView canMergeView = vh.getCanMergeView(); 249 | if (canMergeView != null) { 250 | canMergeView.setAdapter(mSimpleAdapter); 251 | } 252 | return vh; 253 | } 254 | 255 | public int getParentIndex() { 256 | return parentIndex; 257 | } 258 | 259 | @Override 260 | public void onBindViewHolder(VH holder, int position) { 261 | CanMergeView canMergeView = holder.getCanMergeView(); 262 | if (canMergeView != null) { 263 | canMergeView.initSub(parentIndex,position); 264 | } 265 | mSimpleAdapter.onBindSubViewHolder(holder,parentIndex,position); 266 | } 267 | 268 | @Override 269 | public int getItemCount() { 270 | if (mData == null) return 0; 271 | return mData.size(); 272 | } 273 | 274 | @Override 275 | public void onItemClick(int position, View pressedView) { 276 | mSimpleAdapter.onItemClick(pressedView, parentIndex, position); 277 | } 278 | 279 | @Override 280 | public void initData(int parentIndex, List data) { 281 | mData = data; 282 | this.parentIndex = parentIndex; 283 | notifyDataSetChanged(); 284 | } 285 | 286 | @Override 287 | public boolean onMove(int selectedPosition, int targetPosition) { 288 | notifyItemMoved(selectedPosition, targetPosition); 289 | T t = mData.remove(selectedPosition); 290 | mData.add(targetPosition, t); 291 | if(parentIndex != -1) { 292 | mSimpleMainAdapter.notifyItemChanged(parentIndex); 293 | } 294 | return true; 295 | } 296 | 297 | public List getData() { 298 | return mData; 299 | } 300 | 301 | } 302 | 303 | public static class ViewHolder extends RecyclerView.ViewHolder { 304 | protected CanMergeView mCanMergeView; 305 | private int paddingLeft; 306 | private int paddingRight; 307 | private int paddingTop; 308 | private int paddingBottom; 309 | 310 | public ViewHolder(View itemView) { 311 | super(itemView); 312 | if (itemView instanceof CanMergeView) { 313 | mCanMergeView = (CanMergeView) itemView; 314 | } else if (itemView instanceof ViewGroup) { 315 | ViewGroup group = (ViewGroup) itemView; 316 | paddingLeft = group.getPaddingLeft(); 317 | paddingRight = group.getPaddingRight(); 318 | paddingTop = group.getPaddingTop(); 319 | paddingBottom = group.getPaddingBottom(); 320 | //只遍历一层 寻找第一个符合条件的view 321 | for (int i = 0; i < group.getChildCount(); i++) { 322 | View child = group.getChildAt(i); 323 | if (child instanceof CanMergeView) { 324 | mCanMergeView = (CanMergeView) child; 325 | break; 326 | } 327 | } 328 | } 329 | } 330 | 331 | public CanMergeView getCanMergeView() { 332 | return mCanMergeView; 333 | } 334 | 335 | public int getPaddingLeft() { 336 | return paddingLeft; 337 | } 338 | 339 | public int getPaddingRight() { 340 | return paddingRight; 341 | } 342 | 343 | public int getPaddingTop() { 344 | return paddingTop; 345 | } 346 | 347 | public int getPaddingBottom() { 348 | return paddingBottom; 349 | } 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/simple/widget/BagDrawable.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify.simple.widget; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ObjectAnimator; 6 | import android.graphics.Canvas; 7 | import android.graphics.ColorFilter; 8 | import android.graphics.Paint; 9 | import android.graphics.PixelFormat; 10 | import android.graphics.RadialGradient; 11 | import android.graphics.RectF; 12 | import android.graphics.Shader; 13 | import android.graphics.drawable.Drawable; 14 | import android.util.Property; 15 | 16 | 17 | /** 18 | *

19 | * Date: 16/6/7 10:32 20 | * Author: zhendong.wu@shoufuyou.com 21 | *

22 | */ 23 | class BagDrawable extends Drawable { 24 | private RectF mRectF; 25 | private Paint mPaint; 26 | private Paint mOutlinePaint; 27 | private boolean keepShow = false; 28 | private boolean inMerge = false; 29 | private int mOutLineWidth; 30 | private int mOutLineColor; 31 | private int mOutlinePadding; 32 | private int mSavedOutlinePadding; 33 | private float mRadius = 5; 34 | private int mAnimationDuration = 200; 35 | private ObjectAnimator mStarAnimator; 36 | private ObjectAnimator mCancelAnimator; 37 | // private int[] mColors = new int[]{0xFF808080,0xFF808080,0xFFDDDDDD,0xFFFFFFFF,0xFFDDDDDD,0xFF808080,0xFF808080, 38 | // 0xFFDDDDDD,0xFFFFFFFF,0xFFDDDDDD,0xFF808080,0xFF808080}; 39 | // private float[] mPositions = new float[]{0f,0.11f,0.11f,0.125f,0.14f,0.14f,0.61f,0.61f,0.625f,0.64f,0.64f,1f}; 40 | private int mCenterColor = 0xFFFFFFFF; 41 | private int mEdgeColor = 0xFF808080; 42 | 43 | public BagDrawable(int outlinePadding) { 44 | mRectF = new RectF(); 45 | mPaint = new Paint(); 46 | mPaint.setAntiAlias(true); 47 | mOutlinePaint = new Paint(); 48 | mOutlinePaint.setAntiAlias(true); 49 | mOutlinePaint.setStyle(Paint.Style.STROKE); 50 | mOutlinePaint.setStrokeCap(Paint.Cap.ROUND); 51 | mOutlinePadding = outlinePadding; 52 | mSavedOutlinePadding = outlinePadding; 53 | mRadius = outlinePadding; 54 | } 55 | 56 | public void setKeepShow(boolean keepShow) { 57 | this.keepShow = keepShow; 58 | } 59 | 60 | public void setOutlineStyle(int color, int width) { 61 | mOutLineColor = color; 62 | mOutLineWidth = width; 63 | } 64 | 65 | @Override 66 | public void draw(Canvas canvas) { 67 | if (keepShow||inMerge) { 68 | canvas.save(); 69 | canvas.clipRect(getBounds()); 70 | mRectF.set(getBounds()); 71 | 72 | mRectF.inset(mOutlinePadding, mOutlinePadding); 73 | if (mOutLineWidth > 0) { 74 | mOutlinePaint.setStrokeWidth(mOutLineWidth); 75 | mRectF.inset(mOutLineWidth, mOutLineWidth); 76 | mOutlinePaint.setColor(mOutLineColor); 77 | canvas.drawRoundRect(mRectF, mRadius, mRadius, mOutlinePaint); 78 | } 79 | // mPaint.setShader(new SweepGradient(mRectF.centerX(),mRectF.centerY(),mColors,mPositions)); 80 | mPaint.setShader(new RadialGradient(mRectF.centerX(), mRectF.centerY(), mRectF.width(), mCenterColor, mEdgeColor, Shader.TileMode.CLAMP)); 81 | canvas.drawRoundRect(mRectF, mRadius, mRadius, mPaint); 82 | canvas.restore(); 83 | } 84 | } 85 | 86 | @Override 87 | public void setAlpha(int alpha) { 88 | mPaint.setAlpha(alpha); 89 | invalidateSelf(); 90 | } 91 | 92 | @Override 93 | public void setColorFilter(ColorFilter colorFilter) { 94 | mPaint.setColorFilter(colorFilter); 95 | invalidateSelf(); 96 | } 97 | 98 | @Override 99 | public int getOpacity() { 100 | return PixelFormat.TRANSLUCENT; 101 | } 102 | 103 | public void startMergeAnimation() { 104 | if(mCancelAnimator != null&&mCancelAnimator.isRunning()){ 105 | mCancelAnimator.cancel(); 106 | } 107 | if(mStarAnimator == null) { 108 | mStarAnimator = ObjectAnimator.ofInt(this, mOutlineProperty, 0); 109 | mStarAnimator.setDuration(mAnimationDuration); 110 | mStarAnimator.addListener(new AnimatorListenerAdapter() { 111 | @Override 112 | public void onAnimationStart(Animator animation) { 113 | inMerge = true; 114 | } 115 | }); 116 | }else if(mStarAnimator.isRunning()){ 117 | mStarAnimator.cancel(); 118 | } 119 | mStarAnimator.start(); 120 | } 121 | public void cancelMergeAnimation(){ 122 | if(mStarAnimator != null && mStarAnimator.isRunning()){ 123 | mStarAnimator.cancel(); 124 | } 125 | if(mCancelAnimator == null) { 126 | mCancelAnimator = ObjectAnimator.ofInt(this, mOutlineProperty, mSavedOutlinePadding); 127 | mCancelAnimator.setDuration(mAnimationDuration); 128 | mCancelAnimator.addListener(new AnimatorListenerAdapter() { 129 | @Override 130 | public void onAnimationEnd(Animator animation) { 131 | inMerge = false; 132 | } 133 | }); 134 | }else if(mCancelAnimator.isRunning()){ 135 | mCancelAnimator.cancel(); 136 | } 137 | mCancelAnimator.start(); 138 | } 139 | 140 | private Property mOutlineProperty = new Property(Integer.class,"outline") { 141 | @Override 142 | public Integer get(BagDrawable object) { 143 | return object.mOutlinePadding; 144 | } 145 | 146 | @Override 147 | public void set(BagDrawable object, Integer value) { 148 | object.mOutlinePadding = value; 149 | invalidateSelf(); 150 | } 151 | }; 152 | } 153 | -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/simple/widget/CanMergeView.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify.simple.widget; 2 | 3 | import android.graphics.Point; 4 | 5 | import com.anarchy.classify.ChangeInfo; 6 | import com.anarchy.classify.simple.SimpleAdapter; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * Version 1.0 12 | *

13 | * Date: 16/6/7 10:29 14 | * Author: zhendong.wu@shoufuyou.com 15 | *

16 | * Copyright © 2014-2016 Shanghai Xiaotu Network Technology Co., Ltd. 17 | */ 18 | public interface CanMergeView { 19 | /** 20 | * 进入merge状态 21 | */ 22 | void onMergeStart(); 23 | 24 | /** 25 | * 离开merge状态 26 | */ 27 | void onMergeCancel(); 28 | 29 | /** 30 | * 结束merge事件 31 | */ 32 | void onMerged(); 33 | 34 | /** 35 | * 开始merge动画 36 | * @param duration 动画持续时间 37 | */ 38 | void startMergeAnimation(int duration); 39 | 40 | /** 41 | * 准备merge 42 | * @return 返回新添加的view 应该放置在布局中的位置坐标 43 | */ 44 | ChangeInfo prepareMerge(); 45 | /** 46 | * 设置适配器 47 | * @param simpleAdapter 48 | */ 49 | void setAdapter(SimpleAdapter simpleAdapter); 50 | 51 | /** 52 | * 初始化 主层级 53 | * @param list 54 | */ 55 | void initMain(int parentIndex, List list); 56 | 57 | /** 58 | * 初始化 次级层级 59 | * @param parentIndex 60 | * @param subIndex 61 | */ 62 | void initSub(int parentIndex,int subIndex); 63 | 64 | 65 | int getOutlinePadding(); 66 | } 67 | -------------------------------------------------------------------------------- /classify/src/main/java/com/anarchy/classify/simple/widget/InsertAbleGridView.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify.simple.widget; 2 | 3 | import android.animation.ObjectAnimator; 4 | import android.animation.PropertyValuesHolder; 5 | import android.animation.ValueAnimator; 6 | import android.content.Context; 7 | import android.content.res.TypedArray; 8 | import android.graphics.Paint; 9 | import android.graphics.Point; 10 | import android.support.v4.widget.ScrollerCompat; 11 | import android.util.AttributeSet; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.view.animation.DecelerateInterpolator; 15 | import android.widget.ImageView; 16 | 17 | import com.anarchy.classify.ChangeInfo; 18 | import com.anarchy.classify.R; 19 | import com.anarchy.classify.simple.SimpleAdapter; 20 | import com.anarchy.classify.util.L; 21 | 22 | import java.util.List; 23 | 24 | /** 25 | * 显示merge状态及以列表形式排列子view 26 | * 接收一个 view 的集合 或则 图片的集合 27 | */ 28 | public class InsertAbleGridView extends ViewGroup implements CanMergeView{ 29 | private int mRowCount; 30 | private int mColumnCount; 31 | private int mRowGap; 32 | private int mColumnGap; 33 | private int mOutLinePadding; 34 | private int mInnerPadding; 35 | private BagDrawable mBagDrawable; 36 | private SimpleAdapter mSimpleAdapter; 37 | private int parentIndex; 38 | private ChangeInfo mReturnInfo = new ChangeInfo(); 39 | private ScrollerCompat mScroller; 40 | public InsertAbleGridView(Context context) { 41 | this(context,null); 42 | } 43 | 44 | public InsertAbleGridView(Context context, AttributeSet attrs) { 45 | this(context,attrs,0); 46 | } 47 | 48 | public InsertAbleGridView(Context context, AttributeSet attrs, int defStyleAttr) { 49 | super(context, attrs, defStyleAttr); 50 | init(context,attrs,defStyleAttr); 51 | } 52 | 53 | 54 | private void init(Context context,AttributeSet attrs,int defStyleAttr){ 55 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.InsertAbleGridView,defStyleAttr,R.style.InsertAbleGridViewDefaultStyle); 56 | mRowCount = a.getInt(R.styleable.InsertAbleGridView_RowCount,2); 57 | mColumnCount = a.getInt(R.styleable.InsertAbleGridView_ColumnCount,2); 58 | mRowGap = a.getDimensionPixelSize(R.styleable.InsertAbleGridView_RowGap,10); 59 | mColumnGap = a.getDimensionPixelSize(R.styleable.InsertAbleGridView_ColumnGap,10); 60 | mOutLinePadding = a.getDimensionPixelSize(R.styleable.InsertAbleGridView_OutlinePadding,10); 61 | mInnerPadding = a.getDimensionPixelOffset(R.styleable.InsertAbleGridView_InnerPadding,10); 62 | mBagDrawable = new BagDrawable(mOutLinePadding); 63 | mBagDrawable.setOutlineStyle(a.getColor(R.styleable.InsertAbleGridView_OutlineColor,0),a.getDimensionPixelSize(R.styleable.InsertAbleGridView_OutlineWidth,3)); 64 | setBackgroundDrawable(mBagDrawable); 65 | a.recycle(); 66 | mScroller = ScrollerCompat.create(context); 67 | } 68 | 69 | @Override 70 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 71 | int childCount = getChildCount(); 72 | int width = Math.max(r - l - getPaddingLeft()-getPaddingRight()-2*mOutLinePadding,0); 73 | int height = Math.max(b - t - getPaddingBottom()-getPaddingTop()-2*mOutLinePadding,0); 74 | int itemWidth = getItemWidth(width); 75 | int itemHeight = getItemHeight(height); 76 | int itemTotal = mRowCount*mColumnCount; 77 | if(childCount>0){ 78 | if(childCount == 1){ 79 | mBagDrawable.setKeepShow(false); 80 | getChildAt(0).layout(getPaddingLeft()+mOutLinePadding,getPaddingTop()+mOutLinePadding,getPaddingLeft()+mOutLinePadding+width,getPaddingTop()+mOutLinePadding+height); 81 | }else { 82 | mBagDrawable.setKeepShow(true); 83 | int row,col; 84 | for(int i=0;i=childCount-itemTotal && childCount%itemTotal != 0){ 95 | int newI = i%itemTotal; 96 | row = newI/mColumnCount; 97 | col = newI%mColumnCount; 98 | int left = getPaddingLeft()+mInnerPadding+mOutLinePadding+col*(itemWidth+mColumnGap); 99 | int right = left + itemWidth; 100 | int top = getHeight()+getPaddingTop()+mInnerPadding+mOutLinePadding+row*(itemHeight + mRowGap); 101 | int bottom = top+itemHeight; 102 | child.layout(left,top,right,bottom); 103 | } 104 | 105 | } 106 | } 107 | } 108 | } 109 | private ValueAnimator createConvertAnimator(final View view){ 110 | int width = getWidth() - getPaddingLeft()-getPaddingRight()-2*mOutLinePadding; 111 | int height = getHeight() - getPaddingBottom() - getPaddingTop()-2*mOutLinePadding; 112 | PropertyValuesHolder left = PropertyValuesHolder.ofInt("left",view.getLeft(),getPaddingLeft()+mInnerPadding+mOutLinePadding); 113 | PropertyValuesHolder right = PropertyValuesHolder.ofInt("right",view.getRight(),getPaddingLeft()+mInnerPadding+mOutLinePadding+getItemWidth(width)); 114 | PropertyValuesHolder top = PropertyValuesHolder.ofInt("top",view.getTop(),getPaddingTop()+mInnerPadding+mOutLinePadding); 115 | PropertyValuesHolder bottom = PropertyValuesHolder.ofInt("bottom",view.getBottom(),getPaddingTop()+mInnerPadding+mOutLinePadding+getItemHeight(height)); 116 | ValueAnimator animator = ObjectAnimator.ofPropertyValuesHolder(left,right,top,bottom); 117 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 118 | @Override 119 | public void onAnimationUpdate(ValueAnimator animation) { 120 | int left = (int) animation.getAnimatedValue("left"); 121 | int right = (int) animation.getAnimatedValue("right"); 122 | int top = (int) animation.getAnimatedValue("top"); 123 | int bottom = (int) animation.getAnimatedValue("bottom"); 124 | view.layout(left,top,right,bottom); 125 | } 126 | }); 127 | animator.setInterpolator(new DecelerateInterpolator()); 128 | return animator; 129 | } 130 | 131 | private int getItemWidth(int width){ 132 | return (width-2*mInnerPadding - (mColumnCount-1)*mRowGap)/mColumnCount; 133 | } 134 | private int getItemHeight(int height){ 135 | return (height-2*mInnerPadding -(mRowCount-1)*mColumnGap)/mRowCount; 136 | } 137 | @Override 138 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 139 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 140 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); 141 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 142 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 143 | if (widthMode != MeasureSpec.EXACTLY) 144 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 145 | if (heightMode != MeasureSpec.EXACTLY) 146 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); 147 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 148 | } 149 | 150 | @Override 151 | public void onMergeStart() { 152 | mBagDrawable.startMergeAnimation(); 153 | if(getChildCount() >= mRowCount*mColumnCount){ 154 | mScroller.startScroll(0,0,0,getHeight(),500); 155 | invalidate(); 156 | } 157 | } 158 | 159 | @Override 160 | public void computeScroll() { 161 | if(mScroller.computeScrollOffset()){ 162 | scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); 163 | invalidate(); 164 | } 165 | } 166 | 167 | @Override 168 | public void onMergeCancel() { 169 | mBagDrawable.cancelMergeAnimation(); 170 | if(getChildCount() >= mRowCount*mColumnCount){ 171 | mScroller.startScroll(0,getHeight(),0,-getHeight(),500); 172 | } 173 | } 174 | 175 | @Override 176 | public void onMerged() { 177 | mBagDrawable.setKeepShow(true); 178 | mBagDrawable.cancelMergeAnimation(); 179 | if(getChildCount() >= mRowCount*mColumnCount){ 180 | mScroller.startScroll(0,getHeight(),0,-getHeight(),500); 181 | } 182 | } 183 | 184 | @Override 185 | public void startMergeAnimation(int duration) { 186 | if(getChildCount() == 1){ 187 | View child = getChildAt(0); 188 | createConvertAnimator(child).setDuration(duration).start(); 189 | } 190 | } 191 | 192 | @Override 193 | public ChangeInfo prepareMerge() { 194 | int futureCount = getChildCount() + 1; 195 | if(futureCount > 1){ 196 | if(futureCount > mRowCount*mColumnCount) futureCount = futureCount%(mRowCount*mColumnCount); 197 | if(futureCount == 0) futureCount = mRowCount*mColumnCount; 198 | futureCount--; 199 | int row = futureCount/mColumnCount; 200 | int col = futureCount%mColumnCount; 201 | int width = getWidth() - getPaddingLeft()-getPaddingRight()-2*mOutLinePadding; 202 | int height = getHeight() - getPaddingTop() - getPaddingBottom()-2*mOutLinePadding; 203 | int itemWidth = getItemWidth(width); 204 | int itemHeight = getItemHeight(height); 205 | int left = getPaddingLeft()+mInnerPadding+mOutLinePadding+col*(itemWidth+mColumnGap); 206 | int top = getPaddingTop()+mInnerPadding+mOutLinePadding+row*(itemHeight + mRowGap); 207 | mReturnInfo.left = left; 208 | mReturnInfo.top = top; 209 | mReturnInfo.itemWidth = itemWidth; 210 | mReturnInfo.itemHeight = itemHeight; 211 | return mReturnInfo; 212 | } 213 | return null; 214 | } 215 | 216 | @Override 217 | public void setAdapter(SimpleAdapter simpleAdapter) { 218 | mSimpleAdapter = simpleAdapter; 219 | } 220 | 221 | @Override 222 | public void initMain(int parentIndex, List list) { 223 | removeAllViewsInLayout(); 224 | this.parentIndex = parentIndex; 225 | for(int i =0;i 8 | * Date: 16/6/1 16:06 9 | * Author: zhendong.wu@shoufuyou.com 10 | *

11 | * Copyright © 2014-2016 Shanghai Xiaotu Network Technology Co., Ltd. 12 | */ 13 | public class L { 14 | private static final String TAG = "ClassifyView"; 15 | private static final boolean DEBUG = true; 16 | public static void d(String msg){ 17 | if(DEBUG){ 18 | Log.d(TAG,msg); 19 | } 20 | } 21 | public static void d(String msg,Object... objects){ 22 | if(DEBUG){ 23 | Log.d(TAG,String.format(msg,objects)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /classify/src/main/res/drawable/ic_add_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /classify/src/main/res/layout/simple_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /classify/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /classify/src/main/res/values/classify_style.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 21 | -------------------------------------------------------------------------------- /classify/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | classify 3 | 4 | -------------------------------------------------------------------------------- /classify/src/test/java/com/anarchy/classify/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.anarchy.classify; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 9 | */ 10 | public class ExampleUnitTest { 11 | @Test 12 | public void addition_isCorrect() throws Exception { 13 | assertEquals(4, 2 + 2); 14 | } 15 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beasonshu/ClassifyView/3771450ad6782d4e31b5694231194b8d7892ac8f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 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-2.10-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 | -------------------------------------------------------------------------------- /screenshot/classifyView.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beasonshu/ClassifyView/3771450ad6782d4e31b5694231194b8d7892ac8f/screenshot/classifyView.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':classify' 2 | --------------------------------------------------------------------------------