├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ └── styles.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 │ │ │ └── layout │ │ │ │ ├── activity_main.xml │ │ │ │ └── item_view.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── weigan │ │ │ │ └── googlephotoselect │ │ │ │ ├── DataModel.java │ │ │ │ ├── MainActivity.java │ │ │ │ ├── MyAdapter.java │ │ │ │ ├── DragSelectTouchListener.java │ │ │ │ └── SelectItemAnimator.java │ │ └── AndroidManifest.xml │ └── test │ │ └── java │ │ └── com │ │ └── weigan │ │ └── googlephotoselect │ │ └── ExampleUnitTest.java ├── art │ └── MyVideoGif.gif ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── .idea ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── runConfigurations.xml └── compiler.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── README.md └── gradle.properties /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | GooglePhotoSelect 3 | 4 | -------------------------------------------------------------------------------- /app/art/MyVideoGif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidongjian/AndroidDragSelect-SimulateGooglePhoto/HEAD/app/art/MyVideoGif.gif -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidongjian/AndroidDragSelect-SimulateGooglePhoto/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidongjian/AndroidDragSelect-SimulateGooglePhoto/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidongjian/AndroidDragSelect-SimulateGooglePhoto/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidongjian/AndroidDragSelect-SimulateGooglePhoto/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidongjian/AndroidDragSelect-SimulateGooglePhoto/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidongjian/AndroidDragSelect-SimulateGooglePhoto/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .idea/.name 10 | .idea/gradle.xml 11 | .idea/misc.xml 12 | .idea/modules.xml 13 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon May 23 11:12:46 CST 2016 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GooglePhotoSelect 2 | 仿照Google相册的页面选择效果 3 | 4 | 对应文章:http://www.jianshu.com/p/7ba76d211299 5 | 6 | ![screenshot](https://github.com/weidongjian/GooglePhotoSelect/raw/master/app/art/MyVideoGif.gif) 7 | 8 | > 2016.5.23更新 9 | 10 | 1. 选择跟反选的逻辑也放到DragSelectTouchListener中,跟adapter进一步解耦 11 | 2. 自动滚动,由ScrollerCompat替代handle,使滚动如丝滑般顺畅 12 | 3. 修复滚动的时候,同时有其他手指触碰屏幕造成的错误 -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/test/java/com/weigan/googlephotoselect/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.weigan.googlephotoselect; 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 | } -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 D:\Android\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/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | applicationId "com.weigan.googlephotoselect" 9 | minSdkVersion 15 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | compile 'com.android.support:appcompat-v7:23.2.1' 25 | compile 'com.android.support:recyclerview-v7:23.2.1' 26 | } 27 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/weigan/googlephotoselect/DataModel.java: -------------------------------------------------------------------------------- 1 | package com.weigan.googlephotoselect; 2 | 3 | /** 4 | * Created by Administrator on 2016/5/7. 5 | */ 6 | public class DataModel { 7 | 8 | 9 | private boolean isSelected; 10 | private int position; 11 | 12 | public DataModel(boolean isSelected, int position) { 13 | this.isSelected = isSelected; 14 | this.position = position; 15 | } 16 | 17 | public boolean isSelected() { 18 | return isSelected; 19 | } 20 | 21 | public void setSelected(boolean isSelected) { 22 | this.isSelected = isSelected; 23 | } 24 | 25 | public int getPosition() { 26 | return position; 27 | } 28 | 29 | public void setPosition(int position) { 30 | this.position = position; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 23 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/src/main/java/com/weigan/googlephotoselect/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.weigan.googlephotoselect; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.ActionBar; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.widget.GridLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.View; 9 | 10 | public class MainActivity extends AppCompatActivity { 11 | private RecyclerView mRecyclerView; 12 | private MyAdapter adapter; 13 | private DragSelectTouchListener touchListener; 14 | 15 | private ActionBar actionBar; 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_main); 21 | 22 | actionBar = getSupportActionBar(); 23 | 24 | mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView); 25 | final GridLayoutManager layoutManager = new GridLayoutManager(this, 4); 26 | mRecyclerView.setLayoutManager(layoutManager); 27 | adapter = new MyAdapter(this); 28 | mRecyclerView.setAdapter(adapter); 29 | 30 | adapter.setLongClickListener(new View.OnLongClickListener() { 31 | @Override 32 | public boolean onLongClick(View v) { 33 | int position = mRecyclerView.getChildAdapterPosition(v); 34 | adapter.setSelected(position, true); 35 | touchListener.setStartSelectPosition(position); 36 | return false; 37 | } 38 | }); 39 | 40 | adapter.setClickListener(new View.OnClickListener() { 41 | @Override 42 | public void onClick(View v) { 43 | int position = mRecyclerView.getChildAdapterPosition(v); 44 | adapter.setSelected(position, true); 45 | } 46 | }); 47 | 48 | touchListener = new DragSelectTouchListener(); 49 | 50 | //监听滑动选择 51 | mRecyclerView.addOnItemTouchListener(touchListener); 52 | 53 | touchListener.setSelectListener(new DragSelectTouchListener.onSelectListener() { 54 | @Override 55 | public void onSelectChange(int start, int end, boolean isSelected) { 56 | //选择的范围回调 57 | adapter.selectRangeChange(start, end, isSelected); 58 | actionBar.setTitle(String.valueOf(adapter.getSelectedSize()) + " selected"); 59 | } 60 | }); 61 | 62 | RecyclerView.ItemAnimator itemAnimator = new SelectItemAnimator(); 63 | //设置选择状态切换时候的动画执行时间 64 | itemAnimator.setChangeDuration(300); 65 | 66 | mRecyclerView.setItemAnimator(itemAnimator); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/weigan/googlephotoselect/MyAdapter.java: -------------------------------------------------------------------------------- 1 | package com.weigan.googlephotoselect; 2 | 3 | import android.content.Context; 4 | import android.graphics.Color; 5 | import android.support.v4.view.ViewCompat; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.TextView; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * Created by Administrator on 2016/5/7. 17 | */ 18 | public class MyAdapter extends RecyclerView.Adapter { 19 | private LayoutInflater inflater; 20 | private static final int ITEM_COUNT = 150; 21 | 22 | private int colorSelect = Color.MAGENTA; 23 | private int colorNormal = Color.WHITE; 24 | 25 | private List items; 26 | 27 | private View.OnLongClickListener longClickListener; 28 | private View.OnClickListener clickListener; 29 | 30 | public MyAdapter(Context context) { 31 | inflater = LayoutInflater.from(context); 32 | items = new ArrayList<>(); 33 | for (int i = 0; i < ITEM_COUNT; i++) { 34 | items.add(new DataModel(false, i)); 35 | } 36 | } 37 | 38 | @Override 39 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 40 | View view = inflater.inflate(R.layout.item_view, parent, false); 41 | view.setOnLongClickListener(longClickListener); 42 | view.setOnClickListener(clickListener); 43 | return new ViewHolder(view); 44 | } 45 | 46 | @Override 47 | public void onBindViewHolder(ViewHolder holder, int position) { 48 | DataModel item = items.get(position); 49 | if (item.isSelected()) { 50 | ViewCompat.setScaleX(holder.itemView, 0.8f); 51 | ViewCompat.setScaleY(holder.itemView, 0.8f); 52 | holder.textView.setTextColor(colorSelect); 53 | } else { 54 | ViewCompat.setScaleX(holder.itemView, 1f); 55 | ViewCompat.setScaleY(holder.itemView, 1f); 56 | holder.textView.setTextColor(colorNormal); 57 | } 58 | holder.textView.setText(String.valueOf(position)); 59 | } 60 | 61 | @Override 62 | public int getItemCount() { 63 | return items.size(); 64 | } 65 | 66 | public void setSelected(int position, boolean selected) { 67 | if (items.get(position).isSelected() != selected) { 68 | items.get(position).setSelected(selected); 69 | notifyItemChanged(position); 70 | } 71 | } 72 | 73 | public void removeItem(int position) { 74 | items.remove(position); 75 | notifyItemRemoved(position); 76 | } 77 | 78 | public void setLongClickListener(View.OnLongClickListener clickListener) { 79 | this.longClickListener = clickListener; 80 | } 81 | 82 | public void setClickListener(View.OnClickListener clickListener) { 83 | this.clickListener = clickListener; 84 | } 85 | 86 | public void selectRangeChange(int start, int end, boolean isSelected) { 87 | if (start < 0 || end >= items.size()) { 88 | return; 89 | } 90 | if (isSelected) { 91 | dataSelect(start, end); 92 | } else { 93 | dataUnselect(start, end); 94 | } 95 | } 96 | 97 | public int getSelectedSize() { 98 | if (items.isEmpty()) { 99 | return 0; 100 | } 101 | int result = 0; 102 | for (DataModel item : items) { 103 | if (item.isSelected()) { 104 | result++; 105 | } 106 | } 107 | return result; 108 | } 109 | 110 | private void dataSelect(int start, int end) { 111 | for (int i = start; i <= end; i++) { 112 | DataModel model = getItem(i); 113 | if (!model.isSelected()) { 114 | model.setSelected(true); 115 | notifyItemChanged(i); 116 | } 117 | } 118 | } 119 | 120 | private void dataUnselect(int start, int end) { 121 | for (int i = start; i <= end; i++) { 122 | DataModel model = getItem(i); 123 | if (model.isSelected()) { 124 | model.setSelected(false); 125 | notifyItemChanged(i); 126 | } 127 | } 128 | } 129 | 130 | 131 | public DataModel getItem(int i) { 132 | return items.get(i); 133 | } 134 | 135 | protected static class ViewHolder extends RecyclerView.ViewHolder { 136 | private TextView textView; 137 | private View parentView; 138 | 139 | public ViewHolder(View itemView) { 140 | super(itemView); 141 | textView = (TextView) itemView.findViewById(R.id.textView); 142 | parentView = itemView.findViewById(R.id.parent); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /app/src/main/java/com/weigan/googlephotoselect/DragSelectTouchListener.java: -------------------------------------------------------------------------------- 1 | package com.weigan.googlephotoselect; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.os.Handler; 6 | import android.os.Looper; 7 | import android.support.v4.view.MotionEventCompat; 8 | import android.support.v4.view.ViewCompat; 9 | import android.support.v4.widget.ScrollerCompat; 10 | import android.support.v7.widget.RecyclerView; 11 | import android.util.Log; 12 | import android.view.MotionEvent; 13 | import android.view.View; 14 | import android.view.animation.LinearInterpolator; 15 | 16 | /** 17 | * Created by Administrator on 2016/5/7. 18 | */ 19 | public class DragSelectTouchListener implements RecyclerView.OnItemTouchListener { 20 | 21 | private boolean isActive; 22 | private int start, end; 23 | 24 | private onSelectListener selectListener; 25 | 26 | private RecyclerView recyclerView; 27 | 28 | private static final int DELAY = 25; 29 | 30 | private int autoScrollDistance = (int) (Resources.getSystem().getDisplayMetrics().density * 56); 31 | 32 | private int mTopBound, mBottomBound; 33 | 34 | private boolean inTopSpot, inBottomSpot; 35 | 36 | private Handler autoScrollHandler = new Handler(Looper.getMainLooper()); 37 | 38 | private int scrollDistance; 39 | 40 | private float lastX, lastY; 41 | 42 | private static final int MAX_SCROLL_DISTANCE = 16; 43 | 44 | //这个数越大,滚动的速度增加越慢 45 | private static final int SCROLL_FECTOR = 6; 46 | 47 | private int lastStart, lastEnd; 48 | 49 | private ScrollerCompat scroller; 50 | 51 | private Runnable scrollRunnable = new Runnable() { 52 | @Override 53 | public void run() { 54 | if (!inTopSpot && !inBottomSpot) { 55 | return; 56 | } 57 | scrollBy(scrollDistance); 58 | autoScrollHandler.postDelayed(this, DELAY); 59 | } 60 | }; 61 | 62 | public void setSelectListener(onSelectListener selectListener) { 63 | this.selectListener = selectListener; 64 | } 65 | 66 | public interface onSelectListener{ 67 | /** 68 | * 选择结果的回调 69 | * @param start 开始的位置 70 | * @param end 结束的位置 71 | * @param isSelected 是否选中 72 | */ 73 | void onSelectChange(int start, int end, boolean isSelected); 74 | }; 75 | 76 | public DragSelectTouchListener() { 77 | reset(); 78 | } 79 | 80 | @Override 81 | public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { 82 | if (!isActive || rv.getAdapter().getItemCount() == 0) { 83 | return false; 84 | } 85 | int action = MotionEventCompat.getActionMasked(e); 86 | switch (action) { 87 | case MotionEvent.ACTION_POINTER_DOWN: 88 | Log.d("weigan", "onInterceptTouchEvent ACTION_POINTER_DOWN"); 89 | case MotionEvent.ACTION_DOWN: 90 | Log.d("weigan", "onInterceptTouchEvent ACTION_DOWN"); 91 | reset(); 92 | break; 93 | } 94 | recyclerView = rv; 95 | int height = rv.getHeight(); 96 | mTopBound = -20; 97 | mBottomBound = height - autoScrollDistance; 98 | return true; 99 | } 100 | 101 | public void startAutoScroll() { 102 | if (recyclerView == null) { 103 | return; 104 | } 105 | initScroller(recyclerView.getContext()); 106 | if (scroller.isFinished()) { 107 | recyclerView.removeCallbacks(scrollRun); 108 | scroller.startScroll(0, scroller.getCurrY(), 0, 5000, 100000); 109 | ViewCompat.postOnAnimation(recyclerView, scrollRun); 110 | } 111 | } 112 | 113 | private void initScroller(Context context) { 114 | if (scroller == null) { 115 | scroller = ScrollerCompat.create(context, new LinearInterpolator()); 116 | } 117 | } 118 | 119 | public void stopAutoScroll() { 120 | if (scroller != null && !scroller.isFinished()) { 121 | recyclerView.removeCallbacks(scrollRun); 122 | scroller.abortAnimation(); 123 | } 124 | } 125 | 126 | private Runnable scrollRun = new Runnable() { 127 | @Override 128 | public void run() { 129 | if (scroller != null && scroller.computeScrollOffset()) { 130 | Log.d("weigan", "scrollRun called"); 131 | scrollBy(scrollDistance); 132 | ViewCompat.postOnAnimation(recyclerView, scrollRun); 133 | } 134 | } 135 | }; 136 | 137 | @Override 138 | public void onTouchEvent(RecyclerView rv, MotionEvent e) { 139 | if (!isActive) { 140 | return; 141 | } 142 | int action = MotionEventCompat.getActionMasked(e); 143 | switch (action) { 144 | case MotionEvent.ACTION_MOVE: 145 | if (!inTopSpot && !inBottomSpot) { 146 | //更新滑动选择区域 147 | updateSelectedRange(rv, e); 148 | } 149 | //在顶部或者底部触发自动滑动 150 | processAutoScroll(e); 151 | break; 152 | case MotionEvent.ACTION_CANCEL: 153 | case MotionEvent.ACTION_UP: 154 | case MotionEvent.ACTION_POINTER_UP: 155 | //结束滑动选择,初始化各状态值 156 | reset(); 157 | break; 158 | } 159 | } 160 | 161 | private void updateSelectedRange(RecyclerView rv, MotionEvent e) { 162 | updateSelectedRange(rv, e.getX(), e.getY()); 163 | } 164 | 165 | private void updateSelectedRange(RecyclerView rv, float x, float y) { 166 | View child = rv.findChildViewUnder(x, y); 167 | if (child != null) { 168 | int position = rv.getChildAdapterPosition(child); 169 | if (position != RecyclerView.NO_POSITION && end != position) { 170 | end = position; 171 | notifySelectRangeChange(); 172 | } 173 | } 174 | } 175 | 176 | 177 | private void processAutoScroll(MotionEvent event) { 178 | int y = (int) event.getY(); 179 | if (y < mTopBound) { 180 | lastX = event.getX(); 181 | lastY = event.getY(); 182 | scrollDistance = -(mTopBound - y) / SCROLL_FECTOR; 183 | if (!inTopSpot) { 184 | inTopSpot = true; 185 | // autoScrollHandler.removeCallbacks(scrollRunnable); 186 | // autoScrollHandler.postDelayed(scrollRunnable, DELAY); 187 | startAutoScroll(); 188 | } 189 | }else if (y > mBottomBound) { 190 | lastX = event.getX(); 191 | lastY = event.getY(); 192 | scrollDistance = (y - mBottomBound) / SCROLL_FECTOR; 193 | if (!inBottomSpot) { 194 | inBottomSpot = true; 195 | // autoScrollHandler.removeCallbacks(scrollRunnable); 196 | // autoScrollHandler.postDelayed(scrollRunnable, DELAY); 197 | startAutoScroll(); 198 | } 199 | } else { 200 | // autoScrollHandler.removeCallbacks(scrollRunnable); 201 | inBottomSpot = false; 202 | inTopSpot = false; 203 | lastX = Float.MIN_VALUE; 204 | lastY = Float.MIN_VALUE; 205 | stopAutoScroll(); 206 | } 207 | } 208 | 209 | private void notifySelectRangeChange() { 210 | if (selectListener == null) { 211 | return; 212 | } 213 | if (start == RecyclerView.NO_POSITION || end == RecyclerView.NO_POSITION) { 214 | return; 215 | } 216 | 217 | int newStart, newEnd; 218 | newStart = Math.min(start, end); 219 | newEnd = Math.max(start, end); 220 | if (lastStart == RecyclerView.NO_POSITION || lastEnd == RecyclerView.NO_POSITION) { 221 | if (newEnd - newStart == 1) { 222 | selectListener.onSelectChange(newStart, newStart, true); 223 | } else { 224 | selectListener.onSelectChange(newStart, newEnd, true); 225 | } 226 | } else { 227 | if (newStart > lastStart) { 228 | selectListener.onSelectChange(lastStart, newStart - 1, false); 229 | } else if (newStart < lastStart) { 230 | selectListener.onSelectChange(newStart, lastStart - 1, true); 231 | } 232 | 233 | if (newEnd > lastEnd) { 234 | selectListener.onSelectChange(lastEnd + 1, newEnd, true); 235 | } else if (newEnd < lastEnd) { 236 | selectListener.onSelectChange(newEnd + 1, lastEnd, false); 237 | } 238 | } 239 | 240 | lastStart = newStart; 241 | lastEnd = newEnd; 242 | } 243 | 244 | private void reset() { 245 | setIsActive(false); 246 | start = RecyclerView.NO_POSITION; 247 | end = RecyclerView.NO_POSITION; 248 | lastStart = RecyclerView.NO_POSITION; 249 | lastEnd = RecyclerView.NO_POSITION; 250 | autoScrollHandler.removeCallbacks(scrollRunnable); 251 | inTopSpot = false; 252 | inBottomSpot = false; 253 | lastX = Float.MIN_VALUE; 254 | lastY = Float.MIN_VALUE; 255 | stopAutoScroll(); 256 | } 257 | 258 | @Override 259 | public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { 260 | 261 | } 262 | 263 | private void scrollBy(int distance) { 264 | int scrollDistance; 265 | if (distance > 0) { 266 | scrollDistance = Math.min(distance, MAX_SCROLL_DISTANCE); 267 | } else { 268 | scrollDistance = Math.max(distance, -MAX_SCROLL_DISTANCE); 269 | } 270 | recyclerView.scrollBy(0, scrollDistance); 271 | if (lastX != Float.MIN_VALUE && lastY != Float.MIN_VALUE) { 272 | updateSelectedRange(recyclerView, lastX, lastY); 273 | } 274 | } 275 | 276 | public void setIsActive(boolean isActive) { 277 | this.isActive = isActive; 278 | } 279 | 280 | public void setStartSelectPosition(int position) { 281 | setIsActive(true); 282 | start = position; 283 | end = position; 284 | lastStart = position; 285 | lastEnd = position; 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /app/src/main/java/com/weigan/googlephotoselect/SelectItemAnimator.java: -------------------------------------------------------------------------------- 1 | package com.weigan.googlephotoselect; 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 | * Created by Weidongjian on 2016/5/9. 17 | */ 18 | public class SelectItemAnimator extends SimpleItemAnimator { 19 | private static final boolean DEBUG = false; 20 | 21 | private ArrayList mPendingRemovals = new ArrayList<>(); 22 | private ArrayList mPendingAdditions = new ArrayList<>(); 23 | private ArrayList mPendingMoves = new ArrayList<>(); 24 | private ArrayList mPendingChanges = new ArrayList<>(); 25 | 26 | private ArrayList> mAdditionsList = new ArrayList<>(); 27 | private ArrayList> mMovesList = new ArrayList<>(); 28 | private ArrayList> mChangesList = new ArrayList<>(); 29 | 30 | private ArrayList mAddAnimations = new ArrayList<>(); 31 | private ArrayList mMoveAnimations = new ArrayList<>(); 32 | private ArrayList mRemoveAnimations = new ArrayList<>(); 33 | private ArrayList mChangeAnimations = new ArrayList<>(); 34 | 35 | private static class MoveInfo { 36 | public RecyclerView.ViewHolder holder; 37 | public int fromX, fromY, toX, toY; 38 | 39 | private MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { 40 | this.holder = holder; 41 | this.fromX = fromX; 42 | this.fromY = fromY; 43 | this.toX = toX; 44 | this.toY = toY; 45 | } 46 | } 47 | 48 | private static class ChangeInfo { 49 | public RecyclerView.ViewHolder oldHolder, newHolder; 50 | public int fromX, fromY, toX, toY; 51 | private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) { 52 | this.oldHolder = oldHolder; 53 | this.newHolder = newHolder; 54 | } 55 | 56 | private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, 57 | int fromX, int fromY, int toX, int toY) { 58 | this(oldHolder, newHolder); 59 | this.fromX = fromX; 60 | this.fromY = fromY; 61 | this.toX = toX; 62 | this.toY = toY; 63 | } 64 | 65 | @Override 66 | public String toString() { 67 | return "ChangeInfo{" + 68 | "oldHolder=" + oldHolder + 69 | ", newHolder=" + newHolder + 70 | ", fromX=" + fromX + 71 | ", fromY=" + fromY + 72 | ", toX=" + toX + 73 | ", toY=" + toY + 74 | '}'; 75 | } 76 | } 77 | 78 | @Override 79 | public void runPendingAnimations() { 80 | boolean removalsPending = !mPendingRemovals.isEmpty(); 81 | boolean movesPending = !mPendingMoves.isEmpty(); 82 | boolean changesPending = !mPendingChanges.isEmpty(); 83 | boolean additionsPending = !mPendingAdditions.isEmpty(); 84 | if (!removalsPending && !movesPending && !additionsPending && !changesPending) { 85 | // nothing to animate 86 | return; 87 | } 88 | // First, remove stuff 89 | for (RecyclerView.ViewHolder holder : mPendingRemovals) { 90 | animateRemoveImpl(holder); 91 | } 92 | mPendingRemovals.clear(); 93 | // Next, move stuff 94 | if (movesPending) { 95 | final ArrayList moves = new ArrayList<>(); 96 | moves.addAll(mPendingMoves); 97 | mMovesList.add(moves); 98 | mPendingMoves.clear(); 99 | Runnable mover = new Runnable() { 100 | @Override 101 | public void run() { 102 | for (MoveInfo moveInfo : moves) { 103 | animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, 104 | moveInfo.toX, moveInfo.toY); 105 | } 106 | moves.clear(); 107 | mMovesList.remove(moves); 108 | } 109 | }; 110 | if (removalsPending) { 111 | View view = moves.get(0).holder.itemView; 112 | ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); 113 | } else { 114 | mover.run(); 115 | } 116 | } 117 | // Next, change stuff, to run in parallel with move animations 118 | if (changesPending) { 119 | final ArrayList changes = new ArrayList<>(); 120 | changes.addAll(mPendingChanges); 121 | mChangesList.add(changes); 122 | mPendingChanges.clear(); 123 | Runnable changer = new Runnable() { 124 | @Override 125 | public void run() { 126 | for (ChangeInfo change : changes) { 127 | animateChangeImpl(change); 128 | } 129 | changes.clear(); 130 | mChangesList.remove(changes); 131 | } 132 | }; 133 | if (removalsPending) { 134 | RecyclerView.ViewHolder holder = changes.get(0).oldHolder; 135 | ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); 136 | } else { 137 | changer.run(); 138 | } 139 | } 140 | // Next, add stuff 141 | if (additionsPending) { 142 | final ArrayList additions = new ArrayList<>(); 143 | additions.addAll(mPendingAdditions); 144 | mAdditionsList.add(additions); 145 | mPendingAdditions.clear(); 146 | Runnable adder = new Runnable() { 147 | public void run() { 148 | for (RecyclerView.ViewHolder holder : additions) { 149 | animateAddImpl(holder); 150 | } 151 | additions.clear(); 152 | mAdditionsList.remove(additions); 153 | } 154 | }; 155 | if (removalsPending || movesPending || changesPending) { 156 | long removeDuration = removalsPending ? getRemoveDuration() : 0; 157 | long moveDuration = movesPending ? getMoveDuration() : 0; 158 | long changeDuration = changesPending ? getChangeDuration() : 0; 159 | long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); 160 | View view = additions.get(0).itemView; 161 | ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); 162 | } else { 163 | adder.run(); 164 | } 165 | } 166 | } 167 | 168 | @Override 169 | public boolean animateRemove(final RecyclerView.ViewHolder holder) { 170 | resetAnimation(holder); 171 | mPendingRemovals.add(holder); 172 | return true; 173 | } 174 | 175 | private void animateRemoveImpl(final RecyclerView.ViewHolder holder) { 176 | final View view = holder.itemView; 177 | final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); 178 | mRemoveAnimations.add(holder); 179 | animation.setDuration(getRemoveDuration()) 180 | .alpha(0) 181 | .scaleY(0f) 182 | .scaleX(0f) 183 | .setListener(new VpaListenerAdapter() { 184 | @Override 185 | public void onAnimationStart(View view) { 186 | dispatchRemoveStarting(holder); 187 | } 188 | 189 | @Override 190 | public void onAnimationEnd(View view) { 191 | animation.setListener(null); 192 | ViewCompat.setAlpha(view, 1); 193 | ViewCompat.setScaleX(view, 1); 194 | ViewCompat.setScaleY(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 | ViewCompat.setAlpha(newHolder.itemView, 0); 321 | } 322 | mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); 323 | return true; 324 | } 325 | 326 | private void animateChangeImpl(final ChangeInfo changeInfo) { 327 | final RecyclerView.ViewHolder holder = changeInfo.oldHolder; 328 | final View view = holder == null ? null : holder.itemView; 329 | final RecyclerView.ViewHolder newHolder = changeInfo.newHolder; 330 | final View newView = newHolder != null ? newHolder.itemView : null; 331 | if (view != null) { 332 | final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration( 333 | getChangeDuration()); 334 | mChangeAnimations.add(changeInfo.oldHolder); 335 | oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); 336 | oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); 337 | //对于本来是选中的,执行放大的动画,对于不是选择的,执行缩放的动画 338 | boolean isScale = ViewCompat.getScaleX(view) < 0.9f; 339 | float scaleFactor = isScale ? 1f : 0.8f; 340 | oldViewAnim.scaleX(scaleFactor) 341 | .scaleY(scaleFactor) 342 | .setListener(new VpaListenerAdapter() { 343 | @Override 344 | public void onAnimationStart(View view) { 345 | dispatchChangeStarting(changeInfo.oldHolder, true); 346 | } 347 | 348 | @Override 349 | public void onAnimationEnd(View view) { 350 | oldViewAnim.setListener(null); 351 | ViewCompat.setAlpha(view, 1); 352 | ViewCompat.setTranslationX(view, 0); 353 | ViewCompat.setTranslationY(view, 0); 354 | ViewCompat.setScaleX(view, 1f); 355 | ViewCompat.setScaleY(view, 1f); 356 | dispatchChangeFinished(changeInfo.oldHolder, true); 357 | mChangeAnimations.remove(changeInfo.oldHolder); 358 | dispatchFinishedWhenDone(); 359 | } 360 | }).start(); 361 | } 362 | if (newView != null) { 363 | final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView); 364 | mChangeAnimations.add(changeInfo.newHolder); 365 | newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()). 366 | alpha(1) 367 | .setListener(new VpaListenerAdapter() { 368 | @Override 369 | public void onAnimationStart(View view) { 370 | dispatchChangeStarting(changeInfo.newHolder, false); 371 | } 372 | 373 | @Override 374 | public void onAnimationEnd(View view) { 375 | newViewAnimation.setListener(null); 376 | ViewCompat.setAlpha(newView, 1); 377 | ViewCompat.setTranslationX(newView, 0); 378 | ViewCompat.setTranslationY(newView, 0); 379 | dispatchChangeFinished(changeInfo.newHolder, false); 380 | mChangeAnimations.remove(changeInfo.newHolder); 381 | dispatchFinishedWhenDone(); 382 | } 383 | }).start(); 384 | } 385 | } 386 | 387 | private void endChangeAnimation(List infoList, RecyclerView.ViewHolder item) { 388 | for (int i = infoList.size() - 1; i >= 0; i--) { 389 | ChangeInfo changeInfo = infoList.get(i); 390 | if (endChangeAnimationIfNecessary(changeInfo, item)) { 391 | if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { 392 | infoList.remove(changeInfo); 393 | } 394 | } 395 | } 396 | } 397 | 398 | private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { 399 | if (changeInfo.oldHolder != null) { 400 | endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); 401 | } 402 | if (changeInfo.newHolder != null) { 403 | endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); 404 | } 405 | } 406 | private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) { 407 | boolean oldItem = false; 408 | if (changeInfo.newHolder == item) { 409 | changeInfo.newHolder = null; 410 | } else if (changeInfo.oldHolder == item) { 411 | changeInfo.oldHolder = null; 412 | oldItem = true; 413 | } else { 414 | return false; 415 | } 416 | ViewCompat.setAlpha(item.itemView, 1); 417 | ViewCompat.setTranslationX(item.itemView, 0); 418 | ViewCompat.setTranslationY(item.itemView, 0); 419 | dispatchChangeFinished(item, oldItem); 420 | return true; 421 | } 422 | 423 | @Override 424 | public void endAnimation(RecyclerView.ViewHolder item) { 425 | final View view = item.itemView; 426 | // this will trigger end callback which should set properties to their target values. 427 | ViewCompat.animate(view).cancel(); 428 | // TODO if some other animations are chained to end, how do we cancel them as well? 429 | for (int i = mPendingMoves.size() - 1; i >= 0; i--) { 430 | MoveInfo moveInfo = mPendingMoves.get(i); 431 | if (moveInfo.holder == item) { 432 | ViewCompat.setTranslationY(view, 0); 433 | ViewCompat.setTranslationX(view, 0); 434 | dispatchMoveFinished(item); 435 | mPendingMoves.remove(i); 436 | } 437 | } 438 | endChangeAnimation(mPendingChanges, item); 439 | if (mPendingRemovals.remove(item)) { 440 | ViewCompat.setAlpha(view, 1); 441 | dispatchRemoveFinished(item); 442 | } 443 | if (mPendingAdditions.remove(item)) { 444 | ViewCompat.setAlpha(view, 1); 445 | dispatchAddFinished(item); 446 | } 447 | 448 | for (int i = mChangesList.size() - 1; i >= 0; i--) { 449 | ArrayList changes = mChangesList.get(i); 450 | endChangeAnimation(changes, item); 451 | if (changes.isEmpty()) { 452 | mChangesList.remove(i); 453 | } 454 | } 455 | for (int i = mMovesList.size() - 1; i >= 0; i--) { 456 | ArrayList moves = mMovesList.get(i); 457 | for (int j = moves.size() - 1; j >= 0; j--) { 458 | MoveInfo moveInfo = moves.get(j); 459 | if (moveInfo.holder == item) { 460 | ViewCompat.setTranslationY(view, 0); 461 | ViewCompat.setTranslationX(view, 0); 462 | dispatchMoveFinished(item); 463 | moves.remove(j); 464 | if (moves.isEmpty()) { 465 | mMovesList.remove(i); 466 | } 467 | break; 468 | } 469 | } 470 | } 471 | for (int i = mAdditionsList.size() - 1; i >= 0; i--) { 472 | ArrayList additions = mAdditionsList.get(i); 473 | if (additions.remove(item)) { 474 | ViewCompat.setAlpha(view, 1); 475 | dispatchAddFinished(item); 476 | if (additions.isEmpty()) { 477 | mAdditionsList.remove(i); 478 | } 479 | } 480 | } 481 | 482 | // animations should be ended by the cancel above. 483 | //noinspection PointlessBooleanExpression,ConstantConditions 484 | if (mRemoveAnimations.remove(item) && DEBUG) { 485 | throw new IllegalStateException("after animation is cancelled, item should not be in " 486 | + "mRemoveAnimations list"); 487 | } 488 | 489 | //noinspection PointlessBooleanExpression,ConstantConditions 490 | if (mAddAnimations.remove(item) && DEBUG) { 491 | throw new IllegalStateException("after animation is cancelled, item should not be in " 492 | + "mAddAnimations list"); 493 | } 494 | 495 | //noinspection PointlessBooleanExpression,ConstantConditions 496 | if (mChangeAnimations.remove(item) && DEBUG) { 497 | throw new IllegalStateException("after animation is cancelled, item should not be in " 498 | + "mChangeAnimations list"); 499 | } 500 | 501 | //noinspection PointlessBooleanExpression,ConstantConditions 502 | if (mMoveAnimations.remove(item) && DEBUG) { 503 | throw new IllegalStateException("after animation is cancelled, item should not be in " 504 | + "mMoveAnimations list"); 505 | } 506 | dispatchFinishedWhenDone(); 507 | } 508 | 509 | private void resetAnimation(RecyclerView.ViewHolder holder) { 510 | AnimatorCompatHelper.clearInterpolator(holder.itemView); 511 | endAnimation(holder); 512 | } 513 | 514 | @Override 515 | public boolean isRunning() { 516 | return (!mPendingAdditions.isEmpty() || 517 | !mPendingChanges.isEmpty() || 518 | !mPendingMoves.isEmpty() || 519 | !mPendingRemovals.isEmpty() || 520 | !mMoveAnimations.isEmpty() || 521 | !mRemoveAnimations.isEmpty() || 522 | !mAddAnimations.isEmpty() || 523 | !mChangeAnimations.isEmpty() || 524 | !mMovesList.isEmpty() || 525 | !mAdditionsList.isEmpty() || 526 | !mChangesList.isEmpty()); 527 | } 528 | 529 | /** 530 | * Check the state of currently pending and running animations. If there are none 531 | * pending/running, call {@link #dispatchAnimationsFinished()} to notify any 532 | * listeners. 533 | */ 534 | private void dispatchFinishedWhenDone() { 535 | if (!isRunning()) { 536 | dispatchAnimationsFinished(); 537 | } 538 | } 539 | 540 | @Override 541 | public void endAnimations() { 542 | int count = mPendingMoves.size(); 543 | for (int i = count - 1; i >= 0; i--) { 544 | MoveInfo item = mPendingMoves.get(i); 545 | View view = item.holder.itemView; 546 | ViewCompat.setTranslationY(view, 0); 547 | ViewCompat.setTranslationX(view, 0); 548 | dispatchMoveFinished(item.holder); 549 | mPendingMoves.remove(i); 550 | } 551 | count = mPendingRemovals.size(); 552 | for (int i = count - 1; i >= 0; i--) { 553 | RecyclerView.ViewHolder item = mPendingRemovals.get(i); 554 | dispatchRemoveFinished(item); 555 | mPendingRemovals.remove(i); 556 | } 557 | count = mPendingAdditions.size(); 558 | for (int i = count - 1; i >= 0; i--) { 559 | RecyclerView.ViewHolder item = mPendingAdditions.get(i); 560 | View view = item.itemView; 561 | ViewCompat.setAlpha(view, 1); 562 | dispatchAddFinished(item); 563 | mPendingAdditions.remove(i); 564 | } 565 | count = mPendingChanges.size(); 566 | for (int i = count - 1; i >= 0; i--) { 567 | endChangeAnimationIfNecessary(mPendingChanges.get(i)); 568 | } 569 | mPendingChanges.clear(); 570 | if (!isRunning()) { 571 | return; 572 | } 573 | 574 | int listCount = mMovesList.size(); 575 | for (int i = listCount - 1; i >= 0; i--) { 576 | ArrayList moves = mMovesList.get(i); 577 | count = moves.size(); 578 | for (int j = count - 1; j >= 0; j--) { 579 | MoveInfo moveInfo = moves.get(j); 580 | RecyclerView.ViewHolder item = moveInfo.holder; 581 | View view = item.itemView; 582 | ViewCompat.setTranslationY(view, 0); 583 | ViewCompat.setTranslationX(view, 0); 584 | dispatchMoveFinished(moveInfo.holder); 585 | moves.remove(j); 586 | if (moves.isEmpty()) { 587 | mMovesList.remove(moves); 588 | } 589 | } 590 | } 591 | listCount = mAdditionsList.size(); 592 | for (int i = listCount - 1; i >= 0; i--) { 593 | ArrayList additions = mAdditionsList.get(i); 594 | count = additions.size(); 595 | for (int j = count - 1; j >= 0; j--) { 596 | RecyclerView.ViewHolder item = additions.get(j); 597 | View view = item.itemView; 598 | ViewCompat.setAlpha(view, 1); 599 | dispatchAddFinished(item); 600 | additions.remove(j); 601 | if (additions.isEmpty()) { 602 | mAdditionsList.remove(additions); 603 | } 604 | } 605 | } 606 | listCount = mChangesList.size(); 607 | for (int i = listCount - 1; i >= 0; i--) { 608 | ArrayList changes = mChangesList.get(i); 609 | count = changes.size(); 610 | for (int j = count - 1; j >= 0; j--) { 611 | endChangeAnimationIfNecessary(changes.get(j)); 612 | if (changes.isEmpty()) { 613 | mChangesList.remove(changes); 614 | } 615 | } 616 | } 617 | 618 | cancelAll(mRemoveAnimations); 619 | cancelAll(mMoveAnimations); 620 | cancelAll(mAddAnimations); 621 | cancelAll(mChangeAnimations); 622 | 623 | dispatchAnimationsFinished(); 624 | } 625 | 626 | void cancelAll(List viewHolders) { 627 | for (int i = viewHolders.size() - 1; i >= 0; i--) { 628 | ViewCompat.animate(viewHolders.get(i).itemView).cancel(); 629 | } 630 | } 631 | 632 | /** 633 | * {@inheritDoc} 634 | *

635 | * If the payload list is not empty, DefaultItemAnimator returns true. 636 | * When this is the case: 637 | *

    638 | *
  • If you override {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, both 639 | * ViewHolder arguments will be the same instance. 640 | *
  • 641 | *
  • 642 | * If you are not overriding {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, 643 | * then DefaultItemAnimator will call {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int)} and 644 | * run a move animation instead. 645 | *
  • 646 | *
647 | */ 648 | @Override 649 | public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, 650 | @NonNull List payloads) { 651 | return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); 652 | } 653 | 654 | private static class VpaListenerAdapter implements ViewPropertyAnimatorListener { 655 | @Override 656 | public void onAnimationStart(View view) {} 657 | 658 | @Override 659 | public void onAnimationEnd(View view) {} 660 | 661 | @Override 662 | public void onAnimationCancel(View view) {} 663 | } 664 | } 665 | 666 | --------------------------------------------------------------------------------