├── .gitignore
├── .idea
├── .name
├── compiler.xml
├── copyright
│ └── profiles_settings.xml
├── dictionaries
│ └── Wang.xml
├── encodings.xml
├── gradle.xml
├── misc.xml
├── modules.xml
├── runConfigurations.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── mwang
│ │ └── irregulargridview
│ │ └── ApplicationTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── mwang
│ │ │ └── irregulargridview
│ │ │ ├── BaseAdapter.java
│ │ │ ├── BaseFragment.java
│ │ │ ├── DynamicItemAnimator.java
│ │ │ ├── IrregularLayoutManager.java
│ │ │ ├── MainActivity.java
│ │ │ ├── SimpleAdapter.java
│ │ │ ├── SimpleFragment.java
│ │ │ ├── SimpleImageAdapter.java
│ │ │ └── SimpleImageFragment.java
│ └── res
│ │ ├── drawable-hdpi
│ │ ├── ic_check_circle_white_24dp.png
│ │ ├── ic_delete_black_24dp.png
│ │ └── ic_delete_white_24dp.png
│ │ ├── drawable-mdpi
│ │ ├── ic_check_circle_white_24dp.png
│ │ ├── ic_delete_black_24dp.png
│ │ └── ic_delete_white_24dp.png
│ │ ├── drawable-xhdpi
│ │ ├── ic_check_circle_white_24dp.png
│ │ ├── ic_delete_black_24dp.png
│ │ └── ic_delete_white_24dp.png
│ │ ├── drawable-xxhdpi
│ │ ├── ic_check_circle_white_24dp.png
│ │ ├── ic_delete_black_24dp.png
│ │ └── ic_delete_white_24dp.png
│ │ ├── drawable-xxxhdpi
│ │ ├── ic_check_circle_white_24dp.png
│ │ ├── ic_delete_black_24dp.png
│ │ └── ic_delete_white_24dp.png
│ │ ├── drawable
│ │ ├── bg_empty_photo.xml
│ │ ├── bg_grid_item_text.xml
│ │ └── side_nav_bar.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── app_bar_main.xml
│ │ ├── content_main.xml
│ │ ├── fragment_base.xml
│ │ ├── grid_item_image.xml
│ │ ├── grid_item_text.xml
│ │ └── nav_header_main.xml
│ │ ├── menu
│ │ ├── activity_main_drawer.xml
│ │ ├── appbar_menu.xml
│ │ └── context_menu.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
│ └── mwang
│ └── irregulargridview
│ └── ExampleUnitTest.java
├── build.gradle
├── gif
├── Base.gif
├── Gallery.gif
└── test.gif
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | IrregularGridView
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/dictionaries/Wang.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
24 |
25 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | 1.8
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # IrregularGridView
2 |
3 | An irregular GridView based on the RecyclerView.
4 |
5 | It is composed of child views with 4 kinds of sizes.
6 |
7 | ##Preview
8 |
9 |  
10 |
11 | ##Motivation
12 |
13 | It is similar to the [DynamicCardLayout](https://github.com/dodola/DynamicCardLayout). However, the DynamicCardLayout does not support recycle of views, which may require too much memory.
14 |
15 | ##Function
16 |
17 | - Recycle of views.
18 | - Remove animation.
19 |
20 | ##Usage
21 |
22 |
23 |
24 | Firstly, initialize parameters.
25 |
26 | // n is the number of spans.
27 | IrregularLayoutManager layoutManager = new IrregularLayoutManager(getContext(), n);
28 | recyclerView.setLayoutManager(layoutManager);
29 | recyclerView.setItemAnimator(new DynamicItemAnimator());
30 | Then, you should set the prefered layout size for each view during onBindViewHolder(). In fact, your only need to set the number of spans occupied by the view in each direction. Here is an example.
31 |
32 | /**
33 | * This is an example method to set the size for each child view.
34 | * @param view holder.itemView.
35 | * @param position the adapter position of the holder.
36 | */
37 | private void setViewParams(View v, int position){
38 | IrregularLayoutManager.LayoutParams lp = (IrregularLayoutManager.LayoutParams)v.getLayoutParams();
39 | lp.widthNum = yourPreferedWidthSpanNum;
40 | lp.heightNum = yourPreferedHeightSpanNum;
41 | }
42 |
43 | The IrregularLayoutManager will "try" to layout child views with the sizes you set. The child view may be shrunk in the horizontal direction due to space limit.
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/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.mwang.irregulargridview"
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 | testCompile 'junit:junit:4.12'
25 | compile 'com.android.support:appcompat-v7:23.1.1'
26 | compile 'com.android.support:recyclerview-v7:23.1.1'
27 | compile 'com.android.support:design:23.1.1'
28 | compile 'com.android.support:support-v4:23.1.1'
29 | compile 'com.github.bumptech.glide:glide:3.7.0'
30 | }
31 |
--------------------------------------------------------------------------------
/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 F:\AndroidSDK\android-sdks/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/mwang/irregulargridview/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.mwang.irregulargridview;
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 |
3 |
4 |
5 |
6 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/mwang/irregulargridview/BaseAdapter.java:
--------------------------------------------------------------------------------
1 | package com.mwang.irregulargridview;
2 |
3 | import android.content.Context;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.View;
6 |
7 | import java.util.ArrayList;
8 | import java.util.Collections;
9 | import java.util.ListIterator;
10 |
11 | public abstract class BaseAdapter extends RecyclerView.Adapter{
12 |
13 | protected Context mContext;
14 | protected RecyclerView recyclerView;
15 | private ArrayList mSelectedDataIndexSet;
16 | private OnItemClickLitener mOnItemClickLitener;
17 |
18 | public BaseAdapter(Context context, RecyclerView rec){
19 | mContext = context;
20 | recyclerView = rec;
21 | mSelectedDataIndexSet = new ArrayList<>();
22 | }
23 |
24 | @Override
25 | public void onBindViewHolder(final VH holder, int position){
26 |
27 | if(mSelectedDataIndexSet != null && mSelectedDataIndexSet.contains(new Integer(position)) ){
28 | updateSelectedItem(holder);
29 | }else{
30 | updateUnselectedItem(holder);
31 | }
32 |
33 | if(mOnItemClickLitener != null){
34 | holder.itemView.setOnClickListener(new View.OnClickListener() {
35 | @Override
36 | public void onClick(View v) {
37 | mOnItemClickLitener.onItemClick(holder, holder.getAdapterPosition());
38 | }
39 | });
40 | holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
41 | @Override
42 | public boolean onLongClick(View v) {
43 | return mOnItemClickLitener.onItemLongClick(holder, holder.getAdapterPosition());
44 | }
45 | });
46 | }
47 | }
48 |
49 | public static class VH extends RecyclerView.ViewHolder {
50 | public VH(View v){
51 | super(v);
52 | }
53 | }
54 |
55 | public interface OnItemClickLitener {
56 | void onItemClick(VH holder, int position);
57 | boolean onItemLongClick(VH holder, int position);
58 | }
59 |
60 | public void setOnItemClickLitener(OnItemClickLitener mOnItemClickLitener)
61 | {
62 | this.mOnItemClickLitener = mOnItemClickLitener;
63 | }
64 |
65 | public ArrayList getSelectedItems() {
66 | ArrayList arrayList = new ArrayList(mSelectedDataIndexSet);
67 | if (mSelectedDataIndexSet != null && !mSelectedDataIndexSet.isEmpty()){
68 | Collections.sort(mSelectedDataIndexSet);
69 | Collections.copy(arrayList, mSelectedDataIndexSet);
70 | mSelectedDataIndexSet.clear();
71 | }
72 | return arrayList;
73 | }
74 |
75 | public void resetSelectedItems(){
76 | if(mSelectedDataIndexSet != null){
77 | ListIterator listIterator = mSelectedDataIndexSet.listIterator();
78 | while(listIterator.hasNext()){
79 | int position = listIterator.next();
80 | VH holder = (VH)recyclerView.findViewHolderForAdapterPosition(position);
81 | if(holder != null) {
82 | updateUnselectedItem(holder);
83 | }else{
84 | notifyItemChanged(position);
85 | }
86 | }
87 | mSelectedDataIndexSet.clear();
88 | }
89 | }
90 |
91 | public void reverseSelect(VH holder, int position){
92 | if(mSelectedDataIndexSet.contains(position)){
93 | mSelectedDataIndexSet.remove(new Integer(position));
94 | updateUnselectedItem(holder);
95 | }else if(!mSelectedDataIndexSet.contains(position)){
96 | mSelectedDataIndexSet.add(position);
97 | updateSelectedItem(holder);
98 | }
99 | }
100 |
101 | public void selectItem(VH holder, int position){
102 | if(!mSelectedDataIndexSet.contains(position)){
103 | mSelectedDataIndexSet.add(position);
104 | updateSelectedItem(holder);
105 | }
106 | }
107 |
108 | protected abstract void updateSelectedItem(VH holder);
109 |
110 | protected abstract void updateUnselectedItem(VH holder);
111 |
112 | }
113 |
--------------------------------------------------------------------------------
/app/src/main/java/com/mwang/irregulargridview/BaseFragment.java:
--------------------------------------------------------------------------------
1 | package com.mwang.irregulargridview;
2 |
3 | import android.os.Bundle;
4 | import android.support.v4.app.Fragment;
5 | import android.support.v7.app.AppCompatActivity;
6 | import android.support.v7.view.ActionMode;
7 | import android.view.Menu;
8 | import android.view.MenuInflater;
9 | import android.view.MenuItem;
10 |
11 | public abstract class BaseFragment extends Fragment implements ActionMode.Callback{
12 |
13 | protected ActionMode mActionMode;
14 |
15 | @Override
16 | public void onCreate(Bundle savedInstanceState) {
17 | super.onCreate(savedInstanceState);
18 | initData();
19 | setHasOptionsMenu(true);
20 | }
21 |
22 | @Override
23 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
24 | super.onCreateOptionsMenu(menu, inflater);
25 | inflater.inflate(R.menu.appbar_menu, menu);
26 | }
27 |
28 | @Override
29 | public boolean onOptionsItemSelected(MenuItem item) {
30 | switch(item.getItemId()){
31 | case R.id.action_select:
32 | mActionMode = ((AppCompatActivity)getContext())
33 | .startSupportActionMode(BaseFragment.this);
34 | return true;
35 | default:
36 | return super.onOptionsItemSelected(item);
37 | }
38 | }
39 |
40 |
41 | // Called when the action mode is created; startActionMode() was called
42 | @Override
43 | public boolean onCreateActionMode(ActionMode mode, Menu menu) {
44 | // Inflate a menu resource providing context menu items
45 | MenuInflater inflater = mode.getMenuInflater();
46 | inflater.inflate(R.menu.context_menu, menu);
47 | return true;
48 | }
49 |
50 | // Called each time the action mode is shown. Always called after onCreateActionMode, but
51 | // may be called multiple times if the mode is invalidated.
52 | @Override
53 | public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
54 | return false; // Return false if nothing is done
55 | }
56 |
57 | // Called when the user selects a contextual menu item
58 | @Override
59 | public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
60 | switch (item.getItemId()) {
61 | case R.id.menu_delete:
62 | deleteSelectedItems();
63 | mode.finish(); // Action picked, so close the CAB
64 | return true;
65 | default:
66 | return false;
67 | }
68 | }
69 |
70 | // Called when the user exits the action mode
71 | @Override
72 | public void onDestroyActionMode(ActionMode mode) {
73 | resetSelectedItems();
74 | mActionMode = null;
75 | }
76 |
77 | public abstract void initData();
78 |
79 | public abstract void deleteSelectedItems();
80 |
81 | public abstract void resetSelectedItems();
82 |
83 | }
84 |
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/mwang/irregulargridview/DynamicItemAnimator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.mwang.irregulargridview;
17 |
18 | import android.support.annotation.NonNull;
19 | import android.support.annotation.Nullable;
20 | import android.support.v4.animation.AnimatorCompatHelper;
21 | import android.support.v4.view.ViewCompat;
22 | import android.support.v4.view.ViewPropertyAnimatorCompat;
23 | import android.support.v4.view.ViewPropertyAnimatorListener;
24 | import android.support.v7.widget.DefaultItemAnimator;
25 | import android.support.v7.widget.RecyclerView;
26 | import android.support.v7.widget.SimpleItemAnimator;
27 | import android.view.View;
28 |
29 | import java.util.ArrayList;
30 | import java.util.List;
31 |
32 | /**
33 | * This is mainly based on the DefaultItemAnimator.
34 | * I just add the scale animation for my IrregularRecyclerView.
35 | */
36 | public class DynamicItemAnimator extends SimpleItemAnimator {
37 |
38 | private static final boolean DEBUG = false;
39 |
40 | private ArrayList mPendingRemovals = new ArrayList<>();
41 | private ArrayList mPendingAdditions = new ArrayList<>();
42 | private ArrayList mPendingMoves = new ArrayList<>();
43 | private ArrayList mPendingChanges = new ArrayList<>();
44 |
45 | private ArrayList> mAdditionsList = new ArrayList<>();
46 | private ArrayList> mMovesList = new ArrayList<>();
47 | private ArrayList> mChangesList = new ArrayList<>();
48 |
49 | private ArrayList mAddAnimations = new ArrayList<>();
50 | private ArrayList mMoveAnimations = new ArrayList<>();
51 | private ArrayList mRemoveAnimations = new ArrayList<>();
52 | private ArrayList mChangeAnimations = new ArrayList<>();
53 |
54 | private static class MoveInfo {
55 | public RecyclerView.ViewHolder holder;
56 | public int fromX, fromY, toX, toY, fromWidth, fromHeight, toWidth, toHeight;
57 |
58 | private MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY, int fromWidth, int fromHeight, int toWidth, int toHeight) {
59 | this.holder = holder;
60 | this.fromX = fromX;
61 | this.fromY = fromY;
62 | this.toX = toX;
63 | this.toY = toY;
64 | this.fromWidth = fromWidth;
65 | this.fromHeight = fromHeight;
66 | this.toWidth = toWidth;
67 | this.toHeight = toHeight;
68 | }
69 | }
70 |
71 | private static class ChangeInfo {
72 | public RecyclerView.ViewHolder oldHolder, newHolder;
73 | public int fromX, fromY, toX, toY, fromWidth, fromHeight, toWidth, toHeight;
74 | private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) {
75 | this.oldHolder = oldHolder;
76 | this.newHolder = newHolder;
77 | }
78 |
79 | private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
80 | int fromX, int fromY, int toX, int toY, int fromWidth, int fromHeight, int toWidth, int toHeight) {
81 | this(oldHolder, newHolder);
82 | this.fromX = fromX;
83 | this.fromY = fromY;
84 | this.toX = toX;
85 | this.toY = toY;
86 | this.fromWidth = fromWidth;
87 | this.fromHeight = fromHeight;
88 | this.toWidth = toWidth;
89 | this.toHeight = toHeight;
90 | }
91 |
92 | @Override
93 | public String toString() {
94 | return "ChangeInfo{" +
95 | "oldHolder=" + oldHolder +
96 | ", newHolder=" + newHolder +
97 | ", fromX=" + fromX +
98 | ", fromY=" + fromY +
99 | ", toX=" + toX +
100 | ", toY=" + toY +
101 | ", fromWidth=" + fromWidth +
102 | ", fromHeight=" + fromHeight +
103 | ", toWidth=" + toWidth +
104 | ", toHeight=" + toHeight +
105 | '}';
106 | }
107 | }
108 |
109 |
110 |
111 | @Override
112 | public boolean animateDisappearance(RecyclerView.ViewHolder viewHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
113 | int oldLeft = preLayoutInfo.left;
114 | int oldRight = preLayoutInfo.right;
115 | int oldTop = preLayoutInfo.top;
116 | int oldBottom = preLayoutInfo.bottom;
117 | View disappearingItemView = viewHolder.itemView;
118 | RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) disappearingItemView.getLayoutParams();
119 | int newLeft = postLayoutInfo == null ? disappearingItemView.getLeft() : postLayoutInfo.left;
120 | int newRight = postLayoutInfo == null ? disappearingItemView.getRight() : postLayoutInfo.right;
121 | int newTop = postLayoutInfo == null ? disappearingItemView.getTop() : postLayoutInfo.top;
122 | int newBottom = postLayoutInfo == null ? disappearingItemView.getBottom() : postLayoutInfo.bottom;
123 | if (!lp.isItemRemoved() && (oldLeft != newLeft || oldRight != newRight || oldTop != newTop || oldBottom != newBottom)) {
124 | disappearingItemView.layout(newLeft, newTop,
125 | newLeft + disappearingItemView.getWidth(),
126 | newTop + disappearingItemView.getHeight());
127 | return animateMove(viewHolder, oldLeft, oldTop, newLeft, newTop, oldRight - oldLeft, oldBottom - oldTop, newRight - newLeft, newBottom - newTop);
128 | } else {
129 | return animateRemove(viewHolder);
130 | }
131 | }
132 |
133 | @Override
134 | public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder,
135 | @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
136 | if (preLayoutInfo != null){
137 | int fromWidth = preLayoutInfo.right - preLayoutInfo.left;
138 | int fromHeight = preLayoutInfo.bottom - preLayoutInfo.top;
139 | int toWidth = postLayoutInfo.right - postLayoutInfo.left;
140 | int toHeight = postLayoutInfo.bottom - postLayoutInfo.top;
141 | if(preLayoutInfo.left != postLayoutInfo.left
142 | || preLayoutInfo.top != postLayoutInfo.top
143 | || fromWidth != toWidth
144 | || fromHeight != toHeight) {
145 | // slide items in if before/after locations differ
146 | return animateMove(viewHolder, preLayoutInfo.left, preLayoutInfo.top,
147 | postLayoutInfo.left, postLayoutInfo.top, fromWidth, fromHeight, toWidth, toHeight);
148 | }
149 | }
150 | return animateAdd(viewHolder);
151 |
152 | }
153 |
154 | @Override
155 | public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder,
156 | @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
157 | int fromWidth = preInfo.right - preInfo.left;
158 | int fromHeight = preInfo.bottom - preInfo.top;
159 | int toWidth = postInfo.right - postInfo.left;
160 | int toHeight = postInfo.bottom - postInfo.top;
161 | if (preInfo.left != postInfo.left || preInfo.top != postInfo.top || fromWidth != toWidth || fromHeight != toHeight) {
162 | return animateMove(viewHolder,
163 | preInfo.left, preInfo.top, postInfo.left, postInfo.top, fromWidth, fromHeight, toWidth, toHeight);
164 | }
165 | dispatchMoveFinished(viewHolder);
166 | return false;
167 | }
168 |
169 | @Override
170 | public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull RecyclerView.ViewHolder newHolder,
171 | @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
172 | int fromWidth = preInfo.right - preInfo.left;
173 | int fromHeight = preInfo.bottom - preInfo.top;
174 | int toWidth = postInfo.right - postInfo.left;
175 | int toHeight = postInfo.bottom - postInfo.top;
176 |
177 | // Bug notation.
178 | // The code in the SimpleItemAnimator is as follows.
179 | // Note that shouldIgnore() cannot be accessed out of the package.
180 | // And I cannot find any replaceable method.
181 |
182 | // if (newHolder.shouldIgnore()) {
183 | // toLeft = preInfo.left;
184 | // toTop = preInfo.top;
185 | // } else {
186 | // toLeft = postInfo.left;
187 | // toTop = postInfo.top;
188 | // }
189 |
190 | return animateChange(oldHolder, newHolder, preInfo.left, preInfo.top, postInfo.left, postInfo.top, fromWidth, fromHeight, toWidth, toHeight);
191 | }
192 |
193 |
194 | @Override
195 | public void runPendingAnimations() {
196 | boolean removalsPending = !mPendingRemovals.isEmpty();
197 | boolean movesPending = !mPendingMoves.isEmpty();
198 | boolean changesPending = !mPendingChanges.isEmpty();
199 | boolean additionsPending = !mPendingAdditions.isEmpty();
200 | if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
201 | // nothing to animate
202 | return;
203 | }
204 | // First, remove stuff
205 | for (RecyclerView.ViewHolder holder : mPendingRemovals) {
206 | animateRemoveImpl(holder);
207 | }
208 | mPendingRemovals.clear();
209 | // Next, move stuff
210 | if (movesPending) {
211 | final ArrayList moves = new ArrayList<>();
212 | moves.addAll(mPendingMoves);
213 | mMovesList.add(moves);
214 | mPendingMoves.clear();
215 | Runnable mover = new Runnable() {
216 | @Override
217 | public void run() {
218 | for (MoveInfo moveInfo : moves) {
219 | animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, moveInfo.toX, moveInfo.toY,
220 | moveInfo.fromWidth, moveInfo.fromHeight, moveInfo.toWidth, moveInfo.toHeight);
221 | }
222 | moves.clear();
223 | mMovesList.remove(moves);
224 | }
225 | };
226 | if (removalsPending) {
227 | View view = moves.get(0).holder.itemView;
228 | ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
229 | } else {
230 | mover.run();
231 | }
232 | }
233 | // Next, change stuff, to run in parallel with move animations
234 | if (changesPending) {
235 | final ArrayList changes = new ArrayList<>();
236 | changes.addAll(mPendingChanges);
237 | mChangesList.add(changes);
238 | mPendingChanges.clear();
239 | Runnable changer = new Runnable() {
240 | @Override
241 | public void run() {
242 | for (ChangeInfo change : changes) {
243 | animateChangeImpl(change);
244 | }
245 | changes.clear();
246 | mChangesList.remove(changes);
247 | }
248 | };
249 | if (removalsPending) {
250 | RecyclerView.ViewHolder holder = changes.get(0).oldHolder;
251 | ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
252 | } else {
253 | changer.run();
254 | }
255 | }
256 | // Next, add stuff
257 | if (additionsPending) {
258 | final ArrayList additions = new ArrayList<>();
259 | additions.addAll(mPendingAdditions);
260 | mAdditionsList.add(additions);
261 | mPendingAdditions.clear();
262 | Runnable adder = new Runnable() {
263 | public void run() {
264 | for (RecyclerView.ViewHolder holder : additions) {
265 | animateAddImpl(holder);
266 | }
267 | additions.clear();
268 | mAdditionsList.remove(additions);
269 | }
270 | };
271 | if (removalsPending || movesPending || changesPending) {
272 | long removeDuration = removalsPending ? getRemoveDuration() : 0;
273 | long moveDuration = movesPending ? getMoveDuration() : 0;
274 | long changeDuration = changesPending ? getChangeDuration() : 0;
275 | long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
276 | View view = additions.get(0).itemView;
277 | ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
278 | } else {
279 | adder.run();
280 | }
281 | }
282 | }
283 |
284 | @Override
285 | public boolean animateRemove(final RecyclerView.ViewHolder holder) {
286 | resetAnimation(holder);
287 | mPendingRemovals.add(holder);
288 | return true;
289 | }
290 |
291 | private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
292 | final View view = holder.itemView;
293 | final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
294 | mRemoveAnimations.add(holder);
295 | animation.setDuration(getRemoveDuration())
296 | .alpha(0).setListener(new VpaListenerAdapter() {
297 | @Override
298 | public void onAnimationStart(View view) {
299 | dispatchRemoveStarting(holder);
300 | }
301 |
302 | @Override
303 | public void onAnimationEnd(View view) {
304 | animation.setListener(null);
305 | ViewCompat.setAlpha(view, 1);
306 | dispatchRemoveFinished(holder);
307 | mRemoveAnimations.remove(holder);
308 | dispatchFinishedWhenDone();
309 | }
310 | }).start();
311 | }
312 |
313 | @Override
314 | public boolean animateAdd(final RecyclerView.ViewHolder holder) {
315 | resetAnimation(holder);
316 | ViewCompat.setAlpha(holder.itemView, 0);
317 | mPendingAdditions.add(holder);
318 | return true;
319 | }
320 |
321 | private void animateAddImpl(final RecyclerView.ViewHolder holder) {
322 | final View view = holder.itemView;
323 | final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
324 | mAddAnimations.add(holder);
325 | animation.alpha(1).setDuration(getAddDuration()).
326 | setListener(new VpaListenerAdapter() {
327 | @Override
328 | public void onAnimationStart(View view) {
329 | dispatchAddStarting(holder);
330 | }
331 |
332 | @Override
333 | public void onAnimationCancel(View view) {
334 | ViewCompat.setAlpha(view, 1);
335 | }
336 |
337 | @Override
338 | public void onAnimationEnd(View view) {
339 | animation.setListener(null);
340 | dispatchAddFinished(holder);
341 | mAddAnimations.remove(holder);
342 | dispatchFinishedWhenDone();
343 | }
344 | }).start();
345 | }
346 |
347 | @Override
348 | public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY){
349 | return animateMove(holder, fromX, fromY, toX, toY, 1, 1, 1, 1);
350 | }
351 |
352 | public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY,
353 | int fromWidth, int fromHeight, int toWidth, int toHeight) {
354 | final View view = holder.itemView;
355 | fromX += ViewCompat.getTranslationX(holder.itemView);
356 | fromY += ViewCompat.getTranslationY(holder.itemView);
357 | resetAnimation(holder);
358 | int deltaX = toX - fromX;
359 | int deltaY = toY - fromY;
360 | float scaleX = (float)toWidth / fromWidth;
361 | float scaleY = (float)toHeight / fromHeight;
362 | if(scaleX == 0) scaleX = 1;
363 | if(scaleY == 0) scaleY = 1;
364 | if (deltaX == 0 && deltaY == 0 && scaleX == 1 && scaleY == 1) {
365 | dispatchMoveFinished(holder);
366 | return false;
367 | }
368 | view.setPivotX(0);
369 | view.setPivotY(0);
370 | if(scaleX != 1){
371 | ViewCompat.setScaleX(view, 1 / scaleX);
372 | }
373 | if(scaleY != 1){
374 | ViewCompat.setScaleY(view, 1 / scaleY);
375 | }
376 | if (deltaX != 0) {
377 | ViewCompat.setTranslationX(view, -deltaX);
378 | }
379 | if (deltaY != 0) {
380 | ViewCompat.setTranslationY(view, -deltaY);
381 | }
382 |
383 | mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY, fromWidth, fromHeight, toWidth, toHeight));
384 | return true;
385 | }
386 |
387 | private void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY, int fromWidth, int fromHeight, int toWidth, int toHeight) {
388 | final View view = holder.itemView;
389 | final int deltaX = toX - fromX;
390 | final int deltaY = toY - fromY;
391 | final float scaleX = toWidth == 0 ? 1 : (float)toWidth / fromWidth;
392 | final float scaleY = toHeight == 0 ? 1 : (float)toHeight / fromHeight;
393 | if (deltaX != 0) {
394 | ViewCompat.animate(view).translationX(0);
395 | }
396 | if (deltaY != 0) {
397 | ViewCompat.animate(view).translationY(0);
398 | }
399 | if(scaleX != 1){
400 | ViewCompat.animate(view).scaleX(1);
401 | }
402 | if(scaleY != 1){
403 | ViewCompat.animate(view).scaleY(1);
404 | }
405 |
406 | // TODO: make EndActions end listeners instead, since end actions aren't called when
407 | // vpas are canceled (and can't end them. why?)
408 | // need listener functionality in VPACompat for this. Ick.
409 | final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
410 | mMoveAnimations.add(holder);
411 | animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
412 | @Override
413 | public void onAnimationStart(View view) {
414 | dispatchMoveStarting(holder);
415 | }
416 |
417 | @Override
418 | public void onAnimationCancel(View view) {
419 | if (deltaX != 0) {
420 | ViewCompat.setTranslationX(view, 0);
421 | }
422 | if (deltaY != 0) {
423 | ViewCompat.setTranslationY(view, 0);
424 | }
425 | if (scaleX != 1) {
426 | ViewCompat.setScaleX(view, 1);
427 | }
428 | if (scaleY != 1) {
429 | ViewCompat.setScaleY(view, 1);
430 | }
431 | }
432 |
433 | @Override
434 | public void onAnimationEnd(View view) {
435 | animation.setListener(null);
436 | dispatchMoveFinished(holder);
437 | mMoveAnimations.remove(holder);
438 | dispatchFinishedWhenDone();
439 | }
440 | }).start();
441 | }
442 |
443 |
444 | @Override
445 | public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
446 | int fromX, int fromY, int toX, int toY){
447 | return animateChange(oldHolder, newHolder, fromX, fromY, toX, toY, 1, 1, 1, 1);
448 | }
449 |
450 | public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
451 | int fromX, int fromY, int toX, int toY, int fromWidth, int fromHeight, int toWidth, int toHeight) {
452 | if (oldHolder == newHolder) {
453 | // Don't know how to run change animations when the same view holder is re-used.
454 | // run a move animation to handle position changes.
455 | return animateMove(oldHolder, fromX, fromY, toX, toY, fromWidth, fromHeight, toWidth, toHeight);
456 | }
457 | final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
458 | final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
459 | final float prevScaleX = ViewCompat.getScaleX(oldHolder.itemView);
460 | final float prevScaleY = ViewCompat.getScaleY(oldHolder.itemView);
461 | final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
462 | resetAnimation(oldHolder);
463 | int deltaX = (int) (toX - fromX - prevTranslationX);
464 | int deltaY = (int) (toY - fromY - prevTranslationY);
465 | float scaleX = (float)toWidth / fromWidth;
466 | float scaleY = (float)toHeight / fromHeight;
467 | if(scaleX == 0) scaleX = 1;
468 | if(scaleY == 0) scaleY = 1;
469 | // recover prev translation state after ending animation
470 | ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
471 | ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
472 |
473 | ViewCompat.setScaleX(oldHolder.itemView, prevScaleX);
474 | ViewCompat.setScaleY(oldHolder.itemView, prevScaleY);
475 | ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
476 | if (newHolder != null) {
477 | // carry over translation values
478 | resetAnimation(newHolder);
479 | ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
480 | ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
481 | newHolder.itemView.setPivotX(0);
482 | newHolder.itemView.setPivotY(0);
483 | ViewCompat.setScaleX(newHolder.itemView, 1 / scaleX);
484 | ViewCompat.setScaleY(newHolder.itemView, 1 / scaleY);
485 | ViewCompat.setAlpha(newHolder.itemView, 0);
486 | }
487 | mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY, fromWidth, fromHeight, toWidth, toHeight));
488 | return true;
489 | }
490 |
491 | private void animateChangeImpl(final ChangeInfo changeInfo) {
492 | final RecyclerView.ViewHolder holder = changeInfo.oldHolder;
493 | final View view = holder == null ? null : holder.itemView;
494 | final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
495 | final View newView = newHolder != null ? newHolder.itemView : null;
496 | if (view != null) {
497 | final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
498 | getChangeDuration());
499 | mChangeAnimations.add(changeInfo.oldHolder);
500 | oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
501 | oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
502 | float scaleX = (float)changeInfo.toWidth / changeInfo.fromWidth;
503 | float scaleY = (float)changeInfo.toHeight / changeInfo.fromHeight;
504 | if(scaleX == 0) scaleX = 1;
505 | if(scaleY == 0) scaleY = 1;
506 |
507 | oldViewAnim.scaleX(scaleX);
508 | oldViewAnim.scaleY(scaleY);
509 | oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
510 | @Override
511 | public void onAnimationStart(View view) {
512 | dispatchChangeStarting(changeInfo.oldHolder, true);
513 | }
514 |
515 | @Override
516 | public void onAnimationEnd(View view) {
517 | oldViewAnim.setListener(null);
518 | ViewCompat.setAlpha(view, 1);
519 | ViewCompat.setTranslationX(view, 0);
520 | ViewCompat.setTranslationY(view, 0);
521 | ViewCompat.setScaleX(view, 1);
522 | ViewCompat.setScaleY(view, 1);
523 | dispatchChangeFinished(changeInfo.oldHolder, true);
524 | mChangeAnimations.remove(changeInfo.oldHolder);
525 | dispatchFinishedWhenDone();
526 | }
527 | }).start();
528 | }
529 | if (newView != null) {
530 | final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
531 | mChangeAnimations.add(changeInfo.newHolder);
532 |
533 | newViewAnimation.translationX(0).translationY(0).
534 | scaleX(1).scaleY(1).setDuration(getChangeDuration()).
535 | alpha(1).setListener(new VpaListenerAdapter() {
536 | @Override
537 | public void onAnimationStart(View view) {
538 | dispatchChangeStarting(changeInfo.newHolder, false);
539 | }
540 | @Override
541 | public void onAnimationEnd(View view) {
542 | newViewAnimation.setListener(null);
543 | ViewCompat.setAlpha(newView, 1);
544 | ViewCompat.setTranslationX(newView, 0);
545 | ViewCompat.setTranslationY(newView, 0);
546 | ViewCompat.setScaleX(view, 1);
547 | ViewCompat.setScaleY(view, 1);
548 | dispatchChangeFinished(changeInfo.newHolder, false);
549 | mChangeAnimations.remove(changeInfo.newHolder);
550 | dispatchFinishedWhenDone();
551 | }
552 | }).start();
553 | }
554 | }
555 |
556 | private void endChangeAnimation(List infoList, RecyclerView.ViewHolder item) {
557 | for (int i = infoList.size() - 1; i >= 0; i--) {
558 | ChangeInfo changeInfo = infoList.get(i);
559 | if (endChangeAnimationIfNecessary(changeInfo, item)) {
560 | if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
561 | infoList.remove(changeInfo);
562 | }
563 | }
564 | }
565 | }
566 |
567 | private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
568 | if (changeInfo.oldHolder != null) {
569 | endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
570 | }
571 | if (changeInfo.newHolder != null) {
572 | endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
573 | }
574 | }
575 | private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) {
576 | boolean oldItem = false;
577 | if (changeInfo.newHolder == item) {
578 | changeInfo.newHolder = null;
579 | } else if (changeInfo.oldHolder == item) {
580 | changeInfo.oldHolder = null;
581 | oldItem = true;
582 | } else {
583 | return false;
584 | }
585 | ViewCompat.setAlpha(item.itemView, 1);
586 | ViewCompat.setTranslationX(item.itemView, 0);
587 | ViewCompat.setTranslationY(item.itemView, 0);
588 | ViewCompat.setScaleX(item.itemView, 1);
589 | ViewCompat.setScaleY(item.itemView, 1);
590 | dispatchChangeFinished(item, oldItem);
591 | return true;
592 | }
593 |
594 | @Override
595 | public void endAnimation(RecyclerView.ViewHolder item) {
596 | final View view = item.itemView;
597 | // this will trigger end callback which should set properties to their target values.
598 | ViewCompat.animate(view).cancel();
599 | // TODO if some other animations are chained to end, how do we cancel them as well?
600 | for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
601 | MoveInfo moveInfo = mPendingMoves.get(i);
602 | if (moveInfo.holder == item) {
603 | ViewCompat.setTranslationY(view, 0);
604 | ViewCompat.setTranslationX(view, 0);
605 | ViewCompat.setScaleX(view, 1);
606 | ViewCompat.setScaleY(view, 1);
607 | dispatchMoveFinished(item);
608 | mPendingMoves.remove(i);
609 | }
610 | }
611 | endChangeAnimation(mPendingChanges, item);
612 | if (mPendingRemovals.remove(item)) {
613 | ViewCompat.setAlpha(view, 1);
614 | dispatchRemoveFinished(item);
615 | }
616 | if (mPendingAdditions.remove(item)) {
617 | ViewCompat.setAlpha(view, 1);
618 | dispatchAddFinished(item);
619 | }
620 |
621 | for (int i = mChangesList.size() - 1; i >= 0; i--) {
622 | ArrayList changes = mChangesList.get(i);
623 | endChangeAnimation(changes, item);
624 | if (changes.isEmpty()) {
625 | mChangesList.remove(i);
626 | }
627 | }
628 | for (int i = mMovesList.size() - 1; i >= 0; i--) {
629 | ArrayList moves = mMovesList.get(i);
630 | for (int j = moves.size() - 1; j >= 0; j--) {
631 | MoveInfo moveInfo = moves.get(j);
632 | if (moveInfo.holder == item) {
633 | ViewCompat.setTranslationY(view, 0);
634 | ViewCompat.setTranslationX(view, 0);
635 | ViewCompat.setScaleX(view, 1);
636 | ViewCompat.setScaleY(view, 1);
637 | dispatchMoveFinished(item);
638 | moves.remove(j);
639 | if (moves.isEmpty()) {
640 | mMovesList.remove(i);
641 | }
642 | break;
643 | }
644 | }
645 | }
646 | for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
647 | ArrayList additions = mAdditionsList.get(i);
648 | if (additions.remove(item)) {
649 | ViewCompat.setAlpha(view, 1);
650 | dispatchAddFinished(item);
651 | if (additions.isEmpty()) {
652 | mAdditionsList.remove(i);
653 | }
654 | }
655 | }
656 |
657 | // animations should be ended by the cancel above.
658 | //noinspection PointlessBooleanExpression,ConstantConditions
659 | if (mRemoveAnimations.remove(item) && DEBUG) {
660 | throw new IllegalStateException("after animation is cancelled, item should not be in "
661 | + "mRemoveAnimations list");
662 | }
663 |
664 | //noinspection PointlessBooleanExpression,ConstantConditions
665 | if (mAddAnimations.remove(item) && DEBUG) {
666 | throw new IllegalStateException("after animation is cancelled, item should not be in "
667 | + "mAddAnimations list");
668 | }
669 |
670 | //noinspection PointlessBooleanExpression,ConstantConditions
671 | if (mChangeAnimations.remove(item) && DEBUG) {
672 | throw new IllegalStateException("after animation is cancelled, item should not be in "
673 | + "mChangeAnimations list");
674 | }
675 |
676 | //noinspection PointlessBooleanExpression,ConstantConditions
677 | if (mMoveAnimations.remove(item) && DEBUG) {
678 | throw new IllegalStateException("after animation is cancelled, item should not be in "
679 | + "mMoveAnimations list");
680 | }
681 | dispatchFinishedWhenDone();
682 | }
683 |
684 | private void resetAnimation(RecyclerView.ViewHolder holder) {
685 | AnimatorCompatHelper.clearInterpolator(holder.itemView);
686 | endAnimation(holder);
687 | }
688 |
689 | @Override
690 | public boolean isRunning() {
691 | return (!mPendingAdditions.isEmpty() ||
692 | !mPendingChanges.isEmpty() ||
693 | !mPendingMoves.isEmpty() ||
694 | !mPendingRemovals.isEmpty() ||
695 | !mMoveAnimations.isEmpty() ||
696 | !mRemoveAnimations.isEmpty() ||
697 | !mAddAnimations.isEmpty() ||
698 | !mChangeAnimations.isEmpty() ||
699 | !mMovesList.isEmpty() ||
700 | !mAdditionsList.isEmpty() ||
701 | !mChangesList.isEmpty());
702 | }
703 |
704 | /**
705 | * Check the state of currently pending and running animations. If there are none
706 | * pending/running, call {@link #dispatchAnimationsFinished()} to notify any
707 | * listeners.
708 | */
709 | private void dispatchFinishedWhenDone() {
710 | if (!isRunning()) {
711 | dispatchAnimationsFinished();
712 | }
713 | }
714 |
715 | @Override
716 | public void endAnimations() {
717 | int count = mPendingMoves.size();
718 | for (int i = count - 1; i >= 0; i--) {
719 | MoveInfo item = mPendingMoves.get(i);
720 | View view = item.holder.itemView;
721 | ViewCompat.setTranslationY(view, 0);
722 | ViewCompat.setTranslationX(view, 0);
723 | ViewCompat.setScaleX(view, 1);
724 | ViewCompat.setScaleY(view, 1);
725 | dispatchMoveFinished(item.holder);
726 | mPendingMoves.remove(i);
727 | }
728 | count = mPendingRemovals.size();
729 | for (int i = count - 1; i >= 0; i--) {
730 | RecyclerView.ViewHolder item = mPendingRemovals.get(i);
731 | dispatchRemoveFinished(item);
732 | mPendingRemovals.remove(i);
733 | }
734 | count = mPendingAdditions.size();
735 | for (int i = count - 1; i >= 0; i--) {
736 | RecyclerView.ViewHolder item = mPendingAdditions.get(i);
737 | View view = item.itemView;
738 | ViewCompat.setAlpha(view, 1);
739 | dispatchAddFinished(item);
740 | mPendingAdditions.remove(i);
741 | }
742 | count = mPendingChanges.size();
743 | for (int i = count - 1; i >= 0; i--) {
744 | endChangeAnimationIfNecessary(mPendingChanges.get(i));
745 | }
746 | mPendingChanges.clear();
747 | if (!isRunning()) {
748 | return;
749 | }
750 |
751 | int listCount = mMovesList.size();
752 | for (int i = listCount - 1; i >= 0; i--) {
753 | ArrayList moves = mMovesList.get(i);
754 | count = moves.size();
755 | for (int j = count - 1; j >= 0; j--) {
756 | MoveInfo moveInfo = moves.get(j);
757 | RecyclerView.ViewHolder item = moveInfo.holder;
758 | View view = item.itemView;
759 | ViewCompat.setTranslationY(view, 0);
760 | ViewCompat.setTranslationX(view, 0);
761 | ViewCompat.setScaleX(view, 1);
762 | ViewCompat.setScaleY(view, 1);
763 | dispatchMoveFinished(moveInfo.holder);
764 | moves.remove(j);
765 | if (moves.isEmpty()) {
766 | mMovesList.remove(moves);
767 | }
768 | }
769 | }
770 | listCount = mAdditionsList.size();
771 | for (int i = listCount - 1; i >= 0; i--) {
772 | ArrayList additions = mAdditionsList.get(i);
773 | count = additions.size();
774 | for (int j = count - 1; j >= 0; j--) {
775 | RecyclerView.ViewHolder item = additions.get(j);
776 | View view = item.itemView;
777 | ViewCompat.setAlpha(view, 1);
778 | dispatchAddFinished(item);
779 | additions.remove(j);
780 | if (additions.isEmpty()) {
781 | mAdditionsList.remove(additions);
782 | }
783 | }
784 | }
785 | listCount = mChangesList.size();
786 | for (int i = listCount - 1; i >= 0; i--) {
787 | ArrayList changes = mChangesList.get(i);
788 | count = changes.size();
789 | for (int j = count - 1; j >= 0; j--) {
790 | endChangeAnimationIfNecessary(changes.get(j));
791 | if (changes.isEmpty()) {
792 | mChangesList.remove(changes);
793 | }
794 | }
795 | }
796 |
797 | cancelAll(mRemoveAnimations);
798 | cancelAll(mMoveAnimations);
799 | cancelAll(mAddAnimations);
800 | cancelAll(mChangeAnimations);
801 |
802 | dispatchAnimationsFinished();
803 | }
804 |
805 | void cancelAll(List viewHolders) {
806 | for (int i = viewHolders.size() - 1; i >= 0; i--) {
807 | ViewCompat.animate(viewHolders.get(i).itemView).cancel();
808 | }
809 | }
810 |
811 | private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
812 | @Override
813 | public void onAnimationStart(View view) {}
814 |
815 | @Override
816 | public void onAnimationEnd(View view) {}
817 |
818 | @Override
819 | public void onAnimationCancel(View view) {}
820 | }
821 |
822 | }
823 |
--------------------------------------------------------------------------------
/app/src/main/java/com/mwang/irregulargridview/IrregularLayoutManager.java:
--------------------------------------------------------------------------------
1 | package com.mwang.irregulargridview;
2 |
3 | import android.content.Context;
4 | import android.graphics.Rect;
5 | import android.support.v7.widget.RecyclerView;
6 | import android.util.AttributeSet;
7 | import android.util.Log;
8 | import android.util.SparseIntArray;
9 | import android.view.View;
10 | import android.view.ViewGroup;
11 |
12 | import java.util.Arrays;
13 | import java.util.Comparator;
14 | import java.util.Iterator;
15 | import java.util.TreeMap;
16 |
17 | /**
18 | * An IrregularLayoutManager which composed of items with 4 kinds of sizes.
19 | * 1x1, 1x2, 2x1 and 2x2.
20 | */
21 | public class IrregularLayoutManager extends RecyclerView.LayoutManager {
22 |
23 | /* Default spanCount. */
24 | private static final int DEFAULT_SPAN_COUNT = 4;
25 | /* Current spanCount. */
26 | private int mSpanCount = DEFAULT_SPAN_COUNT;
27 |
28 | /* Store the bottom of each span. */
29 | private int[] spanBottom;
30 | /* The minimum of the spanBottom. */
31 | private int spanBottomMin;
32 | /* The maximum of the spanBottom. */
33 | private int spanBottomMax;
34 | /* The top of each span. */
35 | private int[] spanTop;
36 | /* The minimum of the spanTop. */
37 | private int spanTopMin;
38 | /* The maximum of the spanTop. */
39 | private int spanTopMax;
40 |
41 | /* Store the first span index where the first and the second span are both empty. */
42 | private int firstTwoEmptyBottomSpanIndex;
43 | /* The index of the first empty span. */
44 | private int firstOneEmptyBottomSpanIndex;
45 |
46 | /* The top border of the area to be filled. */
47 | private int topBorder;
48 | /* The bottom border. */
49 | private int bottomBorder;
50 |
51 | /**
52 | * Store the left and right borders for each span according to the span count.
53 | * This is the same as the mCachedBorders in the GridLayoutManager.
54 | */
55 | private int[] spanWidthBorders;
56 | /* Size for the smallest span. */
57 | private int sizePerSpan;
58 | /* The current position we need to lay out. */
59 | private int mCurrentPosition;
60 | /* The first and the last position of attached items. */
61 | private int firstAttachedItemPosition;
62 | private int lastAttachedItemPosition;
63 |
64 | /**
65 | * Store the layout widthNum and heightNum for each item.
66 | * The layout width is about widthNum * sizePerSpan.
67 | * The layout height is heightNum * sizePerSpan.
68 | */
69 | private SparseIntArray itemLayoutWidthCache;
70 | private SparseIntArray itemLayoutHeightCache;
71 | /* The first span index the item occupied. */
72 | private SparseIntArray itemOccupiedStartSpan;
73 |
74 | /* The scroll offset. */
75 | private int scrollOffset;
76 |
77 | /* The first item which is removed with notifyItemRemoved(). */
78 | private int firstChangedPosition;
79 | /* The number of removed items except for the items out of the bottom border. */
80 | private int removedTopAndBoundPositionCount;
81 | /**
82 | * If it is true, we need to update some parameters,
83 | * i.e., firstChangedPosition, removedTopAndBoundPositionCount.
84 | */
85 | private boolean isBeforePreLayout;
86 |
87 | /* A disappearing view cache with descending order. */
88 | private TreeMap disappearingViewCache;
89 |
90 | /* Determine whether onLayoutChildren() is triggered with notifyDataSetChanged(). */
91 | private boolean isNotifyDataSetChanged;
92 |
93 | /* The Rect for the items to be laid out. */
94 | final Rect mDecorInsets = new Rect();
95 |
96 | /**
97 | * The following params is calculated during the pre-layout phase
98 | * and is used for the real layout.
99 | */
100 | private int fakeSpanBottomMin;
101 | private int fakeSpanBottomMax;
102 | private int fakeCurrentPosition;
103 | private int fakeFirstAttachedItemPosition;
104 | private int[] fakeSpanTop;
105 | private int[] fakeSpanBottom;
106 | private int fakeFirstTwoEmptyBottomSpanIndex;
107 | private int fakeFirstOneEmptyBottomSpanIndex;
108 | private SparseIntArray fakeItemLayoutWidthCache;
109 | private SparseIntArray fakeItemLayoutHeightCache;
110 | private SparseIntArray fakeItemOccupiedStartSpan;
111 |
112 | /**
113 | * Default number of the spans is DEFAULT_SPAN_COUNT.
114 | * @param context
115 | */
116 | public IrregularLayoutManager(Context context){
117 | this(context, DEFAULT_SPAN_COUNT);
118 | }
119 |
120 | /**
121 | * You can set the number of spans with this constructor.
122 | * @param context
123 | * @param spanCount The number of spans.
124 | */
125 | public IrregularLayoutManager(Context context, int spanCount){
126 | super();
127 | setSpanCount(spanCount);
128 | }
129 |
130 | /**
131 | * set the number of span, it should be at least 2.
132 | * @param spanCount The number of spans.
133 | */
134 | private void setSpanCount(int spanCount){
135 | if(spanCount == mSpanCount)
136 | return;
137 | if(spanCount < 2)
138 | throw new IllegalArgumentException("Span count should be at least 2. Provided "
139 | + spanCount);
140 | mSpanCount = spanCount;
141 | }
142 |
143 | /**
144 | *
145 | * @return sizePerSpan
146 | */
147 | public int getSizePerSpan(){
148 | return sizePerSpan;
149 | }
150 |
151 | /**
152 | * If you want to customize the animation, it should return true.
153 | * @return
154 | */
155 | @Override
156 | public boolean supportsPredictiveItemAnimations() {
157 | return true;
158 | }
159 |
160 | @Override
161 | public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
162 | removeAllViews();
163 | }
164 |
165 | /**
166 | * This method is triggered with notifyDataSetChanged().
167 | * We need the label the isNotifyDataSetChanged before onLayoutChildren.
168 | * @param recyclerView
169 | */
170 | @Override
171 | public void onItemsChanged(RecyclerView recyclerView){
172 | isNotifyDataSetChanged = true;
173 | }
174 |
175 | /**
176 | * Triggered with notifyItemRemoved().
177 | * This method will be triggered before the pre-layout for invisible items (out of bounds)
178 | * and after the pre-layout for visible items.
179 | * If there are items removed out of the top border, we update the firstChangedPosition
180 | * and removedTopAndBoundPositionCount.
181 | * @param recyclerView
182 | * @param positionStart The start position of removed items.
183 | * @param itemCount The number of removed items from the start position.
184 | */
185 | @Override
186 | public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
187 | if(isBeforePreLayout){
188 | if(firstChangedPosition > positionStart || firstChangedPosition == -1)
189 | firstChangedPosition = positionStart;
190 | if(firstChangedPosition < firstAttachedItemPosition)
191 | removedTopAndBoundPositionCount += itemCount;
192 | }
193 | }
194 |
195 | /**
196 | * Called when it is initial layout, or the data set is changed.
197 | * If supportsPredictiveItemAnimations() returns true, it will be called twice,
198 | * i.e., the pre-layout and the real layout.
199 | * @param recycler
200 | * @param state
201 | */
202 | @Override
203 | public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
204 |
205 | // Nothing to be laid out, just clear attached views and return.
206 | if(state.getItemCount() == 0){
207 | detachAndScrapAttachedViews(recycler);
208 | return;
209 | }
210 | // For the pre-layout, we need to layout current attached views and appearing views.
211 | if(state.isPreLayout()){
212 | // If nothing is attached, just return.
213 | if(getChildCount() == 0)
214 | return;
215 | // For the current attached views, find the views removed and update
216 | // removedTopAndBoundPositionCount and firstChangedPosition.
217 | final int childCount = getChildCount();
218 | for(int i = 0; i < childCount; i++){
219 | View child = getChildAt(i);
220 | RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
221 | if(lp.isItemRemoved()){
222 | removedTopAndBoundPositionCount++;
223 | if(firstChangedPosition == -1 ||
224 | firstAttachedItemPosition + i < firstChangedPosition){
225 | firstChangedPosition = firstAttachedItemPosition + i;
226 | }
227 | }
228 | }
229 | // If removedTopAndBoundPositionCount = 0, items changes out of the bottom border,
230 | // So we have nothing to do during the pre-layout.
231 | // Otherwise we need to lay out current attached views and appearing views.
232 | if(removedTopAndBoundPositionCount != 0){
233 | layoutAttachedAndAppearingViews(recycler, state);
234 | }
235 | // Reset isBeforePreLayout after the pre-layout ends.
236 | isBeforePreLayout = false;
237 | return;
238 | }
239 |
240 | // The real layout.
241 | // First or empty layout, initialize layout parameters and fill.
242 | if(getChildCount() == 0){
243 | initializeLayoutParameters();
244 | fillGrid(recycler, state, true);
245 | return;
246 | }
247 | // If it is triggered with notifyDataSetChanged(),
248 | // we just clear attached views and layout from the beginning.
249 | if(isNotifyDataSetChanged){
250 | detachAndScrapAttachedViews(recycler);
251 | initializeLayoutParameters();
252 | fillGrid(recycler, state, true);
253 | isNotifyDataSetChanged = false;
254 | return;
255 | }
256 |
257 | // Adapter data set changes.
258 | if(firstChangedPosition == -1){ // No item is removed
259 | // reset parameters.
260 | mCurrentPosition = firstAttachedItemPosition;
261 | lastAttachedItemPosition = firstAttachedItemPosition;
262 | topBorder = getPaddingTop();
263 | bottomBorder = getHeight() - getPaddingBottom();
264 | spanBottom = Arrays.copyOf(spanTop, mSpanCount);
265 | updateSpanBottomParameters();
266 | // Fill the area.
267 | detachAndScrapAttachedViews(recycler);
268 | fillGrid(recycler, state, true);
269 |
270 | // Reset isBeforePreLayout.
271 | isBeforePreLayout = true;
272 | // firstChangedPosition = -1;
273 | // removedTopAndBoundPositionCount = 0;
274 | return;
275 | }
276 | // There are removed items.
277 | // Clear the cache from the firstChangedPosition
278 | for(int i = firstChangedPosition; i < state.getItemCount(); i++){
279 | if(itemLayoutWidthCache.get(i, 0) != 0){
280 | itemLayoutWidthCache.delete(i);
281 | itemLayoutHeightCache.delete(i);
282 | itemOccupiedStartSpan.delete(i);
283 | }
284 | if(fakeItemLayoutWidthCache.get(i, 0) != 0){
285 | itemLayoutWidthCache.put(i, fakeItemLayoutWidthCache.get(i));
286 | itemLayoutHeightCache.put(i, fakeItemLayoutHeightCache.get(i));
287 | itemOccupiedStartSpan.put(i, fakeItemOccupiedStartSpan.get(i));
288 | }
289 | }
290 | fakeItemLayoutWidthCache.clear();
291 | fakeItemLayoutHeightCache.clear();
292 | fakeItemOccupiedStartSpan.clear();
293 |
294 | detachAndScrapAttachedViews(recycler);
295 |
296 | // There are removed items out of the upper bound.
297 | if(firstChangedPosition < firstAttachedItemPosition) {
298 | mCurrentPosition = firstAttachedItemPosition;
299 | lastAttachedItemPosition = firstAttachedItemPosition;
300 | topBorder = getPaddingTop();
301 | bottomBorder = getHeight() - getPaddingBottom();
302 | spanBottom = Arrays.copyOf(spanTop, mSpanCount);
303 | updateSpanBottomParameters();
304 | fillGrid(recycler, state, true);
305 | // If it cannot fill until the bottomBorder, call scrollBy() to fill.
306 | if(spanBottomMax < bottomBorder){
307 | scrollBy(spanBottomMax - bottomBorder, recycler, state);
308 | }
309 | // Finally, we layout disappearing views.
310 | layoutDisappearingViews(recycler, state);
311 | }else{ // There are no removed items out of the upper bound.
312 | // Just set layout parameters and fill the visible area.
313 | mCurrentPosition = firstAttachedItemPosition;
314 | lastAttachedItemPosition = firstAttachedItemPosition;
315 | topBorder = getPaddingTop();
316 | bottomBorder = getHeight() - getPaddingBottom();
317 | spanBottom = Arrays.copyOf(spanTop, mSpanCount);
318 | updateSpanBottomParameters();
319 | fillGrid(recycler, state, true);
320 | // The number of items is too small, call scrollBy() to fill.
321 | if(spanBottomMax - bottomBorder < 0){
322 | scrollBy(spanBottomMax - bottomBorder, recycler, state);
323 | }
324 | }
325 | // After the real layout, we need to clear some parameters.
326 | isBeforePreLayout = true;
327 | firstChangedPosition = -1;
328 | removedTopAndBoundPositionCount = 0;
329 | disappearingViewCache.clear();
330 | }
331 |
332 | /**
333 | * Return true to indicate that it supports scrolling vertically.
334 | * @return
335 | */
336 | @Override
337 | public boolean canScrollVertically(){
338 | return true;
339 | }
340 |
341 | /**
342 | * We need to fill some extra space and offset children in this method.
343 | * @param dy The distance scrolled.
344 | * @param recycler
345 | * @param state
346 | * @return
347 | */
348 | @Override
349 | public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state){
350 | // Nothing to do when there are no attached items or dy = 0.
351 | if(getChildCount() == 0 || dy == 0){
352 | return 0;
353 | }
354 | return scrollBy(dy, recycler, state);
355 |
356 | }
357 |
358 | /**
359 | * The real logic for scroll.
360 | * @param dy The distance scrolled.
361 | * @param recycler
362 | * @param state
363 | * @return
364 | */
365 | private int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state){
366 |
367 | int delta = 0;
368 | // When scroll down, layout from left to right, top to bottom
369 | // Scroll down, update bottomBorder and fill.
370 | if(dy > 0){
371 | topBorder = getPaddingTop();
372 | bottomBorder += dy;
373 | mCurrentPosition = lastAttachedItemPosition + 1;
374 | fillGrid(recycler, state, true);
375 | // Offset child views.
376 | if(spanBottomMin >= bottomBorder) {
377 | delta = dy;
378 | bottomBorder -= dy;
379 | }else { // There are no more items we need to lay out.
380 | bottomBorder = getHeight() - getPaddingBottom();
381 | if(spanBottomMax - bottomBorder >= dy){
382 | delta = dy;
383 | }else{
384 | delta = Math.max(0, spanBottomMax - bottomBorder);
385 | }
386 | }
387 | offsetChildrenVertical(-delta);
388 | // After offset children, we need to update parameters.
389 | for(int i = 0; i < mSpanCount; i++) {
390 | spanTop[i] -= delta;
391 | spanBottom[i] -= delta;
392 | }
393 | spanTopMin -= delta;
394 | spanTopMax -= delta;
395 | spanBottomMin -= delta;
396 | spanBottomMax -= delta;
397 | // Recycle views out of the topBorder
398 | recycleTopInvisibleViews(recycler);
399 | }else{ // dy < 0
400 | // Scroll up, update topBorder and fill.
401 | topBorder += dy;
402 | bottomBorder = getHeight() - getPaddingBottom();
403 | // Happens when we delete too much items,
404 | // and the item for firstAttachedItemPosition is null.
405 | if(firstAttachedItemPosition == -1 ||
406 | firstAttachedItemPosition >= state.getItemCount()){
407 | firstAttachedItemPosition = state.getItemCount() - 1;
408 | lastAttachedItemPosition = firstAttachedItemPosition;
409 | mCurrentPosition = firstAttachedItemPosition;
410 | }else{
411 | mCurrentPosition = firstAttachedItemPosition - 1;
412 | }
413 | fillGrid(recycler, state, false);
414 | // Offset child views.
415 | if(spanTopMax <= topBorder) {
416 | delta = dy;
417 | topBorder -= dy;
418 | }else { // There are no more items we need to lay out.
419 | topBorder = getPaddingTop();
420 | if(spanTopMin - topBorder <= dy){
421 | delta = dy;
422 | }else{
423 | delta = -Math.max(0, topBorder - spanTopMin);
424 | }
425 | }
426 | offsetChildrenVertical(-delta);
427 | // After offset children, we need to update parameters.
428 | for(int i = 0; i < mSpanCount; i++) {
429 | spanTop[i] -= delta;
430 | spanBottom[i] -= delta;
431 | }
432 | spanTopMin -= delta;
433 | spanTopMax -= delta;
434 | spanBottomMin -= delta;
435 | spanBottomMax -= delta;
436 | // Recycle views out of the bottomBorder.
437 | recycleBottomInvisibleViews(recycler);
438 | }
439 | // Update scrollOffset.
440 | scrollOffset += delta;
441 | return delta;
442 | }
443 |
444 | /**
445 | * Initialize necessary parameters.
446 | */
447 | private void initializeLayoutParameters(){
448 |
449 | topBorder = getPaddingTop();
450 | bottomBorder = getHeight() - getPaddingBottom();
451 | spanTop = new int[mSpanCount];
452 | Arrays.fill(spanTop, getPaddingTop());
453 | spanBottom = new int[mSpanCount];
454 | Arrays.fill(spanBottom,getPaddingTop());
455 | spanTopMin = getPaddingTop();
456 | spanTopMax = getPaddingTop();
457 | spanBottomMin = getPaddingTop();
458 | spanBottomMax = getPaddingTop();
459 | firstOneEmptyBottomSpanIndex = 0;
460 | firstTwoEmptyBottomSpanIndex = 0;
461 | spanWidthBorders = new int[mSpanCount + 1];
462 | calculateSpanWidthBorders(getWidth() - getPaddingLeft() - getPaddingRight());
463 | mCurrentPosition = 0;
464 | firstAttachedItemPosition = 0;
465 | lastAttachedItemPosition = 0;
466 | itemLayoutWidthCache = new SparseIntArray();
467 | itemLayoutHeightCache = new SparseIntArray();
468 | itemOccupiedStartSpan = new SparseIntArray();
469 | //isRandomSize = true;
470 | scrollOffset = 0;
471 | isBeforePreLayout = true;
472 | firstChangedPosition = -1;
473 | removedTopAndBoundPositionCount = 0;
474 | disappearingViewCache = new TreeMap<>(new Comparator() {
475 | @Override
476 | public int compare(Integer lhs, Integer rhs) {
477 | return rhs.compareTo(lhs);
478 | }
479 | });
480 | isNotifyDataSetChanged = false;
481 |
482 | fakeItemLayoutWidthCache = new SparseIntArray();
483 | fakeItemLayoutHeightCache = new SparseIntArray();
484 | fakeItemOccupiedStartSpan = new SparseIntArray();
485 | }
486 |
487 | /**
488 | * Update spanBottomMin, spanBottomMax,
489 | * firstOneEmptyBottomSpanIndex and firstTwoEmptyBottomSpanIndex.
490 | */
491 | private void updateSpanBottomParameters(){
492 | spanBottomMin = spanBottom[0];
493 | spanBottomMax = spanBottom[0];
494 | for(int i = 1; i < mSpanCount; i++){
495 | if(spanBottomMin > spanBottom[i])
496 | spanBottomMin = spanBottom[i];
497 | if(spanBottomMax < spanBottom[i])
498 | spanBottomMax = spanBottom[i];
499 | }
500 | for(int i = 0; i < mSpanCount; i++){
501 | if(spanBottom[i] == spanBottomMin){
502 | firstOneEmptyBottomSpanIndex = i;
503 | break;
504 | }
505 | }
506 | firstTwoEmptyBottomSpanIndex = -1;
507 | for(int i = firstOneEmptyBottomSpanIndex; i < mSpanCount - 1; i++){
508 | if(spanBottom[i] == spanBottomMin && spanBottom[i + 1] == spanBottomMin){
509 | firstTwoEmptyBottomSpanIndex = i;
510 | break;
511 | }
512 | }
513 | }
514 |
515 | /**
516 | * Update fake params.
517 | */
518 | private void updateFakeSpanBottomParameters(){
519 | fakeSpanBottomMin = fakeSpanBottom[0];
520 | fakeSpanBottomMax = fakeSpanBottom[0];
521 | for(int i = 1; i < mSpanCount; i++){
522 | if(fakeSpanBottomMin > fakeSpanBottom[i])
523 | fakeSpanBottomMin = fakeSpanBottom[i];
524 | if(fakeSpanBottomMax < fakeSpanBottom[i])
525 | fakeSpanBottomMax = fakeSpanBottom[i];
526 | }
527 | for(int i = 0; i < mSpanCount; i++){
528 | if(fakeSpanBottom[i] == fakeSpanBottomMin){
529 | fakeFirstOneEmptyBottomSpanIndex = i;
530 | break;
531 | }
532 | }
533 | fakeFirstTwoEmptyBottomSpanIndex = -1;
534 | for(int i = fakeFirstOneEmptyBottomSpanIndex; i < mSpanCount - 1; i++){
535 | if(fakeSpanBottom[i] == fakeSpanBottomMin && fakeSpanBottom[i + 1] == fakeSpanBottomMin){
536 | fakeFirstTwoEmptyBottomSpanIndex = i;
537 | break;
538 | }
539 | }
540 | }
541 |
542 | /**
543 | * Update spanTopMin and spanTopMax.
544 | */
545 | private void updateSpanTopParameters(){
546 | spanTopMin = spanTop[0];
547 | spanTopMax = spanTop[0];
548 | for(int i = 1; i < mSpanCount; i++){
549 | if(spanTopMin > spanTop[i])
550 | spanTopMin = spanTop[i];
551 | if(spanTopMax < spanTop[i])
552 | spanTopMax = spanTop[i];
553 | }
554 | }
555 |
556 | /**
557 | * Calculate spanWidthBorders.
558 | * This is the same as calculateItemBorders(int totalSpace) in the GridLayoutManager.
559 | * @param totalSpace
560 | */
561 | private void calculateSpanWidthBorders(int totalSpace){
562 | if(spanWidthBorders == null || spanWidthBorders.length != mSpanCount + 1
563 | || spanWidthBorders[spanWidthBorders.length - 1] != totalSpace){
564 | spanWidthBorders = new int[mSpanCount + 1];
565 | }
566 | spanWidthBorders[0] = 0;
567 | sizePerSpan = totalSpace / mSpanCount;
568 | int sizePerSpanRemainder = totalSpace % mSpanCount;
569 | int consumedPixels = 0;
570 | int additionalSize = 0;
571 | for (int i = 1; i <= mSpanCount; i++) {
572 | int itemSize = sizePerSpan;
573 | additionalSize += sizePerSpanRemainder;
574 | if (additionalSize > 0 && (mSpanCount - additionalSize) < sizePerSpanRemainder) {
575 | itemSize += 1;
576 | additionalSize -= mSpanCount;
577 | }
578 | consumedPixels += itemSize;
579 | spanWidthBorders[i] = consumedPixels;
580 | }
581 | }
582 |
583 |
584 | /**
585 | * fill the area between the topBorder and the bottomBorder.
586 | * @param recycler
587 | * @param state
588 | * @param isFillBottom The direction, from the top to the bottom or the reverse.
589 | */
590 | private void fillGrid(RecyclerView.Recycler recycler, RecyclerView.State state,
591 | boolean isFillBottom) {
592 | while ( ( (isFillBottom && spanBottomMin <= bottomBorder) || (!isFillBottom && spanTopMax >= topBorder) )
593 | && mCurrentPosition >=0 && mCurrentPosition < state.getItemCount()) {
594 | layoutChunk(recycler, state, isFillBottom);
595 | }
596 | }
597 |
598 | /**
599 | * Fill the area for pre-layout.
600 | * On the one hand, we layout current attached view and appearing views.
601 | * On the other hand, we calculate the layout params for the real layout.
602 | * And the stop criterion is calculated according the params.
603 | * @param recycler
604 | * @param state
605 | */
606 | private void fillGridForPreLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
607 | while ( fakeSpanBottomMin <= bottomBorder
608 | && mCurrentPosition >=0 && mCurrentPosition < state.getItemCount()) {
609 | layoutChunk(recycler, state, true, true);
610 | }
611 | }
612 |
613 |
614 | /**
615 | * Here we lay out current attached views and appearing views.
616 | * Called when there are removed items out of the top border and in the visible area.
617 | * @param recycler
618 | * @param state
619 | */
620 | private void layoutAttachedAndAppearingViews(RecyclerView.Recycler recycler,
621 | RecyclerView.State state){
622 |
623 | // There are no removed items out of the top border.
624 | // So we just layout from the firstAttachedItemPosition.
625 | // We do the pre layout and calculate the layout params for the real layout.
626 | if(firstChangedPosition >= firstAttachedItemPosition){
627 | // Store the layout parameters.
628 | int firstAttachedItemPositionTemp = firstAttachedItemPosition;
629 | int lastAttachedItemPositionTemp = lastAttachedItemPosition;
630 | int[] spanTopTemp = Arrays.copyOf(spanTop, mSpanCount);
631 | int[] spanBottomTemp = Arrays.copyOf(spanBottom, mSpanCount);
632 |
633 | topBorder = getPaddingTop();
634 | bottomBorder = getHeight() - getPaddingBottom();
635 | spanBottom = Arrays.copyOf(spanTop, mSpanCount);
636 | updateSpanBottomParameters();
637 |
638 | detachAndScrapAttachedViews(recycler);
639 |
640 | mCurrentPosition = firstAttachedItemPosition;
641 | lastAttachedItemPosition = firstAttachedItemPosition;
642 |
643 | // Set the fake params.
644 | fakeSpanBottomMin = spanBottomMin;
645 | fakeSpanBottomMax = spanBottomMax;
646 | fakeCurrentPosition = mCurrentPosition;
647 | fakeFirstAttachedItemPosition = firstAttachedItemPosition;
648 | fakeFirstOneEmptyBottomSpanIndex = firstOneEmptyBottomSpanIndex;
649 | fakeFirstTwoEmptyBottomSpanIndex = firstTwoEmptyBottomSpanIndex;
650 | fakeSpanTop = Arrays.copyOf(spanTop, mSpanCount);
651 | fakeSpanBottom = Arrays.copyOf(spanBottom, mSpanCount);
652 |
653 | // Lay out current attached views and appearing views.
654 | fillGridForPreLayout(recycler, state);
655 |
656 | // Restore the layout parameters.
657 | firstAttachedItemPosition = firstAttachedItemPositionTemp;
658 | lastAttachedItemPosition = lastAttachedItemPositionTemp;
659 | spanTop = Arrays.copyOf(spanTopTemp, mSpanCount);
660 | spanBottom = Arrays.copyOf(spanBottomTemp, mSpanCount);
661 | updateSpanTopParameters();
662 | updateSpanBottomParameters();
663 | }else{ // There are removed items out of the top border.
664 |
665 | // Calculate the spanTop begin with the firstChangedPosition
666 | // and update layout parameters.
667 | topBorder = getPaddingTop() - scrollOffset;
668 | Arrays.fill(spanTop, topBorder);
669 | for (int i = 0; i < firstChangedPosition; i++) {
670 | for (int j = 0; j < itemLayoutWidthCache.get(i); j++) {
671 | int spanIndex = itemOccupiedStartSpan.get(i) + j;
672 | spanTop[spanIndex] += itemLayoutHeightCache.get(i) * sizePerSpan;
673 | }
674 | }
675 | updateSpanTopParameters();
676 | bottomBorder = getHeight() - getPaddingBottom();
677 | spanBottom = Arrays.copyOf(spanTop, mSpanCount);
678 | updateSpanBottomParameters();
679 | mCurrentPosition = firstChangedPosition;
680 | // Fill from the spanTop until bottomBorder.
681 | // Note that we just lay out attached views and appearing views.
682 | // The firstAttachedItemPosition may change,
683 | // set it as -1 and update it during the layout
684 | firstAttachedItemPosition = -1;
685 | lastAttachedItemPosition = -1;
686 |
687 | detachAndScrapAttachedViews(recycler);
688 |
689 | // Set the fake params.
690 | fakeSpanBottomMin = spanBottomMin;
691 | fakeSpanBottomMax = spanBottomMax;
692 | fakeCurrentPosition = mCurrentPosition;
693 | fakeFirstAttachedItemPosition = firstAttachedItemPosition;
694 | fakeFirstOneEmptyBottomSpanIndex = firstOneEmptyBottomSpanIndex;
695 | fakeFirstTwoEmptyBottomSpanIndex = firstTwoEmptyBottomSpanIndex;
696 | fakeSpanTop = Arrays.copyOf(spanTop, mSpanCount);
697 | fakeSpanBottom = Arrays.copyOf(spanBottom, mSpanCount);
698 |
699 | // Lay out current attached views and appearing views.
700 | fillGridForPreLayout(recycler, state);
701 |
702 | // Restore the layout parameters.
703 | firstAttachedItemPosition = fakeFirstAttachedItemPosition;
704 | spanTop = Arrays.copyOf(fakeSpanTop, mSpanCount);
705 | spanBottom = Arrays.copyOf(fakeSpanBottom, mSpanCount);
706 | updateSpanTopParameters();
707 | updateSpanBottomParameters();
708 | }
709 |
710 | }
711 |
712 | // Lay out disappearing views from the last one to the first one
713 | private void layoutDisappearingViews(RecyclerView.Recycler recycler, RecyclerView.State state){
714 | Iterator iterator = disappearingViewCache.keySet().iterator();
715 | while(iterator.hasNext()){
716 | int position = iterator.next();
717 | View view = recycler.getViewForPosition(position);
718 | DisappearingViewParams params = disappearingViewCache.get(position);
719 | addDisappearingView(view, 0);
720 | view.measure(params.widthSpec, params.heightSpec);
721 | layoutDecorated(view, params.left, params.top, params.right, params.bottom);
722 | }
723 | }
724 |
725 | /**
726 | * The layout process for each item.
727 | * @param recycler
728 | * @param state
729 | * @param isFillBottom
730 | */
731 | private void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
732 | boolean isFillBottom){
733 | layoutChunk(recycler, state, isFillBottom, false);
734 | }
735 |
736 | /**
737 | * The layout process for each item.
738 | * If isPreLayout = true, we lay out attached views and appearing views.
739 | * Otherwise we lay out views between the topBorder and the bottomBorder.
740 | * @param recycler
741 | * @param state
742 | * @param isFillBottom
743 | * @param isPreLayout
744 | */
745 | private void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
746 | boolean isFillBottom, boolean isPreLayout){
747 |
748 | int widthNum = 0, heightNum = 0, nextItemIndex = 0;
749 |
750 | int fakeWidthNum = 0, fakeHeightNum = 0, fakeNextItemIndex = 0;
751 |
752 | View view = null;
753 | DisappearingViewParams params = null;
754 | // If disappearingViewCache contains the params of the current view to be laid out,
755 | // get its params. This happens when too many items are removed,
756 | // and the fillGird() cannot fill to the bottom. Then scrollBy() is called.
757 | if(disappearingViewCache.containsKey(mCurrentPosition)){
758 | params = disappearingViewCache.get(mCurrentPosition);
759 | }
760 | // Get view from the recycler.
761 | view = recycler.getViewForPosition(mCurrentPosition);
762 |
763 | final LayoutParams lp = (LayoutParams) view.getLayoutParams();
764 |
765 | // Calculate the widthNum and the heightNum.
766 | // If the cache contains the widthNum and heightNum, get them from the cache.
767 | if(itemLayoutWidthCache.get(mCurrentPosition, 0) != 0){
768 | widthNum = itemLayoutWidthCache.get(mCurrentPosition);
769 | heightNum = itemLayoutHeightCache.get(mCurrentPosition);
770 | nextItemIndex = itemOccupiedStartSpan.get(mCurrentPosition);
771 | }else{
772 | // Otherwise, if LayoutParams contains them, get them from the LayoutParams.
773 | if(lp.widthNum != 0){
774 | widthNum = lp.widthNum;
775 | heightNum = lp.heightNum;
776 | }else{
777 | // Otherwise, calculate the widthNum and the heightNum
778 | // according to the size of the child view.
779 | widthNum = Math.min(2, Math.max(1, lp.width / sizePerSpan));
780 | heightNum = Math.min(2, Math.max(1, lp.height / sizePerSpan));
781 | lp.widthNum = widthNum;
782 | lp.heightNum = heightNum;
783 | }
784 | // If widthNum = 2 and there are no two sequential empty spans, just set widthNum as 1.
785 | if (isFillBottom && firstTwoEmptyBottomSpanIndex == -1) {
786 | widthNum = 1;
787 | }
788 | // Store the layout widthNum and heightNum (different from the original one).
789 | itemLayoutWidthCache.put(mCurrentPosition, widthNum);
790 | itemLayoutHeightCache.put(mCurrentPosition, heightNum);
791 | // Calculate the index of the first occupied span.
792 | if(isFillBottom) {
793 | nextItemIndex = widthNum == 1 ?
794 | firstOneEmptyBottomSpanIndex : firstTwoEmptyBottomSpanIndex;
795 | }
796 | // Store the index of the first occupied span, which is useful when scrolling up.
797 | itemOccupiedStartSpan.put(mCurrentPosition, nextItemIndex);
798 | }
799 |
800 | // Calculate fake params.
801 | if(isPreLayout && !lp.isItemRemoved()){
802 | fakeWidthNum = lp.widthNum;
803 | fakeHeightNum = lp.heightNum;
804 | if(fakeFirstTwoEmptyBottomSpanIndex == -1){
805 | fakeWidthNum = 1;
806 | }
807 | fakeNextItemIndex = fakeWidthNum == 1 ?
808 | fakeFirstOneEmptyBottomSpanIndex : fakeFirstTwoEmptyBottomSpanIndex;
809 | fakeItemLayoutWidthCache.put(fakeCurrentPosition, fakeWidthNum);
810 | fakeItemLayoutHeightCache.put(fakeCurrentPosition, fakeHeightNum);
811 | fakeItemOccupiedStartSpan.put(fakeCurrentPosition, fakeNextItemIndex);
812 | }
813 |
814 | // Calculate the left, right, top and bottom of the view to be laid out.
815 | int left = 0, right = 0, top = 0, bottom = 0;
816 | int fakeLeft = 0, fakeRight = 0, fakeTop = 0, fakeBottom = 0;
817 |
818 | // We do not need to calculate decorations for views in the disappearingViewCache.
819 | if(params == null) {
820 | calculateItemDecorationsForChild(view, mDecorInsets);
821 | }
822 | left = getPaddingLeft() + spanWidthBorders[nextItemIndex] + lp.leftMargin;
823 | right = getPaddingLeft() + spanWidthBorders[nextItemIndex + widthNum] - lp.rightMargin;
824 | if(isFillBottom){
825 | top = getPaddingTop() + spanBottomMin + lp.topMargin;
826 | bottom = getPaddingTop() + spanBottomMin + sizePerSpan * heightNum - lp.bottomMargin;
827 | }else{
828 | bottom = getPaddingTop() + spanTop[nextItemIndex] - lp.bottomMargin;
829 | top = getPaddingTop() + spanTop[nextItemIndex] - sizePerSpan * heightNum + lp.topMargin;
830 | }
831 |
832 | if(isPreLayout && !lp.isItemRemoved()){
833 | fakeLeft = getPaddingLeft() + spanWidthBorders[fakeNextItemIndex] + lp.leftMargin;
834 | fakeRight = getPaddingLeft() + spanWidthBorders[fakeNextItemIndex + fakeWidthNum]
835 | - lp.rightMargin;
836 | fakeTop = getPaddingTop() + fakeSpanBottomMin + lp.topMargin;
837 | fakeBottom = getPaddingTop() + fakeSpanBottomMin + sizePerSpan * fakeHeightNum
838 | - lp.bottomMargin;
839 | }
840 |
841 | // If we lay out the view to fill bottom, add the view to the end.
842 | if(isFillBottom) {
843 |
844 | if(!isPreLayout){
845 | addView(view);
846 | }else if(bottom + lp.bottomMargin >= getPaddingTop() || // Attached
847 | firstAttachedItemPosition != -1 ||
848 | fakeBottom + lp.bottomMargin >= getPaddingTop() || // Appearing
849 | fakeFirstAttachedItemPosition != -1){
850 | // If it is pre-layout, we just lay out attached views and appearing views.
851 | if(lp.isItemRemoved()) {
852 | addDisappearingView(view);
853 | } else {
854 | addView(view);
855 | }
856 | }
857 | }else if(!isFillBottom){ // Otherwise it is added to the beginning.
858 | addView(view, 0);
859 | }
860 |
861 | // Make measureSpec.
862 | int widthSpec, heightSpec;
863 | int fakeWidthSpec = 0, fakeHeightSpec = 0;
864 | if(params == null) {
865 | widthSpec = View.MeasureSpec.makeMeasureSpec(
866 | right - left - mDecorInsets.left - mDecorInsets.right, View.MeasureSpec.EXACTLY);
867 | heightSpec = View.MeasureSpec.makeMeasureSpec(
868 | bottom - top - mDecorInsets.top - mDecorInsets.bottom, View.MeasureSpec.EXACTLY);
869 | }else{
870 | // If disappearingViewCache contains the params,
871 | // get the widthSpec and the heightSpec from it.
872 | widthSpec = params.widthSpec;
873 | heightSpec = params.heightSpec;
874 | }
875 |
876 | if(isPreLayout && !lp.isItemRemoved()){
877 | fakeWidthSpec = View.MeasureSpec.makeMeasureSpec(
878 | fakeRight - fakeLeft - mDecorInsets.left - mDecorInsets.right,
879 | View.MeasureSpec.EXACTLY);
880 | fakeHeightSpec = View.MeasureSpec.makeMeasureSpec(
881 | fakeBottom - fakeTop - mDecorInsets.top - mDecorInsets.bottom,
882 | View.MeasureSpec.EXACTLY);
883 | }
884 |
885 | // Measure child.
886 | // If isPreLayout = true, we just measure and lay out attached views and appearing views.
887 | if(!isPreLayout ||
888 | (isPreLayout && (bottom + lp.bottomMargin >= getPaddingTop() || // Attached
889 | firstAttachedItemPosition != -1 ||
890 | fakeBottom + lp.bottomMargin >= getPaddingTop() || // Appearing
891 | fakeFirstAttachedItemPosition != -1)
892 | && !lp.isItemRemoved())){
893 | view.measure(widthSpec, heightSpec);
894 | layoutDecorated(view, left, top, right, bottom);
895 | }
896 | // If isPreLayout = true, for disappearing views, we put the params and position into cache.
897 | if(isPreLayout && (bottom + lp.bottomMargin >= getPaddingTop() || // Currently visible
898 | firstAttachedItemPosition != -1)
899 | && (fakeBottom + lp.bottomMargin < getPaddingTop() && // Invisible in real layout
900 | fakeFirstAttachedItemPosition == -1)
901 | && !lp.isItemRemoved()){
902 | disappearingViewCache.put(fakeCurrentPosition,
903 | new DisappearingViewParams(fakeWidthSpec, fakeHeightSpec,
904 | fakeLeft, fakeTop, fakeRight, fakeBottom));
905 | }
906 | // For the normal layout,
907 | // if we lay out a disappearing view, it should be removed from the cache.
908 | if(!isPreLayout && params != null){
909 | disappearingViewCache.remove(mCurrentPosition);
910 | }
911 |
912 | // update some parameters
913 | if(isFillBottom){
914 | for (int i = 0; i < widthNum; i++)
915 | spanBottom[nextItemIndex + i] += sizePerSpan * heightNum;
916 | updateSpanBottomParameters();
917 | if(!isPreLayout){
918 | lastAttachedItemPosition = mCurrentPosition;
919 | }else{
920 | // If isPreLayout = true.
921 | for (int i = 0; i < fakeWidthNum; i++)
922 | fakeSpanBottom[fakeNextItemIndex + i] += sizePerSpan * fakeHeightNum;
923 | updateFakeSpanBottomParameters();
924 | // we need to update fakeFirstAttachedItemPosition and firstAttachedItemPosition.
925 | if(fakeFirstAttachedItemPosition == -1 &&
926 | !lp.isItemRemoved() &&
927 | fakeBottom + lp.bottomMargin >= getPaddingTop()){
928 | fakeFirstAttachedItemPosition = fakeCurrentPosition;
929 | }
930 | if(firstAttachedItemPosition == -1 && bottom + lp.bottomMargin >= getPaddingTop()){
931 | firstAttachedItemPosition = mCurrentPosition;
932 | }
933 | }
934 | mCurrentPosition++;
935 | if(isPreLayout && !lp.isItemRemoved()){
936 | fakeCurrentPosition++;
937 | }
938 | // Update fakeSpanTop and spanTop.
939 | if(isPreLayout && fakeFirstAttachedItemPosition == -1){
940 | for (int i = 0; i < fakeWidthNum; i++)
941 | fakeSpanTop[fakeNextItemIndex + i] += sizePerSpan * fakeHeightNum;
942 | }
943 | if(isPreLayout && firstAttachedItemPosition == -1){
944 | for (int i = 0; i < widthNum; i++)
945 | spanTop[nextItemIndex + i] += sizePerSpan * heightNum;
946 | }
947 | }else{
948 | for (int i = 0; i < widthNum; i++)
949 | spanTop[nextItemIndex + i] -= sizePerSpan * heightNum;
950 | updateSpanTopParameters();
951 | firstAttachedItemPosition = mCurrentPosition;
952 | mCurrentPosition--;
953 | }
954 |
955 | }
956 |
957 | /**
958 | * Recycle views out of the top border.
959 | * @param recycler
960 | */
961 | private void recycleTopInvisibleViews(RecyclerView.Recycler recycler){
962 | final int childCount = getChildCount();
963 | for(int i = 0; i <= childCount; i++){
964 | View child = getChildAt(i);
965 | // Recycle views from here.
966 | if(getDecoratedEnd(child) > topBorder){
967 | recycleChildren(recycler, 0, i - 1);
968 | firstAttachedItemPosition += i;
969 | updateSpanTopParameters();
970 | return;
971 | }
972 | // Update spanTop.
973 | int heightNum = itemLayoutHeightCache.get(firstAttachedItemPosition + i);
974 | for(int j = 0; j < itemLayoutWidthCache.get(firstAttachedItemPosition + i); j++){
975 | int spanIndex = itemOccupiedStartSpan.get(firstAttachedItemPosition + i) + j;
976 | spanTop[spanIndex] += heightNum * sizePerSpan;
977 | }
978 | }
979 | }
980 |
981 | /**
982 | * Recycle views out of the bottom border.
983 | * @param recycler
984 | */
985 | private void recycleBottomInvisibleViews(RecyclerView.Recycler recycler){
986 | final int childCount = getChildCount();
987 | for(int i = childCount - 1; i >= 0; i--){
988 | View child = getChildAt(i);
989 | // Recycle views from here.
990 | if(getDecoratedStart(child) < bottomBorder){
991 | recycleChildren(recycler, i + 1, childCount - 1);
992 | lastAttachedItemPosition -= (childCount - 1 - i);
993 | updateSpanBottomParameters();
994 | return;
995 | }
996 | // Update spanBottom.
997 | int position = lastAttachedItemPosition - (childCount - 1 - i);
998 | int heightNum = itemLayoutHeightCache.get(position);
999 | for(int j = 0; j < itemLayoutWidthCache.get(position); j++){
1000 | int spanIndex = itemOccupiedStartSpan.get(position) + j;
1001 | spanBottom[spanIndex] -= heightNum * sizePerSpan;
1002 | }
1003 | }
1004 | }
1005 |
1006 | /**
1007 | * Recycle views from the endIndex to the startIndex.
1008 | * @param startIndex inclusive
1009 | * @param endIndex inclusive
1010 | */
1011 | private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex){
1012 | if(startIndex > endIndex){
1013 | return;
1014 | }
1015 | for(int i = endIndex; i >= startIndex; i--){
1016 | removeAndRecycleViewAt(i, recycler);
1017 | }
1018 | }
1019 |
1020 | /**
1021 | * Helper method to get the top of the view including the decoration and the margin.
1022 | * @param view
1023 | * @return
1024 | */
1025 | public int getDecoratedStart(View view) {
1026 | final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
1027 | view.getLayoutParams();
1028 | return getDecoratedTop(view) - params.topMargin;
1029 | }
1030 |
1031 | /**
1032 | * Helper method to get the bottom of the view including the decoration and the margin.
1033 | * @param view
1034 | * @return
1035 | */
1036 | public int getDecoratedEnd(View view) {
1037 | final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
1038 | view.getLayoutParams();
1039 | return getDecoratedBottom(view) + params.bottomMargin;
1040 | }
1041 |
1042 | @Override
1043 | public RecyclerView.LayoutParams generateDefaultLayoutParams() {
1044 | return new LayoutParams(
1045 | ViewGroup.LayoutParams.WRAP_CONTENT,
1046 | ViewGroup.LayoutParams.WRAP_CONTENT);
1047 | }
1048 | @Override
1049 | public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
1050 | return new LayoutParams(c, attrs);
1051 | }
1052 | @Override
1053 | public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
1054 | if (lp instanceof ViewGroup.MarginLayoutParams) {
1055 | return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
1056 | } else {
1057 | return new LayoutParams(lp);
1058 | }
1059 | }
1060 | @Override
1061 | public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
1062 | return lp instanceof LayoutParams;
1063 | }
1064 |
1065 | public static class LayoutParams extends RecyclerView.LayoutParams {
1066 |
1067 | //Original widthNum.
1068 | public int widthNum;
1069 | //Original heightNum.
1070 | public int heightNum;
1071 |
1072 | public LayoutParams(Context c, AttributeSet attrs) {
1073 | super(c, attrs);
1074 | }
1075 | public LayoutParams(int width, int height) {
1076 | super(width, height);
1077 | }
1078 | public LayoutParams(ViewGroup.MarginLayoutParams source) {
1079 | super(source);
1080 | }
1081 | public LayoutParams(ViewGroup.LayoutParams source) {
1082 | super(source);
1083 | }
1084 | public LayoutParams(RecyclerView.LayoutParams source) {
1085 | super(source);
1086 | }
1087 | }
1088 |
1089 | public static class DisappearingViewParams {
1090 | int left, right, top, bottom;
1091 | int widthSpec, heightSpec;
1092 | DisappearingViewParams(int widthSpec, int heightSpec,
1093 | int left, int top, int right, int bottom){
1094 | this.widthSpec = widthSpec;
1095 | this.heightSpec = heightSpec;
1096 | this.left = left;
1097 | this.right = right;
1098 | this.top = top;
1099 | this.bottom = bottom;
1100 | }
1101 | }
1102 | }
1103 |
--------------------------------------------------------------------------------
/app/src/main/java/com/mwang/irregulargridview/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.mwang.irregulargridview;
2 |
3 | import android.support.design.widget.NavigationView;
4 | import android.support.v4.app.FragmentManager;
5 | import android.support.v4.app.FragmentTransaction;
6 | import android.support.v4.view.GravityCompat;
7 | import android.support.v4.widget.DrawerLayout;
8 | import android.support.v7.app.ActionBarDrawerToggle;
9 | import android.support.v7.app.AppCompatActivity;
10 | import android.os.Bundle;
11 | import android.support.v7.widget.Toolbar;
12 | import android.view.MenuItem;
13 |
14 | public class MainActivity extends AppCompatActivity
15 | implements NavigationView.OnNavigationItemSelectedListener {
16 |
17 | @Override
18 | protected void onCreate(Bundle savedInstanceState) {
19 | super.onCreate(savedInstanceState);
20 | setContentView(R.layout.activity_main);
21 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
22 | setSupportActionBar(toolbar);
23 |
24 | DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
25 | ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
26 | this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
27 | drawer.setDrawerListener(toggle);
28 | toggle.syncState();
29 |
30 | NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
31 | navigationView.setNavigationItemSelectedListener(this);
32 |
33 | FragmentManager fragmentManager = getSupportFragmentManager();
34 | FragmentTransaction ft = fragmentManager.beginTransaction();
35 | ft.replace(R.id.container, SimpleFragment.newInstance());
36 | ft.commit();
37 | }
38 |
39 | @Override
40 | public void onBackPressed() {
41 | DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
42 | if (drawer.isDrawerOpen(GravityCompat.START)) {
43 | drawer.closeDrawer(GravityCompat.START);
44 | } else {
45 | super.onBackPressed();
46 | }
47 | }
48 |
49 | @SuppressWarnings("StatementWithEmptyBody")
50 | @Override
51 | public boolean onNavigationItemSelected(MenuItem item) {
52 | // Handle navigation view item clicks here.
53 | int id = item.getItemId();
54 | FragmentManager fragmentManager = getSupportFragmentManager();
55 | FragmentTransaction ft = fragmentManager.beginTransaction();
56 | if (id == R.id.nav_base) {
57 | ft.replace(R.id.container, SimpleFragment.newInstance());
58 | } else if (id == R.id.nav_gallery) {
59 | ft.replace(R.id.container, SimpleImageFragment.newInstance());
60 | }
61 | ft.commit();
62 |
63 | DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
64 | drawer.closeDrawer(GravityCompat.START);
65 | return true;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/src/main/java/com/mwang/irregulargridview/SimpleAdapter.java:
--------------------------------------------------------------------------------
1 | package com.mwang.irregulargridview;
2 |
3 | import android.content.Context;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 | import android.widget.ImageView;
9 | import android.widget.TextView;
10 |
11 | import java.util.ArrayList;
12 |
13 | public class SimpleAdapter extends BaseAdapter{
14 |
15 | private ArrayList mDataSet;
16 | private ArrayList widthNums;
17 | private ArrayList heightNums;
18 |
19 | public SimpleAdapter(Context context, RecyclerView rec,
20 | ArrayList data, ArrayList w, ArrayList h){
21 | super(context, rec);
22 | mDataSet = data;
23 | widthNums = w;
24 | heightNums = h;
25 | }
26 |
27 | @Override
28 | public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType){
29 | View v = LayoutInflater.from(mContext).inflate(R.layout.grid_item_text,parent,false);
30 | MyViewHolder holder = new MyViewHolder(v);
31 | return holder;
32 | }
33 |
34 | @Override
35 | public void onBindViewHolder(final VH holder, int position){
36 | ((MyViewHolder)holder).tv.setText(mDataSet.get(position));
37 | setViewParams(holder.itemView, position);
38 | super.onBindViewHolder(holder, position);
39 | }
40 |
41 | @Override
42 | public int getItemCount(){
43 | return mDataSet.size();
44 | }
45 |
46 | public static class MyViewHolder extends BaseAdapter.VH {
47 | public TextView tv;
48 | public ImageView checkImage;
49 | public MyViewHolder(View v){
50 | super(v);
51 | tv = (TextView)v.findViewById(R.id.tv_num);
52 | checkImage = (ImageView)v.findViewById(R.id.iv_check);
53 | }
54 | }
55 |
56 | @Override
57 | protected void updateSelectedItem(VH holder){
58 | ((MyViewHolder)holder).checkImage.setVisibility(View.VISIBLE);
59 | ((MyViewHolder)holder).tv.setActivated(true);
60 | }
61 |
62 | @Override
63 | protected void updateUnselectedItem(VH holder){
64 | ((MyViewHolder)holder).checkImage.setVisibility(View.GONE);
65 | ((MyViewHolder)holder).tv.setActivated(false);
66 | }
67 |
68 | private void setViewParams(View v, int position){
69 | IrregularLayoutManager.LayoutParams lp = (IrregularLayoutManager.LayoutParams)v.getLayoutParams();
70 | lp.widthNum = widthNums.get(position);
71 | lp.heightNum = heightNums.get(position);
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/main/java/com/mwang/irregulargridview/SimpleFragment.java:
--------------------------------------------------------------------------------
1 | package com.mwang.irregulargridview;
2 |
3 | import android.os.Bundle;
4 | import android.support.v7.app.AppCompatActivity;
5 | import android.support.v7.widget.DefaultItemAnimator;
6 | import android.support.v7.widget.GridLayoutManager;
7 | import android.support.v7.widget.RecyclerView;
8 | import android.view.LayoutInflater;
9 | import android.view.View;
10 | import android.view.ViewGroup;
11 |
12 | import java.util.ArrayList;
13 | import java.util.Random;
14 |
15 | public class SimpleFragment extends BaseFragment {
16 |
17 | private RecyclerView mGridView;
18 | private ArrayList mStringData;
19 | private ArrayList widthNums;
20 | private ArrayList heightNums;
21 | private SimpleAdapter mAdapter;
22 |
23 | public SimpleFragment() {
24 | // Required empty public constructor
25 | }
26 |
27 | public static SimpleFragment newInstance() {
28 | SimpleFragment fragment = new SimpleFragment();
29 | Bundle args = new Bundle();
30 | fragment.setArguments(args);
31 | return fragment;
32 | }
33 |
34 | @Override
35 | public View onCreateView(LayoutInflater inflater, ViewGroup container,
36 | Bundle savedInstanceState) {
37 | View view = inflater.inflate(R.layout.fragment_base, container, false);
38 |
39 | mGridView = (RecyclerView)view.findViewById(R.id.irregular_gridview);
40 | //mGridView.setLayoutManager(new GridLayoutManager(getContext(), 4));
41 | IrregularLayoutManager layoutManager = new IrregularLayoutManager(getContext(), 4);
42 | mGridView.setLayoutManager(layoutManager);
43 |
44 | mAdapter = new SimpleAdapter(getContext(), mGridView, mStringData, widthNums, heightNums);
45 | mAdapter.setOnItemClickLitener(new BaseAdapter.OnItemClickLitener() {
46 | @Override
47 | public void onItemClick(BaseAdapter.VH holder, int position) {
48 | if (mActionMode != null) {
49 | mAdapter.reverseSelect(holder, position);
50 | }
51 | }
52 |
53 | @Override
54 | public boolean onItemLongClick(BaseAdapter.VH holder, int position) {
55 | if (mActionMode == null) {
56 | mActionMode = ((AppCompatActivity) getContext()).startSupportActionMode(SimpleFragment.this);
57 | }
58 | mAdapter.selectItem(holder, position);
59 | return true;
60 | }
61 | });
62 | mGridView.setAdapter(mAdapter);
63 | DynamicItemAnimator animator = new DynamicItemAnimator();
64 | mGridView.setItemAnimator(new DefaultItemAnimator());
65 |
66 | return view;
67 | }
68 |
69 | @Override
70 | public void initData(){
71 | mStringData = new ArrayList<>();
72 | for(int i = 0; i < 100; i++){
73 | mStringData.add("" + i);
74 | }
75 | Random r = new Random();
76 | widthNums = new ArrayList<>();
77 | heightNums = new ArrayList<>();
78 | for(int i = 0; i < mStringData.size(); i++){
79 | int widthNum, heightNum;
80 | int nextInt = r.nextInt(100);
81 | if (nextInt > 80) {
82 | widthNum = 2;
83 | heightNum = 2;
84 | } else if (nextInt > 60) {
85 | widthNum = 2;
86 | heightNum = 1;
87 | } else if (nextInt > 40) {
88 | widthNum = 1;
89 | heightNum = 2;
90 | } else {
91 | widthNum = 1;
92 | heightNum = 1;
93 | }
94 | widthNums.add(widthNum);
95 | heightNums.add(heightNum);
96 | }
97 | }
98 |
99 | @Override
100 | public void deleteSelectedItems() {
101 | ArrayList list = mAdapter.getSelectedItems();
102 | for (int i = list.size() - 1; i >= 0; i--){
103 | int index = list.get(i);
104 | mStringData.remove(index);
105 | widthNums.remove(index);
106 | heightNums.remove(index);
107 | mAdapter.notifyItemRemoved(index);
108 | }
109 | }
110 |
111 | @Override
112 | public void resetSelectedItems(){
113 | mAdapter.resetSelectedItems();
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/app/src/main/java/com/mwang/irregulargridview/SimpleImageAdapter.java:
--------------------------------------------------------------------------------
1 | package com.mwang.irregulargridview;
2 |
3 | import android.content.Context;
4 | import android.graphics.BitmapFactory;
5 | import android.graphics.Color;
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.ImageView;
11 |
12 | import com.bumptech.glide.Glide;
13 |
14 | import java.util.ArrayList;
15 |
16 | public class SimpleImageAdapter extends BaseAdapter{
17 |
18 | private int sizePerSpan;
19 | private ArrayList mImageDataPath;
20 |
21 | public SimpleImageAdapter(Context context, RecyclerView rec, ArrayList arr){
22 | super(context, rec);
23 | mImageDataPath = arr;
24 | }
25 |
26 | @Override
27 | public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType){
28 | View v = LayoutInflater.from(mContext).inflate(R.layout.grid_item_image, parent, false);
29 | MyViewHolder holder = new MyViewHolder(v);
30 | return holder;
31 | }
32 |
33 | @Override
34 | public void onBindViewHolder(final VH holder, int position){
35 |
36 | IrregularLayoutManager.LayoutParams lp = setViewParams(mImageDataPath.get(position),
37 | holder.itemView);
38 |
39 | if(sizePerSpan == 0){
40 | sizePerSpan = ((IrregularLayoutManager)recyclerView.getLayoutManager()).getSizePerSpan() + 1;
41 | }
42 |
43 | Glide.with(mContext)
44 | .load("file://" + mImageDataPath.get(position))
45 | .override(lp.widthNum * sizePerSpan, lp.heightNum * sizePerSpan)
46 | .centerCrop()
47 | .placeholder(R.drawable.bg_empty_photo)
48 | .into(((MyViewHolder)holder).photo);
49 |
50 | super.onBindViewHolder(holder, position);
51 | }
52 |
53 | @Override
54 | public int getItemCount(){
55 | return mImageDataPath.size();
56 | }
57 |
58 | public static class MyViewHolder extends BaseAdapter.VH {
59 | public ImageView photo;
60 | public ImageView checkImage;
61 | public MyViewHolder(View v){
62 | super(v);
63 | photo = (ImageView)v.findViewById(R.id.iv_photo);
64 | checkImage = (ImageView)v.findViewById(R.id.iv_check);
65 | }
66 | }
67 |
68 | @Override
69 | protected void updateSelectedItem(VH holder){
70 | ((MyViewHolder)holder).checkImage.setVisibility(View.VISIBLE);
71 | ((MyViewHolder)holder).photo.setColorFilter(Color.argb(120, 00, 00, 00));
72 | }
73 |
74 | @Override
75 | protected void updateUnselectedItem(VH holder){
76 | ((MyViewHolder)holder).checkImage.setVisibility(View.GONE);
77 | ((MyViewHolder)holder).photo.clearColorFilter();
78 | }
79 |
80 | private IrregularLayoutManager.LayoutParams setViewParams(String path, View view){
81 | BitmapFactory.Options options = new BitmapFactory.Options();
82 | options.inJustDecodeBounds = true;
83 | BitmapFactory.decodeFile(path, options);
84 | int imageHeight = options.outHeight;
85 | int imageWidth = options.outWidth;
86 | int widthNum = 1, heightNum = 1;
87 | if(imageWidth >= 1200 && imageHeight >= 1200){
88 | widthNum = 2;
89 | heightNum = 2;
90 | }else if(imageWidth >= imageHeight * 1.5){
91 | widthNum = 2;
92 | }else if(imageHeight >= imageWidth * 1.3){
93 | heightNum = 2;
94 | }
95 | IrregularLayoutManager.LayoutParams lp = (IrregularLayoutManager.LayoutParams)view.getLayoutParams();
96 | lp.widthNum = widthNum;
97 | lp.heightNum = heightNum;
98 | return lp;
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/app/src/main/java/com/mwang/irregulargridview/SimpleImageFragment.java:
--------------------------------------------------------------------------------
1 | package com.mwang.irregulargridview;
2 |
3 | import android.content.Context;
4 | import android.database.Cursor;
5 | import android.graphics.Point;
6 | import android.os.Bundle;
7 | import android.os.Environment;
8 | import android.provider.MediaStore;
9 | import android.support.v4.app.Fragment;
10 | import android.support.v7.app.AppCompatActivity;
11 | import android.support.v7.view.ActionMode;
12 | import android.support.v7.widget.GridLayoutManager;
13 | import android.support.v7.widget.RecyclerView;
14 | import android.view.LayoutInflater;
15 | import android.view.Menu;
16 | import android.view.MenuInflater;
17 | import android.view.MenuItem;
18 | import android.view.View;
19 | import android.view.ViewGroup;
20 | import android.view.WindowManager;
21 |
22 | import com.bumptech.glide.Glide;
23 |
24 | import java.util.ArrayList;
25 |
26 | public class SimpleImageFragment extends BaseFragment{
27 |
28 | private RecyclerView mGridView;
29 | private ArrayList mImageDataPath;
30 | private SimpleImageAdapter mImageAdapter;
31 |
32 | public SimpleImageFragment() {
33 | // Required empty public constructor
34 | }
35 |
36 | public static SimpleImageFragment newInstance() {
37 | SimpleImageFragment fragment = new SimpleImageFragment();
38 | Bundle args = new Bundle();
39 | fragment.setArguments(args);
40 | return fragment;
41 | }
42 |
43 | @Override
44 | public View onCreateView(LayoutInflater inflater, ViewGroup container,
45 | Bundle savedInstanceState) {
46 | View view = inflater.inflate(R.layout.fragment_base, container, false);
47 |
48 | mGridView = (RecyclerView)view.findViewById(R.id.irregular_gridview);
49 |
50 | IrregularLayoutManager layoutManager = new IrregularLayoutManager(getContext(), 4);
51 | mGridView.setLayoutManager(layoutManager);
52 |
53 | mImageAdapter = new SimpleImageAdapter(getContext(), mGridView, mImageDataPath);
54 | mImageAdapter.setOnItemClickLitener(new BaseAdapter.OnItemClickLitener() {
55 | @Override
56 | public void onItemClick(BaseAdapter.VH holder, int position) {
57 | if (mActionMode != null) {
58 | mImageAdapter.reverseSelect(holder, position);
59 | }
60 | }
61 | @Override
62 | public boolean onItemLongClick(BaseAdapter.VH holder, int position) {
63 | if (mActionMode == null) {
64 | mActionMode = ((AppCompatActivity) getContext())
65 | .startSupportActionMode(SimpleImageFragment.this);
66 | }
67 | mImageAdapter.selectItem(holder, position);
68 | return true;
69 | }
70 | });
71 | mGridView.setAdapter(mImageAdapter);
72 |
73 | mGridView.setItemAnimator(new DynamicItemAnimator());
74 |
75 | return view;
76 | }
77 |
78 | @Override
79 | public void initData(){
80 | mImageDataPath = new ArrayList<>();
81 |
82 | String picFolder = Environment.getExternalStoragePublicDirectory(
83 | Environment.DIRECTORY_PICTURES).getAbsolutePath();
84 | String[] proj = {MediaStore.Images.Media.DATA};
85 | Cursor cursor = MediaStore.Images.Media.query(getContext().getContentResolver(),
86 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, proj,
87 | "", null, MediaStore.Images.Media.DATE_TAKEN + " DESC");
88 | if(cursor != null){
89 | int dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA);
90 | while(cursor.moveToNext()){
91 | String path = cursor.getString(dataColumn);
92 | if(path != null && path.startsWith(picFolder)){
93 | mImageDataPath.add(path);
94 | }
95 | }
96 | }
97 | cursor.close();
98 | }
99 |
100 | @Override
101 | public void deleteSelectedItems() {
102 | ArrayList list = mImageAdapter.getSelectedItems();
103 | for (int i = list.size() - 1; i >= 0; i--){
104 | int index = list.get(i);
105 | mImageDataPath.remove(index);
106 | mImageAdapter.notifyItemRemoved(index);
107 | }
108 | }
109 |
110 | @Override
111 | public void resetSelectedItems(){
112 | mImageAdapter.resetSelectedItems();
113 | }
114 |
115 | }
116 |
117 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_check_circle_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-hdpi/ic_check_circle_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_delete_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-hdpi/ic_delete_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_check_circle_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-mdpi/ic_check_circle_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_delete_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-mdpi/ic_delete_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_check_circle_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-xhdpi/ic_check_circle_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_check_circle_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-xxhdpi/ic_check_circle_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_check_circle_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-xxxhdpi/ic_check_circle_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_empty_photo.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_grid_item_text.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/side_nav_bar.xml:
--------------------------------------------------------------------------------
1 |
3 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/app_bar_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/content_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_base.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/grid_item_image.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/grid_item_text.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
15 |
16 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/nav_header_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/activity_main_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/appbar_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/context_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/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 | #F02E6B
7 | #FFFFFF
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 160dp
5 |
6 | 16dp
7 | 16dp
8 |
9 | 2dp
10 | 4dp
11 |
12 | 40sp
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | IrregularGridView
3 | Delete
4 | Select
5 |
6 | Open navigation drawer
7 | Close navigation drawer
8 |
9 |
10 | Hello blank fragment
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/test/java/com/mwang/irregulargridview/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.mwang.irregulargridview;
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.0'
9 |
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 |
--------------------------------------------------------------------------------
/gif/Base.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/gif/Base.gif
--------------------------------------------------------------------------------
/gif/Gallery.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/gif/Gallery.gif
--------------------------------------------------------------------------------
/gif/test.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/gif/test.gif
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ## Project-wide Gradle settings.
2 | #
3 | # For more details on how to configure your build environment visit
4 | # http://www.gradle.org/docs/current/userguide/build_environment.html
5 | #
6 | # Specifies the JVM arguments used for the daemon process.
7 | # The setting is particularly useful for tweaking memory settings.
8 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
9 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
10 | #
11 | # When configured, Gradle will run in incubating parallel mode.
12 | # This option should only be used with decoupled projects. More details, visit
13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
14 | # org.gradle.parallel=true
15 | #Tue Apr 12 23:12:15 CST 2016
16 | systemProp.http.proxyHost=127.0.0.1
17 | systemProp.http.proxyPort=1080
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Huskyyy/IrregularGridView/f270fc3430e93eea1aa24f0d5f4789c5703c4f0a/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Jun 13 13:19:16 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------