├── .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 | 12 | 13 | 14 | 26 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 53 | 54 | 55 | 56 | 57 | 1.8 58 | 59 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 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 | 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 | ![image](https://github.com/MWang1991/IrregularGridView/blob/master/gif/Base.gif ) ![image](https://github.com/MWang1991/IrregularGridView/blob/master/gif/Gallery.gif ) 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 | 3 | 4 | 5 | 8 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/menu/appbar_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/menu/context_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | -------------------------------------------------------------------------------- /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 |