├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── build.gradle ├── core ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── lucasr │ │ └── twowayview │ │ ├── ClickItemTouchListener.java │ │ ├── ItemClickSupport.java │ │ ├── ItemSelectionSupport.java │ │ └── TwoWayLayoutManager.java │ └── res │ └── values │ ├── attrs.xml │ └── ids.xml ├── gradle.properties ├── gradle ├── scripts │ └── gradle-mvn-push.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images └── sample.png ├── layouts ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── lucasr │ │ └── twowayview │ │ └── widget │ │ ├── BaseLayoutManager.java │ │ ├── DividerItemDecoration.java │ │ ├── GridLayoutManager.java │ │ ├── ItemEntries.java │ │ ├── ItemSpacingOffsets.java │ │ ├── Lanes.java │ │ ├── ListLayoutManager.java │ │ ├── SpacingItemDecoration.java │ │ ├── SpannableGridLayoutManager.java │ │ ├── StaggeredGridLayoutManager.java │ │ └── TwoWayView.java │ └── res │ └── values │ └── attrs.xml ├── sample ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── lucasr │ │ └── twowayview │ │ └── sample │ │ ├── LayoutAdapter.java │ │ ├── LayoutFragment.java │ │ └── MainActivity.java │ └── res │ ├── drawable-hdpi │ ├── ic_grid.png │ ├── ic_launcher.png │ ├── ic_list.png │ ├── ic_spannable.png │ └── ic_staggered.png │ ├── drawable-mdpi │ ├── ic_grid.png │ ├── ic_launcher.png │ ├── ic_list.png │ ├── ic_spannable.png │ └── ic_staggered.png │ ├── drawable-xhdpi │ ├── ic_grid.png │ ├── ic_launcher.png │ ├── ic_list.png │ ├── ic_spannable.png │ └── ic_staggered.png │ ├── drawable-xxhdpi │ ├── ic_grid.png │ ├── ic_list.png │ ├── ic_spannable.png │ └── ic_staggered.png │ ├── drawable │ ├── divider.xml │ └── item_background.xml │ ├── layout │ ├── activity_main.xml │ ├── item.xml │ ├── layout_grid.xml │ ├── layout_list.xml │ ├── layout_spannable_grid.xml │ └── layout_staggered_grid.xml │ ├── values-land │ └── styles.xml │ ├── values-port │ └── styles.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | gen 3 | .project 4 | .classpath 5 | .settings 6 | .idea 7 | *.iml 8 | *.ipr 9 | *.iws 10 | out 11 | target 12 | release.properties 13 | pom.xml.* 14 | build.xml 15 | local.properties 16 | proguard.cfg 17 | .DS_Store 18 | .gradle 19 | build 20 | library/build 21 | sample/build 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | android: 4 | components: 5 | - build-tools-21.1.2 6 | - android-21 7 | - extra-google-m2repository 8 | - extra-android-m2repository 9 | script: ./gradlew build 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | Version 0.1.2 5 | ------------- 6 | 7 | * Add smoothScrollBy() and smoothScrollToPosition() support 8 | * Misc bug fixes 9 | 10 | Version 0.1.1 11 | ------------- 12 | 13 | * Misc bug fixes 14 | 15 | Version 0.1.0 16 | ------------- 17 | 18 | Initial release. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TwoWayView 2 | ========== 3 | 4 | *RecyclerView* made simple. 5 | 6 | ![](images/sample.png) 7 | 8 | Features 9 | ======== 10 | 11 | * A *LayoutManager* base class that greatly simplifies the development of custom layouts for *RecyclerView* 12 | * A collection of feature-complete stock layouts including: 13 | * List 14 | * Grid 15 | * Staggered Grid 16 | * Spannable Grid 17 | * A collection of stock item decorations including: 18 | * Item spacing 19 | * Horizontal/vertical dividers. 20 | * ListView-style pluggable APIs for: 21 | * Item click and long click support e.g. *OnItemClickListener* and *OnItemLongClickListener*. 22 | * Item selection (single and multiple) support e.g. *setChoiceMode()*, *setItemChecked(int, boolean)*, etc. 23 | 24 | Snapshot 25 | ======== 26 | 27 | The new API is still under heavy development but you can try it now via Maven Central snapshots. 28 | 29 | Gradle: 30 | ```groovy 31 | repositories { 32 | maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } 33 | } 34 | 35 | dependencies { 36 | compile 'org.lucasr.twowayview:core:1.0.0-SNAPSHOT@aar' 37 | compile 'org.lucasr.twowayview:layouts:1.0.0-SNAPSHOT@aar' 38 | } 39 | ``` 40 | 41 | Stable Release 42 | ============== 43 | 44 | TwoWayView used to be a standalone *AdapterView* implementation. You can grab it here from Maven Central as follows. 45 | 46 | Grab via Maven: 47 | ```xml 48 | 49 | org.lucasr.twowayview 50 | twowayview 51 | 0.1.4 52 | 53 | ``` 54 | 55 | Gradle: 56 | ```groovy 57 | compile 'org.lucasr.twowayview:twowayview:0.1.4' 58 | ``` 59 | 60 | If you are using ProGuard add the following line to the rules: 61 | ```groovy 62 | -keep class org.lucasr.twowayview.** { *; } 63 | ``` 64 | 65 | Want to help? 66 | ============= 67 | 68 | File new issues to discuss specific aspects of the API and to propose new 69 | features. 70 | 71 | License 72 | ======= 73 | 74 | Copyright (C) 2013 Lucas Rocha 75 | 76 | TwoWayView's code is based on bits and pieces of Android's 77 | AbsListView, Listview, and StaggeredGridView. 78 | 79 | Copyright (C) 2012 The Android Open Source Project 80 | 81 | Licensed under the Apache License, Version 2.0 (the "License"); 82 | you may not use this file except in compliance with the License. 83 | You may obtain a copy of the License at 84 | 85 | http://www.apache.org/licenses/LICENSE-2.0 86 | 87 | Unless required by applicable law or agreed to in writing, software 88 | distributed under the License is distributed on an "AS IS" BASIS, 89 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 90 | See the License for the specific language governing permissions and 91 | limitations under the License. 92 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:1.0.0' 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | repositories { 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | compile 'com.android.support:recyclerview-v7:21.0.0' 9 | } 10 | 11 | android { 12 | compileSdkVersion 21 13 | buildToolsVersion "21.1.2" 14 | resourcePrefix 'twowayview_' 15 | 16 | defaultConfig { 17 | minSdkVersion 10 18 | targetSdkVersion 21 19 | } 20 | } 21 | 22 | apply from: "${rootDir}/gradle/scripts/gradle-mvn-push.gradle" 23 | -------------------------------------------------------------------------------- /core/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=TwoWayView Core Library 2 | POM_ARTIFACT_ID=core 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /core/src/main/java/org/lucasr/twowayview/ClickItemTouchListener.java: -------------------------------------------------------------------------------- 1 | package org.lucasr.twowayview; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.support.v4.view.GestureDetectorCompat; 6 | import android.support.v4.view.MotionEventCompat; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.support.v7.widget.RecyclerView.OnItemTouchListener; 9 | import android.view.GestureDetector.SimpleOnGestureListener; 10 | import android.view.MotionEvent; 11 | import android.view.View; 12 | 13 | abstract class ClickItemTouchListener implements OnItemTouchListener { 14 | private static final String LOGTAG = "ClickItemTouchListener"; 15 | 16 | private final GestureDetectorCompat mGestureDetector; 17 | 18 | ClickItemTouchListener(RecyclerView hostView) { 19 | mGestureDetector = new ItemClickGestureDetector(hostView.getContext(), 20 | new ItemClickGestureListener(hostView)); 21 | } 22 | 23 | private boolean isAttachedToWindow(RecyclerView hostView) { 24 | if (Build.VERSION.SDK_INT >= 19) { 25 | return hostView.isAttachedToWindow(); 26 | } else { 27 | return (hostView.getHandler() != null); 28 | } 29 | } 30 | 31 | private boolean hasAdapter(RecyclerView hostView) { 32 | return (hostView.getAdapter() != null); 33 | } 34 | 35 | @Override 36 | public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) { 37 | if (!isAttachedToWindow(recyclerView) || !hasAdapter(recyclerView)) { 38 | return false; 39 | } 40 | 41 | mGestureDetector.onTouchEvent(event); 42 | return false; 43 | } 44 | 45 | @Override 46 | public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) { 47 | // We can silently track tap and and long presses by silently 48 | // intercepting touch events in the host RecyclerView. 49 | } 50 | 51 | abstract boolean performItemClick(RecyclerView parent, View view, int position, long id); 52 | abstract boolean performItemLongClick(RecyclerView parent, View view, int position, long id); 53 | 54 | private class ItemClickGestureDetector extends GestureDetectorCompat { 55 | private final ItemClickGestureListener mGestureListener; 56 | 57 | public ItemClickGestureDetector(Context context, ItemClickGestureListener listener) { 58 | super(context, listener); 59 | mGestureListener = listener; 60 | } 61 | 62 | @Override 63 | public boolean onTouchEvent(MotionEvent event) { 64 | final boolean handled = super.onTouchEvent(event); 65 | 66 | final int action = event.getAction() & MotionEventCompat.ACTION_MASK; 67 | if (action == MotionEvent.ACTION_UP) { 68 | mGestureListener.dispatchSingleTapUpIfNeeded(event); 69 | } 70 | 71 | return handled; 72 | } 73 | } 74 | 75 | private class ItemClickGestureListener extends SimpleOnGestureListener { 76 | private final RecyclerView mHostView; 77 | private View mTargetChild; 78 | 79 | public ItemClickGestureListener(RecyclerView hostView) { 80 | mHostView = hostView; 81 | } 82 | 83 | public void dispatchSingleTapUpIfNeeded(MotionEvent event) { 84 | // When the long press hook is called but the long press listener 85 | // returns false, the target child will be left around to be 86 | // handled later. In this case, we should still treat the gesture 87 | // as potential item click. 88 | if (mTargetChild != null) { 89 | onSingleTapUp(event); 90 | } 91 | } 92 | 93 | @Override 94 | public boolean onDown(MotionEvent event) { 95 | final int x = (int) event.getX(); 96 | final int y = (int) event.getY(); 97 | 98 | mTargetChild = mHostView.findChildViewUnder(x, y); 99 | return (mTargetChild != null); 100 | } 101 | 102 | @Override 103 | public void onShowPress(MotionEvent event) { 104 | if (mTargetChild != null) { 105 | mTargetChild.setPressed(true); 106 | } 107 | } 108 | 109 | @Override 110 | public boolean onSingleTapUp(MotionEvent event) { 111 | boolean handled = false; 112 | 113 | if (mTargetChild != null) { 114 | mTargetChild.setPressed(false); 115 | 116 | final int position = mHostView.getChildPosition(mTargetChild); 117 | final long id = mHostView.getAdapter().getItemId(position); 118 | handled = performItemClick(mHostView, mTargetChild, position, id); 119 | 120 | mTargetChild = null; 121 | } 122 | 123 | return handled; 124 | } 125 | 126 | @Override 127 | public boolean onScroll(MotionEvent event, MotionEvent event2, float v, float v2) { 128 | if (mTargetChild != null) { 129 | mTargetChild.setPressed(false); 130 | mTargetChild = null; 131 | 132 | return true; 133 | } 134 | 135 | return false; 136 | } 137 | 138 | @Override 139 | public void onLongPress(MotionEvent event) { 140 | if (mTargetChild == null) { 141 | return; 142 | } 143 | 144 | final int position = mHostView.getChildPosition(mTargetChild); 145 | final long id = mHostView.getAdapter().getItemId(position); 146 | final boolean handled = performItemLongClick(mHostView, mTargetChild, position, id); 147 | 148 | if (handled) { 149 | mTargetChild.setPressed(false); 150 | mTargetChild = null; 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /core/src/main/java/org/lucasr/twowayview/ItemClickSupport.java: -------------------------------------------------------------------------------- 1 | package org.lucasr.twowayview; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.HapticFeedbackConstants; 5 | import android.view.SoundEffectConstants; 6 | import android.view.View; 7 | 8 | public class ItemClickSupport { 9 | /** 10 | * Interface definition for a callback to be invoked when an item in the 11 | * RecyclerView has been clicked. 12 | */ 13 | public interface OnItemClickListener { 14 | /** 15 | * Callback method to be invoked when an item in the RecyclerView 16 | * has been clicked. 17 | * 18 | * @param parent The RecyclerView where the click happened. 19 | * @param view The view within the RecyclerView that was clicked 20 | * @param position The position of the view in the adapter. 21 | * @param id The row id of the item that was clicked. 22 | */ 23 | void onItemClick(RecyclerView parent, View view, int position, long id); 24 | } 25 | 26 | /** 27 | * Interface definition for a callback to be invoked when an item in the 28 | * RecyclerView has been clicked and held. 29 | */ 30 | public interface OnItemLongClickListener { 31 | /** 32 | * Callback method to be invoked when an item in the RecyclerView 33 | * has been clicked and held. 34 | * 35 | * @param parent The RecyclerView where the click happened 36 | * @param view The view within the RecyclerView that was clicked 37 | * @param position The position of the view in the list 38 | * @param id The row id of the item that was clicked 39 | * 40 | * @return true if the callback consumed the long click, false otherwise 41 | */ 42 | boolean onItemLongClick(RecyclerView parent, View view, int position, long id); 43 | } 44 | 45 | private final RecyclerView mRecyclerView; 46 | private final TouchListener mTouchListener; 47 | 48 | private OnItemClickListener mItemClickListener; 49 | private OnItemLongClickListener mItemLongClickListener; 50 | 51 | private ItemClickSupport(RecyclerView recyclerView) { 52 | mRecyclerView = recyclerView; 53 | 54 | mTouchListener = new TouchListener(recyclerView); 55 | recyclerView.addOnItemTouchListener(mTouchListener); 56 | } 57 | 58 | /** 59 | * Register a callback to be invoked when an item in the 60 | * RecyclerView has been clicked. 61 | * 62 | * @param listener The callback that will be invoked. 63 | */ 64 | public void setOnItemClickListener(OnItemClickListener listener) { 65 | mItemClickListener = listener; 66 | } 67 | 68 | /** 69 | * Register a callback to be invoked when an item in the 70 | * RecyclerView has been clicked and held. 71 | * 72 | * @param listener The callback that will be invoked. 73 | */ 74 | public void setOnItemLongClickListener(OnItemLongClickListener listener) { 75 | if (!mRecyclerView.isLongClickable()) { 76 | mRecyclerView.setLongClickable(true); 77 | } 78 | 79 | mItemLongClickListener = listener; 80 | } 81 | 82 | public static ItemClickSupport addTo(RecyclerView recyclerView) { 83 | ItemClickSupport itemClickSupport = from(recyclerView); 84 | if (itemClickSupport == null) { 85 | itemClickSupport = new ItemClickSupport(recyclerView); 86 | recyclerView.setTag(R.id.twowayview_item_click_support, itemClickSupport); 87 | } else { 88 | // TODO: Log warning 89 | } 90 | 91 | return itemClickSupport; 92 | } 93 | 94 | public static void removeFrom(RecyclerView recyclerView) { 95 | final ItemClickSupport itemClickSupport = from(recyclerView); 96 | if (itemClickSupport == null) { 97 | // TODO: Log warning 98 | return; 99 | } 100 | 101 | recyclerView.removeOnItemTouchListener(itemClickSupport.mTouchListener); 102 | recyclerView.setTag(R.id.twowayview_item_click_support, null); 103 | } 104 | 105 | public static ItemClickSupport from(RecyclerView recyclerView) { 106 | if (recyclerView == null) { 107 | return null; 108 | } 109 | 110 | return (ItemClickSupport) recyclerView.getTag(R.id.twowayview_item_click_support); 111 | } 112 | 113 | private class TouchListener extends ClickItemTouchListener { 114 | TouchListener(RecyclerView recyclerView) { 115 | super(recyclerView); 116 | } 117 | 118 | @Override 119 | boolean performItemClick(RecyclerView parent, View view, int position, long id) { 120 | if (mItemClickListener != null) { 121 | view.playSoundEffect(SoundEffectConstants.CLICK); 122 | mItemClickListener.onItemClick(parent, view, position, id); 123 | return true; 124 | } 125 | 126 | return false; 127 | } 128 | 129 | @Override 130 | boolean performItemLongClick(RecyclerView parent, View view, int position, long id) { 131 | if (mItemLongClickListener != null) { 132 | view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 133 | return mItemLongClickListener.onItemLongClick(parent, view, position, id); 134 | } 135 | 136 | return false; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /core/src/main/java/org/lucasr/twowayview/ItemSelectionSupport.java: -------------------------------------------------------------------------------- 1 | package org.lucasr.twowayview; 2 | 3 | import android.annotation.TargetApi; 4 | import android.os.Build; 5 | import android.os.Bundle; 6 | import android.os.Parcel; 7 | import android.os.Parcelable; 8 | import android.support.v4.util.LongSparseArray; 9 | import android.support.v7.widget.RecyclerView; 10 | import android.support.v7.widget.RecyclerView.Adapter; 11 | import android.util.SparseBooleanArray; 12 | import android.view.View; 13 | import android.widget.Checkable; 14 | 15 | import static android.os.Build.VERSION_CODES.HONEYCOMB; 16 | 17 | public class ItemSelectionSupport { 18 | public static final int INVALID_POSITION = -1; 19 | 20 | public static enum ChoiceMode { 21 | NONE, 22 | SINGLE, 23 | MULTIPLE 24 | } 25 | 26 | private final RecyclerView mRecyclerView; 27 | private final TouchListener mTouchListener; 28 | 29 | private ChoiceMode mChoiceMode = ChoiceMode.NONE; 30 | private CheckedStates mCheckedStates; 31 | private CheckedIdStates mCheckedIdStates; 32 | private int mCheckedCount; 33 | 34 | private static final String STATE_KEY_CHOICE_MODE = "choiceMode"; 35 | private static final String STATE_KEY_CHECKED_STATES = "checkedStates"; 36 | private static final String STATE_KEY_CHECKED_ID_STATES = "checkedIdStates"; 37 | private static final String STATE_KEY_CHECKED_COUNT = "checkedCount"; 38 | 39 | private static final int CHECK_POSITION_SEARCH_DISTANCE = 20; 40 | 41 | private ItemSelectionSupport(RecyclerView recyclerView) { 42 | mRecyclerView = recyclerView; 43 | 44 | mTouchListener = new TouchListener(recyclerView); 45 | recyclerView.addOnItemTouchListener(mTouchListener); 46 | } 47 | 48 | private void updateOnScreenCheckedViews() { 49 | final int count = mRecyclerView.getChildCount(); 50 | for (int i = 0; i < count; i++) { 51 | final View child = mRecyclerView.getChildAt(i); 52 | final int position = mRecyclerView.getChildPosition(child); 53 | setViewChecked(child, mCheckedStates.get(position)); 54 | } 55 | } 56 | 57 | /** 58 | * Returns the number of items currently selected. This will only be valid 59 | * if the choice mode is not {@link ChoiceMode#NONE} (default). 60 | * 61 | *

To determine the specific items that are currently selected, use one of 62 | * the getChecked* methods. 63 | * 64 | * @return The number of items currently selected 65 | * 66 | * @see #getCheckedItemPosition() 67 | * @see #getCheckedItemPositions() 68 | * @see #getCheckedItemIds() 69 | */ 70 | public int getCheckedItemCount() { 71 | return mCheckedCount; 72 | } 73 | 74 | /** 75 | * Returns the checked state of the specified position. The result is only 76 | * valid if the choice mode has been set to {@link ChoiceMode#SINGLE} 77 | * or {@link ChoiceMode#MULTIPLE}. 78 | * 79 | * @param position The item whose checked state to return 80 | * @return The item's checked state or false if choice mode 81 | * is invalid 82 | * 83 | * @see #setChoiceMode(ChoiceMode) 84 | */ 85 | public boolean isItemChecked(int position) { 86 | if (mChoiceMode != ChoiceMode.NONE && mCheckedStates != null) { 87 | return mCheckedStates.get(position); 88 | } 89 | 90 | return false; 91 | } 92 | 93 | /** 94 | * Returns the currently checked item. The result is only valid if the choice 95 | * mode has been set to {@link ChoiceMode#SINGLE}. 96 | * 97 | * @return The position of the currently checked item or 98 | * {@link #INVALID_POSITION} if nothing is selected 99 | * 100 | * @see #setChoiceMode(ChoiceMode) 101 | */ 102 | public int getCheckedItemPosition() { 103 | if (mChoiceMode == ChoiceMode.SINGLE && mCheckedStates != null && mCheckedStates.size() == 1) { 104 | return mCheckedStates.keyAt(0); 105 | } 106 | 107 | return INVALID_POSITION; 108 | } 109 | 110 | /** 111 | * Returns the set of checked items in the list. The result is only valid if 112 | * the choice mode has not been set to {@link ChoiceMode#NONE}. 113 | * 114 | * @return A SparseBooleanArray which will return true for each call to 115 | * get(int position) where position is a position in the list, 116 | * or null if the choice mode is set to 117 | * {@link ChoiceMode#NONE}. 118 | */ 119 | public SparseBooleanArray getCheckedItemPositions() { 120 | if (mChoiceMode != ChoiceMode.NONE) { 121 | return mCheckedStates; 122 | } 123 | 124 | return null; 125 | } 126 | 127 | /** 128 | * Returns the set of checked items ids. The result is only valid if the 129 | * choice mode has not been set to {@link ChoiceMode#NONE} and the adapter 130 | * has stable IDs. 131 | * 132 | * @return A new array which contains the id of each checked item in the 133 | * list. 134 | * 135 | * @see android.support.v7.widget.RecyclerView.Adapter#hasStableIds() 136 | */ 137 | public long[] getCheckedItemIds() { 138 | if (mChoiceMode == ChoiceMode.NONE 139 | || mCheckedIdStates == null || mRecyclerView.getAdapter() == null) { 140 | return new long[0]; 141 | } 142 | 143 | final int count = mCheckedIdStates.size(); 144 | final long[] ids = new long[count]; 145 | 146 | for (int i = 0; i < count; i++) { 147 | ids[i] = mCheckedIdStates.keyAt(i); 148 | } 149 | 150 | return ids; 151 | } 152 | 153 | /** 154 | * Sets the checked state of the specified position. The is only valid if 155 | * the choice mode has been set to {@link ChoiceMode#SINGLE} or 156 | * {@link ChoiceMode#MULTIPLE}. 157 | * 158 | * @param position The item whose checked state is to be checked 159 | * @param checked The new checked state for the item 160 | */ 161 | public void setItemChecked(int position, boolean checked) { 162 | if (mChoiceMode == ChoiceMode.NONE) { 163 | return; 164 | } 165 | 166 | final Adapter adapter = mRecyclerView.getAdapter(); 167 | 168 | if (mChoiceMode == ChoiceMode.MULTIPLE) { 169 | boolean oldValue = mCheckedStates.get(position); 170 | mCheckedStates.put(position, checked); 171 | 172 | if (mCheckedIdStates != null && adapter.hasStableIds()) { 173 | if (checked) { 174 | mCheckedIdStates.put(adapter.getItemId(position), position); 175 | } else { 176 | mCheckedIdStates.delete(adapter.getItemId(position)); 177 | } 178 | } 179 | 180 | if (oldValue != checked) { 181 | if (checked) { 182 | mCheckedCount++; 183 | } else { 184 | mCheckedCount--; 185 | } 186 | } 187 | } else { 188 | boolean updateIds = mCheckedIdStates != null && adapter.hasStableIds(); 189 | 190 | // Clear all values if we're checking something, or unchecking the currently 191 | // selected item 192 | if (checked || isItemChecked(position)) { 193 | mCheckedStates.clear(); 194 | 195 | if (updateIds) { 196 | mCheckedIdStates.clear(); 197 | } 198 | } 199 | 200 | // This may end up selecting the checked we just cleared but this way 201 | // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on 202 | if (checked) { 203 | mCheckedStates.put(position, true); 204 | 205 | if (updateIds) { 206 | mCheckedIdStates.put(adapter.getItemId(position), position); 207 | } 208 | 209 | mCheckedCount = 1; 210 | } else if (mCheckedStates.size() == 0 || !mCheckedStates.valueAt(0)) { 211 | mCheckedCount = 0; 212 | } 213 | } 214 | 215 | updateOnScreenCheckedViews(); 216 | } 217 | 218 | @TargetApi(HONEYCOMB) 219 | public void setViewChecked(View view, boolean checked) { 220 | if (view instanceof Checkable) { 221 | ((Checkable) view).setChecked(checked); 222 | } else if (Build.VERSION.SDK_INT >= HONEYCOMB) { 223 | view.setActivated(checked); 224 | } 225 | } 226 | 227 | /** 228 | * Clears any choices previously set. 229 | */ 230 | public void clearChoices() { 231 | if (mCheckedStates != null) { 232 | mCheckedStates.clear(); 233 | } 234 | 235 | if (mCheckedIdStates != null) { 236 | mCheckedIdStates.clear(); 237 | } 238 | 239 | mCheckedCount = 0; 240 | updateOnScreenCheckedViews(); 241 | } 242 | 243 | /** 244 | * Returns the current choice mode. 245 | * 246 | * @see #setChoiceMode(ChoiceMode) 247 | */ 248 | public ChoiceMode getChoiceMode() { 249 | return mChoiceMode; 250 | } 251 | 252 | /** 253 | * Defines the choice behavior for the List. By default, Lists do not have any choice behavior 254 | * ({@link ChoiceMode#NONE}). By setting the choiceMode to {@link ChoiceMode#SINGLE}, the 255 | * List allows up to one item to be in a chosen state. By setting the choiceMode to 256 | * {@link ChoiceMode#MULTIPLE}, the list allows any number of items to be chosen. 257 | * 258 | * @param choiceMode One of {@link ChoiceMode#NONE}, {@link ChoiceMode#SINGLE}, or 259 | * {@link ChoiceMode#MULTIPLE} 260 | */ 261 | public void setChoiceMode(ChoiceMode choiceMode) { 262 | if (mChoiceMode == choiceMode) { 263 | return; 264 | } 265 | 266 | mChoiceMode = choiceMode; 267 | 268 | if (mChoiceMode != ChoiceMode.NONE) { 269 | if (mCheckedStates == null) { 270 | mCheckedStates = new CheckedStates(); 271 | } 272 | 273 | final Adapter adapter = mRecyclerView.getAdapter(); 274 | if (mCheckedIdStates == null && adapter != null && adapter.hasStableIds()) { 275 | mCheckedIdStates = new CheckedIdStates(); 276 | } 277 | } 278 | } 279 | 280 | public void onAdapterDataChanged() { 281 | final Adapter adapter = mRecyclerView.getAdapter(); 282 | if (mChoiceMode == ChoiceMode.NONE || adapter == null || !adapter.hasStableIds()) { 283 | return; 284 | } 285 | 286 | final int itemCount = adapter.getItemCount(); 287 | 288 | // Clear out the positional check states, we'll rebuild it below from IDs. 289 | mCheckedStates.clear(); 290 | 291 | for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) { 292 | final long currentId = mCheckedIdStates.keyAt(checkedIndex); 293 | final int currentPosition = mCheckedIdStates.valueAt(checkedIndex); 294 | 295 | final long newPositionId = adapter.getItemId(currentPosition); 296 | if (currentId != newPositionId) { 297 | // Look around to see if the ID is nearby. If not, uncheck it. 298 | final int start = Math.max(0, currentPosition - CHECK_POSITION_SEARCH_DISTANCE); 299 | final int end = Math.min(currentPosition + CHECK_POSITION_SEARCH_DISTANCE, itemCount); 300 | 301 | boolean found = false; 302 | for (int searchPos = start; searchPos < end; searchPos++) { 303 | final long searchId = adapter.getItemId(searchPos); 304 | if (currentId == searchId) { 305 | found = true; 306 | mCheckedStates.put(searchPos, true); 307 | mCheckedIdStates.setValueAt(checkedIndex, searchPos); 308 | break; 309 | } 310 | } 311 | 312 | if (!found) { 313 | mCheckedIdStates.delete(currentId); 314 | mCheckedCount--; 315 | checkedIndex--; 316 | } 317 | } else { 318 | mCheckedStates.put(currentPosition, true); 319 | } 320 | } 321 | } 322 | 323 | public Bundle onSaveInstanceState() { 324 | final Bundle state = new Bundle(); 325 | 326 | state.putInt(STATE_KEY_CHOICE_MODE, mChoiceMode.ordinal()); 327 | state.putParcelable(STATE_KEY_CHECKED_STATES, mCheckedStates); 328 | state.putParcelable(STATE_KEY_CHECKED_ID_STATES, mCheckedIdStates); 329 | state.putInt(STATE_KEY_CHECKED_COUNT, mCheckedCount); 330 | 331 | return state; 332 | } 333 | 334 | public void onRestoreInstanceState(Bundle state) { 335 | mChoiceMode = ChoiceMode.values()[state.getInt(STATE_KEY_CHOICE_MODE)]; 336 | mCheckedStates = state.getParcelable(STATE_KEY_CHECKED_STATES); 337 | mCheckedIdStates = state.getParcelable(STATE_KEY_CHECKED_ID_STATES); 338 | mCheckedCount = state.getInt(STATE_KEY_CHECKED_COUNT); 339 | 340 | // TODO confirm ids here 341 | } 342 | 343 | public static ItemSelectionSupport addTo(RecyclerView recyclerView) { 344 | ItemSelectionSupport itemSelectionSupport = from(recyclerView); 345 | if (itemSelectionSupport == null) { 346 | itemSelectionSupport = new ItemSelectionSupport(recyclerView); 347 | recyclerView.setTag(R.id.twowayview_item_selection_support, itemSelectionSupport); 348 | } else { 349 | // TODO: Log warning 350 | } 351 | 352 | return itemSelectionSupport; 353 | } 354 | 355 | public static void removeFrom(RecyclerView recyclerView) { 356 | final ItemSelectionSupport itemSelection = from(recyclerView); 357 | if (itemSelection == null) { 358 | // TODO: Log warning 359 | return; 360 | } 361 | 362 | itemSelection.clearChoices(); 363 | 364 | recyclerView.removeOnItemTouchListener(itemSelection.mTouchListener); 365 | recyclerView.setTag(R.id.twowayview_item_selection_support, null); 366 | } 367 | 368 | public static ItemSelectionSupport from(RecyclerView recyclerView) { 369 | if (recyclerView == null) { 370 | return null; 371 | } 372 | 373 | return (ItemSelectionSupport) recyclerView.getTag(R.id.twowayview_item_selection_support); 374 | } 375 | 376 | private static class CheckedStates extends SparseBooleanArray implements Parcelable { 377 | private static final int FALSE = 0; 378 | private static final int TRUE = 1; 379 | 380 | public CheckedStates() { 381 | super(); 382 | } 383 | 384 | private CheckedStates(Parcel in) { 385 | final int size = in.readInt(); 386 | if (size > 0) { 387 | for (int i = 0; i < size; i++) { 388 | final int key = in.readInt(); 389 | final boolean value = (in.readInt() == TRUE); 390 | put(key, value); 391 | } 392 | } 393 | } 394 | 395 | @Override 396 | public int describeContents() { 397 | return 0; 398 | } 399 | 400 | @Override 401 | public void writeToParcel(Parcel parcel, int flags) { 402 | final int size = size(); 403 | parcel.writeInt(size); 404 | 405 | for (int i = 0; i < size; i++) { 406 | parcel.writeInt(keyAt(i)); 407 | parcel.writeInt(valueAt(i) ? TRUE : FALSE); 408 | } 409 | } 410 | 411 | public static final Parcelable.Creator CREATOR 412 | = new Parcelable.Creator() { 413 | @Override 414 | public CheckedStates createFromParcel(Parcel in) { 415 | return new CheckedStates(in); 416 | } 417 | 418 | @Override 419 | public CheckedStates[] newArray(int size) { 420 | return new CheckedStates[size]; 421 | } 422 | }; 423 | } 424 | 425 | private static class CheckedIdStates extends LongSparseArray implements Parcelable { 426 | public CheckedIdStates() { 427 | super(); 428 | } 429 | 430 | private CheckedIdStates(Parcel in) { 431 | final int size = in.readInt(); 432 | if (size > 0) { 433 | for (int i = 0; i < size; i++) { 434 | final long key = in.readLong(); 435 | final int value = in.readInt(); 436 | put(key, value); 437 | } 438 | } 439 | } 440 | 441 | @Override 442 | public int describeContents() { 443 | return 0; 444 | } 445 | 446 | @Override 447 | public void writeToParcel(Parcel parcel, int flags) { 448 | final int size = size(); 449 | parcel.writeInt(size); 450 | 451 | for (int i = 0; i < size; i++) { 452 | parcel.writeLong(keyAt(i)); 453 | parcel.writeInt(valueAt(i)); 454 | } 455 | } 456 | 457 | public static final Creator CREATOR 458 | = new Creator() { 459 | @Override 460 | public CheckedIdStates createFromParcel(Parcel in) { 461 | return new CheckedIdStates(in); 462 | } 463 | 464 | @Override 465 | public CheckedIdStates[] newArray(int size) { 466 | return new CheckedIdStates[size]; 467 | } 468 | }; 469 | } 470 | 471 | private class TouchListener extends ClickItemTouchListener { 472 | TouchListener(RecyclerView recyclerView) { 473 | super(recyclerView); 474 | } 475 | 476 | @Override 477 | boolean performItemClick(RecyclerView parent, View view, int position, long id) { 478 | final Adapter adapter = mRecyclerView.getAdapter(); 479 | boolean checkedStateChanged = false; 480 | 481 | if (mChoiceMode == ChoiceMode.MULTIPLE) { 482 | boolean checked = !mCheckedStates.get(position, false); 483 | mCheckedStates.put(position, checked); 484 | 485 | if (mCheckedIdStates != null && adapter.hasStableIds()) { 486 | if (checked) { 487 | mCheckedIdStates.put(adapter.getItemId(position), position); 488 | } else { 489 | mCheckedIdStates.delete(adapter.getItemId(position)); 490 | } 491 | } 492 | 493 | if (checked) { 494 | mCheckedCount++; 495 | } else { 496 | mCheckedCount--; 497 | } 498 | 499 | checkedStateChanged = true; 500 | } else if (mChoiceMode == ChoiceMode.SINGLE) { 501 | boolean checked = !mCheckedStates.get(position, false); 502 | if (checked) { 503 | mCheckedStates.clear(); 504 | mCheckedStates.put(position, true); 505 | 506 | if (mCheckedIdStates != null && adapter.hasStableIds()) { 507 | mCheckedIdStates.clear(); 508 | mCheckedIdStates.put(adapter.getItemId(position), position); 509 | } 510 | 511 | mCheckedCount = 1; 512 | } else if (mCheckedStates.size() == 0 || !mCheckedStates.valueAt(0)) { 513 | mCheckedCount = 0; 514 | } 515 | 516 | checkedStateChanged = true; 517 | } 518 | 519 | if (checkedStateChanged) { 520 | updateOnScreenCheckedViews(); 521 | } 522 | 523 | return false; 524 | } 525 | 526 | @Override 527 | boolean performItemLongClick(RecyclerView parent, View view, int position, long id) { 528 | return true; 529 | } 530 | } 531 | } 532 | -------------------------------------------------------------------------------- /core/src/main/java/org/lucasr/twowayview/TwoWayLayoutManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 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 | 17 | package org.lucasr.twowayview; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.graphics.PointF; 22 | import android.os.Bundle; 23 | import android.os.Parcel; 24 | import android.os.Parcelable; 25 | import android.support.v7.widget.LinearSmoothScroller; 26 | import android.support.v7.widget.RecyclerView; 27 | import android.support.v7.widget.RecyclerView.Adapter; 28 | import android.support.v7.widget.RecyclerView.LayoutManager; 29 | import android.support.v7.widget.RecyclerView.LayoutParams; 30 | import android.support.v7.widget.RecyclerView.Recycler; 31 | import android.support.v7.widget.RecyclerView.State; 32 | import android.support.v7.widget.RecyclerView.ViewHolder; 33 | import android.util.AttributeSet; 34 | import android.view.View; 35 | import android.view.ViewGroup.MarginLayoutParams; 36 | 37 | import java.util.List; 38 | 39 | public abstract class TwoWayLayoutManager extends LayoutManager { 40 | private static final String LOGTAG = "TwoWayLayoutManager"; 41 | 42 | public static enum Orientation { 43 | HORIZONTAL, 44 | VERTICAL 45 | } 46 | 47 | public static enum Direction { 48 | START, 49 | END 50 | } 51 | 52 | private RecyclerView mRecyclerView; 53 | 54 | private boolean mIsVertical = true; 55 | 56 | private SavedState mPendingSavedState = null; 57 | 58 | private int mPendingScrollPosition = RecyclerView.NO_POSITION; 59 | private int mPendingScrollOffset = 0; 60 | 61 | private int mLayoutStart; 62 | private int mLayoutEnd; 63 | 64 | public TwoWayLayoutManager(Context context, AttributeSet attrs) { 65 | this(context, attrs, 0); 66 | } 67 | 68 | public TwoWayLayoutManager(Context context, AttributeSet attrs, int defStyle) { 69 | final TypedArray a = 70 | context.obtainStyledAttributes(attrs, R.styleable.twowayview_TwoWayLayoutManager, defStyle, 0); 71 | 72 | final int indexCount = a.getIndexCount(); 73 | for (int i = 0; i < indexCount; i++) { 74 | final int attr = a.getIndex(i); 75 | 76 | if (attr == R.styleable.twowayview_TwoWayLayoutManager_android_orientation) { 77 | final int orientation = a.getInt(attr, -1); 78 | if (orientation >= 0) { 79 | setOrientation(Orientation.values()[orientation]); 80 | } 81 | } 82 | } 83 | 84 | a.recycle(); 85 | } 86 | 87 | public TwoWayLayoutManager(Orientation orientation) { 88 | mIsVertical = (orientation == Orientation.VERTICAL); 89 | } 90 | 91 | private int getTotalSpace() { 92 | if (mIsVertical) { 93 | return getHeight() - getPaddingBottom() - getPaddingTop(); 94 | } else { 95 | return getWidth() - getPaddingRight() - getPaddingLeft(); 96 | } 97 | } 98 | 99 | protected int getStartWithPadding() { 100 | return (mIsVertical ? getPaddingTop() : getPaddingLeft()); 101 | } 102 | 103 | protected int getEndWithPadding() { 104 | if (mIsVertical) { 105 | return (getHeight() - getPaddingBottom()); 106 | } else { 107 | return (getWidth() - getPaddingRight()); 108 | } 109 | } 110 | 111 | protected int getChildStart(View child) { 112 | return (mIsVertical ? getDecoratedTop(child) : getDecoratedLeft(child)); 113 | } 114 | 115 | protected int getChildEnd(View child) { 116 | return (mIsVertical ? getDecoratedBottom(child) : getDecoratedRight(child)); 117 | } 118 | 119 | protected Adapter getAdapter() { 120 | return (mRecyclerView != null ? mRecyclerView.getAdapter() : null); 121 | } 122 | 123 | private void offsetChildren(int offset) { 124 | if (mIsVertical) { 125 | offsetChildrenVertical(offset); 126 | } else { 127 | offsetChildrenHorizontal(offset); 128 | } 129 | 130 | mLayoutStart += offset; 131 | mLayoutEnd += offset; 132 | } 133 | 134 | private void recycleChildrenOutOfBounds(Direction direction, Recycler recycler) { 135 | if (direction == Direction.END) { 136 | recycleChildrenFromStart(direction, recycler); 137 | } else { 138 | recycleChildrenFromEnd(direction, recycler); 139 | } 140 | } 141 | 142 | private void recycleChildrenFromStart(Direction direction, Recycler recycler) { 143 | final int childCount = getChildCount(); 144 | final int childrenStart = getStartWithPadding(); 145 | 146 | int detachedCount = 0; 147 | for (int i = 0; i < childCount; i++) { 148 | final View child = getChildAt(i); 149 | final int childEnd = getChildEnd(child); 150 | 151 | if (childEnd >= childrenStart) { 152 | break; 153 | } 154 | 155 | detachedCount++; 156 | 157 | detachChild(child, direction); 158 | } 159 | 160 | while (--detachedCount >= 0) { 161 | final View child = getChildAt(0); 162 | removeAndRecycleView(child, recycler); 163 | updateLayoutEdgesFromRemovedChild(child, direction); 164 | } 165 | } 166 | 167 | private void recycleChildrenFromEnd(Direction direction, Recycler recycler) { 168 | final int childrenEnd = getEndWithPadding(); 169 | final int childCount = getChildCount(); 170 | 171 | int firstDetachedPos = 0; 172 | int detachedCount = 0; 173 | for (int i = childCount - 1; i >= 0; i--) { 174 | final View child = getChildAt(i); 175 | final int childStart = getChildStart(child); 176 | 177 | if (childStart <= childrenEnd) { 178 | break; 179 | } 180 | 181 | firstDetachedPos = i; 182 | detachedCount++; 183 | 184 | detachChild(child, direction); 185 | } 186 | 187 | while (--detachedCount >= 0) { 188 | final View child = getChildAt(firstDetachedPos); 189 | removeAndRecycleViewAt(firstDetachedPos, recycler); 190 | updateLayoutEdgesFromRemovedChild(child, direction); 191 | } 192 | } 193 | 194 | private int scrollBy(int delta, Recycler recycler, State state) { 195 | final int childCount = getChildCount(); 196 | if (childCount == 0 || delta == 0) { 197 | return 0; 198 | } 199 | 200 | final int start = getStartWithPadding(); 201 | final int end = getEndWithPadding(); 202 | final int firstPosition = getFirstVisiblePosition(); 203 | 204 | final int totalSpace = getTotalSpace(); 205 | if (delta < 0) { 206 | delta = Math.max(-(totalSpace - 1), delta); 207 | } else { 208 | delta = Math.min(totalSpace - 1, delta); 209 | } 210 | 211 | final boolean cannotScrollBackward = (firstPosition == 0 && 212 | mLayoutStart >= start && delta <= 0); 213 | final boolean cannotScrollForward = (firstPosition + childCount == state.getItemCount() && 214 | mLayoutEnd <= end && delta >= 0); 215 | 216 | if (cannotScrollForward || cannotScrollBackward) { 217 | return 0; 218 | } 219 | 220 | offsetChildren(-delta); 221 | 222 | final Direction direction = (delta > 0 ? Direction.END : Direction.START); 223 | recycleChildrenOutOfBounds(direction, recycler); 224 | 225 | final int absDelta = Math.abs(delta); 226 | if (canAddMoreViews(Direction.START, start - absDelta) || 227 | canAddMoreViews(Direction.END, end + absDelta)) { 228 | fillGap(direction, recycler, state); 229 | } 230 | 231 | return delta; 232 | } 233 | 234 | private void fillGap(Direction direction, Recycler recycler, State state) { 235 | final int childCount = getChildCount(); 236 | final int extraSpace = getExtraLayoutSpace(state); 237 | final int firstPosition = getFirstVisiblePosition(); 238 | 239 | if (direction == Direction.END) { 240 | fillAfter(firstPosition + childCount, recycler, state, extraSpace); 241 | correctTooHigh(childCount, recycler, state); 242 | } else { 243 | fillBefore(firstPosition - 1, recycler, extraSpace); 244 | correctTooLow(childCount, recycler, state); 245 | } 246 | } 247 | 248 | private void fillBefore(int pos, Recycler recycler) { 249 | fillBefore(pos, recycler, 0); 250 | } 251 | 252 | private void fillBefore(int position, Recycler recycler, int extraSpace) { 253 | final int limit = getStartWithPadding() - extraSpace; 254 | 255 | while (canAddMoreViews(Direction.START, limit) && position >= 0) { 256 | makeAndAddView(position, Direction.START, recycler); 257 | position--; 258 | } 259 | } 260 | 261 | private void fillAfter(int pos, Recycler recycler, State state) { 262 | fillAfter(pos, recycler, state, 0); 263 | } 264 | 265 | private void fillAfter(int position, Recycler recycler, State state, int extraSpace) { 266 | final int limit = getEndWithPadding() + extraSpace; 267 | 268 | final int itemCount = state.getItemCount(); 269 | while (canAddMoreViews(Direction.END, limit) && position < itemCount) { 270 | makeAndAddView(position, Direction.END, recycler); 271 | position++; 272 | } 273 | } 274 | 275 | private void fillSpecific(int position, Recycler recycler, State state) { 276 | if (state.getItemCount() <= 0) { 277 | return; 278 | } 279 | 280 | makeAndAddView(position, Direction.END, recycler); 281 | 282 | final int extraSpaceBefore; 283 | final int extraSpaceAfter; 284 | 285 | final int extraSpace = getExtraLayoutSpace(state); 286 | if (state.getTargetScrollPosition() < position) { 287 | extraSpaceAfter = 0; 288 | extraSpaceBefore = extraSpace; 289 | } else { 290 | extraSpaceAfter = extraSpace; 291 | extraSpaceBefore = 0; 292 | } 293 | 294 | fillBefore(position - 1, recycler, extraSpaceBefore); 295 | 296 | // This will correct for the top of the first view not 297 | // touching the top of the parent. 298 | adjustViewsStartOrEnd(); 299 | 300 | fillAfter(position + 1, recycler, state, extraSpaceAfter); 301 | correctTooHigh(getChildCount(), recycler, state); 302 | } 303 | 304 | private void correctTooHigh(int childCount, Recycler recycler, State state) { 305 | // First see if the last item is visible. If it is not, it is OK for the 306 | // top of the list to be pushed up. 307 | final int lastPosition = getLastVisiblePosition(); 308 | if (lastPosition != state.getItemCount() - 1 || childCount == 0) { 309 | return; 310 | } 311 | 312 | // This is bottom of our drawable area. 313 | final int start = getStartWithPadding(); 314 | final int end = getEndWithPadding(); 315 | final int firstPosition = getFirstVisiblePosition(); 316 | 317 | // This is how far the end edge of the last view is from the end of the 318 | // drawable area. 319 | int endOffset = end - mLayoutEnd; 320 | 321 | // Make sure we are 1) Too high, and 2) Either there are more rows above the 322 | // first row or the first row is scrolled off the top of the drawable area 323 | if (endOffset > 0 && (firstPosition > 0 || mLayoutStart < start)) { 324 | if (firstPosition == 0) { 325 | // Don't pull the top too far down. 326 | endOffset = Math.min(endOffset, start - mLayoutStart); 327 | } 328 | 329 | // Move everything down 330 | offsetChildren(endOffset); 331 | 332 | if (firstPosition > 0) { 333 | // Fill the gap that was opened above first position with more 334 | // children, if possible. 335 | fillBefore(firstPosition - 1, recycler); 336 | 337 | // Close up the remaining gap. 338 | adjustViewsStartOrEnd(); 339 | } 340 | } 341 | } 342 | 343 | private void correctTooLow(int childCount, Recycler recycler, State state) { 344 | // First see if the first item is visible. If it is not, it is OK for the 345 | // end of the list to be pushed forward. 346 | final int firstPosition = getFirstVisiblePosition(); 347 | if (firstPosition != 0 || childCount == 0) { 348 | return; 349 | } 350 | 351 | final int start = getStartWithPadding(); 352 | final int end = getEndWithPadding(); 353 | final int itemCount = state.getItemCount(); 354 | final int lastPosition = getLastVisiblePosition(); 355 | 356 | // This is how far the start edge of the first view is from the start of the 357 | // drawable area. 358 | int startOffset = mLayoutStart - start; 359 | 360 | // Make sure we are 1) Too low, and 2) Either there are more columns/rows below the 361 | // last column/row or the last column/row is scrolled off the end of the 362 | // drawable area. 363 | if (startOffset > 0) { 364 | if (lastPosition < itemCount - 1 || mLayoutEnd > end) { 365 | if (lastPosition == itemCount - 1) { 366 | // Don't pull the bottom too far up. 367 | startOffset = Math.min(startOffset, mLayoutEnd - end); 368 | } 369 | 370 | // Move everything up. 371 | offsetChildren(-startOffset); 372 | 373 | if (lastPosition < itemCount - 1) { 374 | // Fill the gap that was opened below the last position with more 375 | // children, if possible. 376 | fillAfter(lastPosition + 1, recycler, state); 377 | 378 | // Close up the remaining gap. 379 | adjustViewsStartOrEnd(); 380 | } 381 | } else if (lastPosition == itemCount - 1) { 382 | adjustViewsStartOrEnd(); 383 | } 384 | } 385 | } 386 | 387 | private void adjustViewsStartOrEnd() { 388 | if (getChildCount() == 0) { 389 | return; 390 | } 391 | 392 | int delta = mLayoutStart - getStartWithPadding(); 393 | if (delta < 0) { 394 | // We only are looking to see if we are too low, not too high 395 | delta = 0; 396 | } 397 | 398 | if (delta != 0) { 399 | offsetChildren(-delta); 400 | } 401 | } 402 | 403 | private static View findNextScrapView(List scrapList, Direction direction, 404 | int position) { 405 | final int scrapCount = scrapList.size(); 406 | 407 | ViewHolder closest = null; 408 | int closestDistance = Integer.MAX_VALUE; 409 | 410 | for (int i = 0; i < scrapCount; i++) { 411 | final ViewHolder holder = scrapList.get(i); 412 | 413 | final int distance = holder.getPosition() - position; 414 | if ((distance < 0 && direction == Direction.END) || 415 | (distance > 0 && direction == Direction.START)) { 416 | continue; 417 | } 418 | 419 | final int absDistance = Math.abs(distance); 420 | if (absDistance < closestDistance) { 421 | closest = holder; 422 | closestDistance = absDistance; 423 | 424 | if (distance == 0) { 425 | break; 426 | } 427 | } 428 | } 429 | 430 | if (closest != null) { 431 | return closest.itemView; 432 | } 433 | 434 | return null; 435 | } 436 | 437 | private void fillFromScrapList(List scrapList, Direction direction) { 438 | final int firstPosition = getFirstVisiblePosition(); 439 | 440 | int position; 441 | if (direction == Direction.END) { 442 | position = firstPosition + getChildCount(); 443 | } else { 444 | position = firstPosition - 1; 445 | } 446 | 447 | View scrapChild; 448 | while ((scrapChild = findNextScrapView(scrapList, direction, position)) != null) { 449 | setupChild(scrapChild, direction); 450 | position += (direction == Direction.END ? 1 : -1); 451 | } 452 | } 453 | 454 | private void setupChild(View child, Direction direction) { 455 | final ItemSelectionSupport itemSelection = ItemSelectionSupport.from(mRecyclerView); 456 | if (itemSelection != null) { 457 | final int position = getPosition(child); 458 | itemSelection.setViewChecked(child, itemSelection.isItemChecked(position)); 459 | } 460 | 461 | measureChild(child, direction); 462 | layoutChild(child, direction); 463 | } 464 | 465 | private View makeAndAddView(int position, Direction direction, Recycler recycler) { 466 | final View child = recycler.getViewForPosition(position); 467 | final boolean isItemRemoved = ((LayoutParams) child.getLayoutParams()).isItemRemoved(); 468 | 469 | if (!isItemRemoved) { 470 | addView(child, (direction == Direction.END ? -1 : 0)); 471 | } 472 | 473 | setupChild(child, direction); 474 | 475 | if (!isItemRemoved) { 476 | updateLayoutEdgesFromNewChild(child); 477 | } 478 | 479 | return child; 480 | } 481 | 482 | private void handleUpdate() { 483 | // Refresh state by requesting layout without changing the 484 | // first visible position. This will ensure the layout will 485 | // sync with the adapter changes. 486 | final int firstPosition = getFirstVisiblePosition(); 487 | final View firstChild = findViewByPosition(firstPosition); 488 | if (firstChild != null) { 489 | setPendingScrollPositionWithOffset(firstPosition, getChildStart(firstChild)); 490 | } else { 491 | setPendingScrollPositionWithOffset(RecyclerView.NO_POSITION, 0); 492 | } 493 | } 494 | 495 | private void updateLayoutEdgesFromNewChild(View newChild) { 496 | final int childStart = getChildStart(newChild); 497 | if (childStart < mLayoutStart) { 498 | mLayoutStart = childStart; 499 | } 500 | 501 | final int childEnd = getChildEnd(newChild); 502 | if (childEnd > mLayoutEnd) { 503 | mLayoutEnd = childEnd; 504 | } 505 | } 506 | 507 | private void updateLayoutEdgesFromRemovedChild(View removedChild, Direction direction) { 508 | final int childCount = getChildCount(); 509 | if (childCount == 0) { 510 | resetLayoutEdges(); 511 | return; 512 | } 513 | 514 | final int removedChildStart = getChildStart(removedChild); 515 | final int removedChildEnd = getChildEnd(removedChild); 516 | 517 | if (removedChildStart > mLayoutStart && removedChildEnd < mLayoutEnd) { 518 | return; 519 | } 520 | 521 | int index; 522 | final int limit; 523 | if (direction == Direction.END) { 524 | // Scrolling towards the end of the layout, child view being 525 | // removed from the start. 526 | mLayoutStart = Integer.MAX_VALUE; 527 | index = 0; 528 | limit = removedChildEnd; 529 | } else { 530 | // Scrolling towards the start of the layout, child view being 531 | // removed from the end. 532 | mLayoutEnd = Integer.MIN_VALUE; 533 | index = childCount - 1; 534 | limit = removedChildStart; 535 | } 536 | 537 | while (index >= 0 && index <= childCount - 1) { 538 | final View child = getChildAt(index); 539 | 540 | if (direction == Direction.END) { 541 | final int childStart = getChildStart(child); 542 | if (childStart < mLayoutStart) { 543 | mLayoutStart = childStart; 544 | } 545 | 546 | // Checked enough child views to update the minimum 547 | // layout start edge, stop. 548 | if (childStart >= limit) { 549 | break; 550 | } 551 | 552 | index++; 553 | } else { 554 | final int childEnd = getChildEnd(child); 555 | if (childEnd > mLayoutEnd) { 556 | mLayoutEnd = childEnd; 557 | } 558 | 559 | // Checked enough child views to update the minimum 560 | // layout end edge, stop. 561 | if (childEnd <= limit) { 562 | break; 563 | } 564 | 565 | index--; 566 | } 567 | } 568 | } 569 | 570 | private void resetLayoutEdges() { 571 | mLayoutStart = getStartWithPadding(); 572 | mLayoutEnd = mLayoutStart; 573 | } 574 | 575 | protected int getExtraLayoutSpace(State state) { 576 | if (state.hasTargetScrollPosition()) { 577 | return getTotalSpace(); 578 | } else { 579 | return 0; 580 | } 581 | } 582 | 583 | private Bundle getPendingItemSelectionState() { 584 | if (mPendingSavedState != null) { 585 | return mPendingSavedState.itemSelectionState; 586 | } 587 | 588 | return null; 589 | } 590 | 591 | protected void setPendingScrollPositionWithOffset(int position, int offset) { 592 | mPendingScrollPosition = position; 593 | mPendingScrollOffset = offset; 594 | } 595 | 596 | protected int getPendingScrollPosition() { 597 | if (mPendingSavedState != null) { 598 | return mPendingSavedState.anchorItemPosition; 599 | } 600 | 601 | return mPendingScrollPosition; 602 | } 603 | 604 | protected int getPendingScrollOffset() { 605 | if (mPendingSavedState != null) { 606 | return 0; 607 | } 608 | 609 | return mPendingScrollOffset; 610 | } 611 | 612 | protected int getAnchorItemPosition(State state) { 613 | final int itemCount = state.getItemCount(); 614 | 615 | int pendingPosition = getPendingScrollPosition(); 616 | if (pendingPosition != RecyclerView.NO_POSITION) { 617 | if (pendingPosition < 0 || pendingPosition >= itemCount) { 618 | pendingPosition = RecyclerView.NO_POSITION; 619 | } 620 | } 621 | 622 | if (pendingPosition != RecyclerView.NO_POSITION) { 623 | return pendingPosition; 624 | } else if (getChildCount() > 0) { 625 | return findFirstValidChildPosition(itemCount); 626 | } else { 627 | return 0; 628 | } 629 | } 630 | 631 | private int findFirstValidChildPosition(int itemCount) { 632 | final int childCount = getChildCount(); 633 | for (int i = 0; i < childCount; i++) { 634 | final View view = getChildAt(i); 635 | final int position = getPosition(view); 636 | if (position >= 0 && position < itemCount) { 637 | return position; 638 | } 639 | } 640 | 641 | return 0; 642 | } 643 | 644 | @Override 645 | public int getDecoratedMeasuredWidth(View child) { 646 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 647 | return super.getDecoratedMeasuredWidth(child) + lp.leftMargin + lp.rightMargin; 648 | } 649 | 650 | @Override 651 | public int getDecoratedMeasuredHeight(View child) { 652 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 653 | return super.getDecoratedMeasuredHeight(child) + lp.topMargin + lp.bottomMargin; 654 | } 655 | 656 | @Override 657 | public int getDecoratedLeft(View child) { 658 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 659 | return super.getDecoratedLeft(child) - lp.leftMargin; 660 | } 661 | 662 | @Override 663 | public int getDecoratedTop(View child) { 664 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 665 | return super.getDecoratedTop(child) - lp.topMargin; 666 | } 667 | 668 | @Override 669 | public int getDecoratedRight(View child) { 670 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 671 | return super.getDecoratedRight(child) + lp.rightMargin; 672 | } 673 | 674 | @Override 675 | public int getDecoratedBottom(View child) { 676 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 677 | return super.getDecoratedBottom(child) + lp.bottomMargin; 678 | } 679 | 680 | @Override 681 | public void layoutDecorated(View child, int left, int top, int right, int bottom) { 682 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 683 | super.layoutDecorated(child, left + lp.leftMargin, top + lp.topMargin, 684 | right - lp.rightMargin, bottom - lp.bottomMargin); 685 | } 686 | 687 | @Override 688 | public void onAttachedToWindow(RecyclerView view) { 689 | super.onAttachedToWindow(view); 690 | mRecyclerView = view; 691 | } 692 | 693 | @Override 694 | public void onDetachedFromWindow(RecyclerView view, Recycler recycler) { 695 | super.onDetachedFromWindow(view, recycler); 696 | mRecyclerView = null; 697 | } 698 | 699 | @Override 700 | public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) { 701 | super.onAdapterChanged(oldAdapter, newAdapter); 702 | 703 | final ItemSelectionSupport itemSelectionSupport = ItemSelectionSupport.from(mRecyclerView); 704 | if (oldAdapter != null && itemSelectionSupport != null) { 705 | itemSelectionSupport.clearChoices(); 706 | } 707 | } 708 | 709 | @Override 710 | public void onLayoutChildren(Recycler recycler, State state) { 711 | final ItemSelectionSupport itemSelection = ItemSelectionSupport.from(mRecyclerView); 712 | if (itemSelection != null) { 713 | final Bundle itemSelectionState = getPendingItemSelectionState(); 714 | if (itemSelectionState != null) { 715 | itemSelection.onRestoreInstanceState(itemSelectionState); 716 | } 717 | 718 | if (state.didStructureChange()) { 719 | itemSelection.onAdapterDataChanged(); 720 | } 721 | } 722 | 723 | final int anchorItemPosition = getAnchorItemPosition(state); 724 | detachAndScrapAttachedViews(recycler); 725 | fillSpecific(anchorItemPosition, recycler, state); 726 | 727 | onLayoutScrapList(recycler, state); 728 | 729 | setPendingScrollPositionWithOffset(RecyclerView.NO_POSITION, 0); 730 | mPendingSavedState = null; 731 | } 732 | 733 | protected void onLayoutScrapList(Recycler recycler, State state) { 734 | final int childCount = getChildCount(); 735 | if (childCount == 0 || state.isPreLayout() || !supportsPredictiveItemAnimations()) { 736 | return; 737 | } 738 | 739 | final List scrapList = recycler.getScrapList(); 740 | fillFromScrapList(scrapList, Direction.START); 741 | fillFromScrapList(scrapList, Direction.END); 742 | } 743 | 744 | protected void detachChild(View child, Direction direction) { 745 | // Do nothing by default. 746 | } 747 | 748 | @Override 749 | public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 750 | handleUpdate(); 751 | } 752 | 753 | @Override 754 | public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { 755 | handleUpdate(); 756 | } 757 | 758 | @Override 759 | public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) { 760 | handleUpdate(); 761 | } 762 | 763 | @Override 764 | public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) { 765 | handleUpdate(); 766 | } 767 | 768 | @Override 769 | public void onItemsChanged(RecyclerView recyclerView) { 770 | handleUpdate(); 771 | } 772 | 773 | @Override 774 | public RecyclerView.LayoutParams generateDefaultLayoutParams() { 775 | if (mIsVertical) { 776 | return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 777 | } else { 778 | return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); 779 | } 780 | } 781 | 782 | @Override 783 | public boolean supportsPredictiveItemAnimations() { 784 | return true; 785 | } 786 | 787 | @Override 788 | public int scrollHorizontallyBy(int dx, Recycler recycler, State state) { 789 | if (mIsVertical) { 790 | return 0; 791 | } 792 | 793 | return scrollBy(dx, recycler, state); 794 | } 795 | 796 | @Override 797 | public int scrollVerticallyBy(int dy, Recycler recycler, State state) { 798 | if (!mIsVertical) { 799 | return 0; 800 | } 801 | 802 | return scrollBy(dy, recycler, state); 803 | } 804 | 805 | @Override 806 | public boolean canScrollHorizontally() { 807 | return !mIsVertical; 808 | } 809 | 810 | @Override 811 | public boolean canScrollVertically() { 812 | return mIsVertical; 813 | } 814 | 815 | @Override 816 | public void scrollToPosition(int position) { 817 | scrollToPositionWithOffset(position, 0); 818 | } 819 | 820 | public void scrollToPositionWithOffset(int position, int offset) { 821 | setPendingScrollPositionWithOffset(position, offset); 822 | requestLayout(); 823 | } 824 | 825 | @Override 826 | public void smoothScrollToPosition(RecyclerView recyclerView, State state, int position) { 827 | final LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) { 828 | @Override 829 | public PointF computeScrollVectorForPosition(int targetPosition) { 830 | if (getChildCount() == 0) { 831 | return null; 832 | } 833 | 834 | final int direction = targetPosition < getFirstVisiblePosition() ? -1 : 1; 835 | if (mIsVertical) { 836 | return new PointF(0, direction); 837 | } else { 838 | return new PointF(direction, 0); 839 | } 840 | } 841 | 842 | @Override 843 | protected int getVerticalSnapPreference() { 844 | return LinearSmoothScroller.SNAP_TO_START; 845 | } 846 | 847 | @Override 848 | protected int getHorizontalSnapPreference() { 849 | return LinearSmoothScroller.SNAP_TO_START; 850 | } 851 | }; 852 | 853 | scroller.setTargetPosition(position); 854 | startSmoothScroll(scroller); 855 | } 856 | 857 | @Override 858 | public int computeHorizontalScrollOffset(State state) { 859 | if (getChildCount() == 0) { 860 | return 0; 861 | } 862 | 863 | return getFirstVisiblePosition(); 864 | } 865 | 866 | @Override 867 | public int computeVerticalScrollOffset(State state) { 868 | if (getChildCount() == 0) { 869 | return 0; 870 | } 871 | 872 | return getFirstVisiblePosition(); 873 | } 874 | 875 | @Override 876 | public int computeHorizontalScrollExtent(State state) { 877 | return getChildCount(); 878 | } 879 | 880 | @Override 881 | public int computeVerticalScrollExtent(State state) { 882 | return getChildCount(); 883 | } 884 | 885 | @Override 886 | public int computeHorizontalScrollRange(State state) { 887 | return state.getItemCount(); 888 | } 889 | 890 | @Override 891 | public int computeVerticalScrollRange(State state) { 892 | return state.getItemCount(); 893 | } 894 | 895 | @Override 896 | public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) { 897 | super.onMeasure(recycler, state, widthSpec, heightSpec); 898 | } 899 | 900 | @Override 901 | public Parcelable onSaveInstanceState() { 902 | final SavedState state = new SavedState(SavedState.EMPTY_STATE); 903 | 904 | int anchorItemPosition = getPendingScrollPosition(); 905 | if (anchorItemPosition == RecyclerView.NO_POSITION) { 906 | anchorItemPosition = getFirstVisiblePosition(); 907 | } 908 | state.anchorItemPosition = anchorItemPosition; 909 | 910 | final ItemSelectionSupport itemSelection = ItemSelectionSupport.from(mRecyclerView); 911 | if (itemSelection != null) { 912 | state.itemSelectionState = itemSelection.onSaveInstanceState(); 913 | } else { 914 | state.itemSelectionState = Bundle.EMPTY; 915 | } 916 | 917 | return state; 918 | } 919 | 920 | @Override 921 | public void onRestoreInstanceState(Parcelable state) { 922 | mPendingSavedState = (SavedState) state; 923 | requestLayout(); 924 | } 925 | 926 | public Orientation getOrientation() { 927 | return (mIsVertical ? Orientation.VERTICAL : Orientation.HORIZONTAL); 928 | } 929 | 930 | public void setOrientation(Orientation orientation) { 931 | final boolean isVertical = (orientation == Orientation.VERTICAL); 932 | if (this.mIsVertical == isVertical) { 933 | return; 934 | } 935 | 936 | this.mIsVertical = isVertical; 937 | requestLayout(); 938 | } 939 | 940 | public int getFirstVisiblePosition() { 941 | if (getChildCount() == 0) { 942 | return 0; 943 | } 944 | 945 | return getPosition(getChildAt(0)); 946 | } 947 | 948 | public int getLastVisiblePosition() { 949 | final int childCount = getChildCount(); 950 | if (childCount == 0) { 951 | return 0; 952 | } 953 | 954 | return getPosition(getChildAt(childCount - 1)); 955 | } 956 | 957 | protected abstract void measureChild(View child, Direction direction); 958 | protected abstract void layoutChild(View child, Direction direction); 959 | 960 | protected abstract boolean canAddMoreViews(Direction direction, int limit); 961 | 962 | protected static class SavedState implements Parcelable { 963 | protected static final SavedState EMPTY_STATE = new SavedState(); 964 | 965 | private final Parcelable superState; 966 | private int anchorItemPosition; 967 | private Bundle itemSelectionState; 968 | 969 | private SavedState() { 970 | superState = null; 971 | } 972 | 973 | protected SavedState(Parcelable superState) { 974 | if (superState == null) { 975 | throw new IllegalArgumentException("superState must not be null"); 976 | } 977 | 978 | this.superState = (superState != EMPTY_STATE ? superState : null); 979 | } 980 | 981 | protected SavedState(Parcel in) { 982 | this.superState = EMPTY_STATE; 983 | anchorItemPosition = in.readInt(); 984 | itemSelectionState = in.readParcelable(getClass().getClassLoader()); 985 | } 986 | 987 | public Parcelable getSuperState() { 988 | return superState; 989 | } 990 | 991 | @Override 992 | public int describeContents() { 993 | return 0; 994 | } 995 | 996 | @Override 997 | public void writeToParcel(Parcel out, int flags) { 998 | out.writeInt(anchorItemPosition); 999 | out.writeParcelable(itemSelectionState, flags); 1000 | } 1001 | 1002 | public static final Parcelable.Creator CREATOR 1003 | = new Parcelable.Creator() { 1004 | @Override 1005 | public SavedState createFromParcel(Parcel in) { 1006 | return new SavedState(in); 1007 | } 1008 | 1009 | @Override 1010 | public SavedState[] newArray(int size) { 1011 | return new SavedState[size]; 1012 | } 1013 | }; 1014 | } 1015 | } 1016 | -------------------------------------------------------------------------------- /core/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /core/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=1.0.0-SNAPSHOT 2 | VERSION_CODE=2 3 | GROUP=org.lucasr.twowayview 4 | 5 | POM_DESCRIPTION=RecyclerView-based framework and layouts. 6 | POM_URL=https://github.com/lucasr/twoway-view 7 | POM_SCM_URL=https://github.com/lucasr/twoway-view 8 | POM_SCM_CONNECTION=scm:git@github.com:lucasr/twoway-view.git 9 | POM_SCM_DEV_CONNECTION=scm:git@github.com:lucasr/twoway-view.git 10 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 11 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 12 | POM_LICENCE_DIST=repo 13 | POM_DEVELOPER_ID=lucasr 14 | POM_DEVELOPER_NAME=Lucas Rocha 15 | -------------------------------------------------------------------------------- /gradle/scripts/gradle-mvn-push.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Chris Banes 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 | 17 | apply plugin: 'maven' 18 | apply plugin: 'signing' 19 | 20 | def isReleaseBuild() { 21 | return VERSION_NAME.contains("SNAPSHOT") == false 22 | } 23 | 24 | def getReleaseRepositoryUrl() { 25 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 26 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 27 | } 28 | 29 | def getSnapshotRepositoryUrl() { 30 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 31 | : "https://oss.sonatype.org/content/repositories/snapshots/" 32 | } 33 | 34 | def getRepositoryUsername() { 35 | return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : "" 36 | } 37 | 38 | def getRepositoryPassword() { 39 | return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : "" 40 | } 41 | 42 | afterEvaluate { project -> 43 | uploadArchives { 44 | repositories { 45 | mavenDeployer { 46 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 47 | 48 | pom.groupId = GROUP 49 | pom.artifactId = POM_ARTIFACT_ID 50 | pom.version = VERSION_NAME 51 | 52 | repository(url: getReleaseRepositoryUrl()) { 53 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 54 | } 55 | snapshotRepository(url: getSnapshotRepositoryUrl()) { 56 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 57 | } 58 | 59 | pom.project { 60 | name POM_NAME 61 | packaging POM_PACKAGING 62 | description POM_DESCRIPTION 63 | url POM_URL 64 | 65 | scm { 66 | url POM_SCM_URL 67 | connection POM_SCM_CONNECTION 68 | developerConnection POM_SCM_DEV_CONNECTION 69 | } 70 | 71 | licenses { 72 | license { 73 | name POM_LICENCE_NAME 74 | url POM_LICENCE_URL 75 | distribution POM_LICENCE_DIST 76 | } 77 | } 78 | 79 | developers { 80 | developer { 81 | id POM_DEVELOPER_ID 82 | name POM_DEVELOPER_NAME 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | signing { 91 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 92 | sign configurations.archives 93 | } 94 | 95 | task androidJavadocs(type: Javadoc) { 96 | source = android.sourceSets.main.java.srcDirs 97 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 98 | } 99 | 100 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 101 | classifier = 'javadoc' 102 | from androidJavadocs.destinationDir 103 | } 104 | 105 | task androidSourcesJar(type: Jar) { 106 | classifier = 'sources' 107 | from android.sourceSets.main.java.sourceFiles 108 | } 109 | 110 | artifacts { 111 | archives androidSourcesJar 112 | archives androidJavadocsJar 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jan 31 22:57:10 GMT 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /images/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/images/sample.png -------------------------------------------------------------------------------- /layouts/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | repositories { 4 | mavenCentral() 5 | } 6 | 7 | android { 8 | compileSdkVersion 21 9 | buildToolsVersion "21.1.2" 10 | resourcePrefix 'twowayview_' 11 | 12 | defaultConfig { 13 | minSdkVersion 10 14 | targetSdkVersion 21 15 | } 16 | } 17 | 18 | dependencies { 19 | compile project(':core') 20 | compile 'com.android.support:recyclerview-v7:21.0.0' 21 | } 22 | 23 | apply from: "${rootDir}/gradle/scripts/gradle-mvn-push.gradle" 24 | -------------------------------------------------------------------------------- /layouts/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=TwoWayView Layouts Library 2 | POM_ARTIFACT_ID=layouts 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /layouts/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /layouts/src/main/java/org/lucasr/twowayview/widget/BaseLayoutManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 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 | 17 | package org.lucasr.twowayview.widget; 18 | 19 | import android.content.Context; 20 | import android.graphics.Rect; 21 | import android.os.Parcel; 22 | import android.os.Parcelable; 23 | import android.support.v7.widget.RecyclerView; 24 | import android.support.v7.widget.RecyclerView.Adapter; 25 | import android.support.v7.widget.RecyclerView.LayoutParams; 26 | import android.support.v7.widget.RecyclerView.Recycler; 27 | import android.support.v7.widget.RecyclerView.State; 28 | import android.util.AttributeSet; 29 | import android.view.View; 30 | import android.view.ViewGroup; 31 | import android.view.ViewGroup.MarginLayoutParams; 32 | 33 | import org.lucasr.twowayview.TwoWayLayoutManager; 34 | import org.lucasr.twowayview.widget.Lanes.LaneInfo; 35 | 36 | import static org.lucasr.twowayview.widget.Lanes.calculateLaneSize; 37 | 38 | public abstract class BaseLayoutManager extends TwoWayLayoutManager { 39 | private static final String LOGTAG = "BaseLayoutManager"; 40 | 41 | protected static class ItemEntry implements Parcelable { 42 | public int startLane; 43 | public int anchorLane; 44 | 45 | private int[] spanMargins; 46 | 47 | public ItemEntry(int startLane, int anchorLane) { 48 | this.startLane = startLane; 49 | this.anchorLane = anchorLane; 50 | } 51 | 52 | public ItemEntry(Parcel in) { 53 | startLane = in.readInt(); 54 | anchorLane = in.readInt(); 55 | 56 | final int marginCount = in.readInt(); 57 | if (marginCount > 0) { 58 | spanMargins = new int[marginCount]; 59 | for (int i = 0; i < marginCount; i++) { 60 | spanMargins[i] = in.readInt(); 61 | } 62 | } 63 | } 64 | 65 | @Override 66 | public int describeContents() { 67 | return 0; 68 | } 69 | 70 | @Override 71 | public void writeToParcel(Parcel out, int flags) { 72 | out.writeInt(startLane); 73 | out.writeInt(anchorLane); 74 | 75 | final int marginCount = (spanMargins != null ? spanMargins.length : 0); 76 | out.writeInt(marginCount); 77 | 78 | for (int i = 0; i < marginCount; i++) { 79 | out.writeInt(spanMargins[i]); 80 | } 81 | } 82 | 83 | void setLane(LaneInfo laneInfo) { 84 | startLane = laneInfo.startLane; 85 | anchorLane = laneInfo.anchorLane; 86 | } 87 | 88 | void invalidateLane() { 89 | startLane = Lanes.NO_LANE; 90 | anchorLane = Lanes.NO_LANE; 91 | spanMargins = null; 92 | } 93 | 94 | private boolean hasSpanMargins() { 95 | return (spanMargins != null); 96 | } 97 | 98 | private int getSpanMargin(int index) { 99 | if (spanMargins == null) { 100 | return 0; 101 | } 102 | 103 | return spanMargins[index]; 104 | } 105 | 106 | private void setSpanMargin(int index, int margin, int span) { 107 | if (spanMargins == null) { 108 | spanMargins = new int[span]; 109 | } 110 | 111 | spanMargins[index] = margin; 112 | } 113 | 114 | public static final Creator CREATOR 115 | = new Creator() { 116 | @Override 117 | public ItemEntry createFromParcel(Parcel in) { 118 | return new ItemEntry(in); 119 | } 120 | 121 | @Override 122 | public ItemEntry[] newArray(int size) { 123 | return new ItemEntry[size]; 124 | } 125 | }; 126 | } 127 | 128 | private enum UpdateOp { 129 | ADD, 130 | REMOVE, 131 | UPDATE, 132 | MOVE 133 | } 134 | 135 | private Lanes mLanes; 136 | private Lanes mLanesToRestore; 137 | 138 | private ItemEntries mItemEntries; 139 | private ItemEntries mItemEntriesToRestore; 140 | 141 | protected final Rect mChildFrame = new Rect(); 142 | protected final Rect mTempRect = new Rect(); 143 | protected final LaneInfo mTempLaneInfo = new LaneInfo(); 144 | 145 | public BaseLayoutManager(Context context, AttributeSet attrs) { 146 | this(context, attrs, 0); 147 | } 148 | 149 | public BaseLayoutManager(Context context, AttributeSet attrs, int defStyle) { 150 | super(context, attrs, defStyle); 151 | } 152 | 153 | public BaseLayoutManager(Orientation orientation) { 154 | super(orientation); 155 | } 156 | 157 | protected void pushChildFrame(ItemEntry entry, Rect childFrame, int lane, int laneSpan, 158 | Direction direction) { 159 | final boolean shouldSetMargins = (direction == Direction.END && 160 | entry != null && !entry.hasSpanMargins()); 161 | 162 | for (int i = lane; i < lane + laneSpan; i++) { 163 | final int spanMargin; 164 | if (entry != null && direction != Direction.END) { 165 | spanMargin = entry.getSpanMargin(i - lane); 166 | } else { 167 | spanMargin = 0; 168 | } 169 | 170 | final int margin = mLanes.pushChildFrame(childFrame, i, spanMargin, direction); 171 | if (laneSpan > 1 && shouldSetMargins) { 172 | entry.setSpanMargin(i - lane, margin, laneSpan); 173 | } 174 | } 175 | } 176 | 177 | private void popChildFrame(ItemEntry entry, Rect childFrame, int lane, int laneSpan, 178 | Direction direction) { 179 | for (int i = lane; i < lane + laneSpan; i++) { 180 | final int spanMargin; 181 | if (entry != null && direction != Direction.END) { 182 | spanMargin = entry.getSpanMargin(i - lane); 183 | } else { 184 | spanMargin = 0; 185 | } 186 | 187 | mLanes.popChildFrame(childFrame, i, spanMargin, direction); 188 | } 189 | } 190 | 191 | void getDecoratedChildFrame(View child, Rect childFrame) { 192 | childFrame.left = getDecoratedLeft(child); 193 | childFrame.top = getDecoratedTop(child); 194 | childFrame.right = getDecoratedRight(child); 195 | childFrame.bottom = getDecoratedBottom(child); 196 | } 197 | 198 | boolean isVertical() { 199 | return (getOrientation() == Orientation.VERTICAL); 200 | } 201 | 202 | Lanes getLanes() { 203 | return mLanes; 204 | } 205 | 206 | void setItemEntryForPosition(int position, ItemEntry entry) { 207 | if (mItemEntries != null) { 208 | mItemEntries.putItemEntry(position, entry); 209 | } 210 | } 211 | 212 | ItemEntry getItemEntryForPosition(int position) { 213 | return (mItemEntries != null ? mItemEntries.getItemEntry(position) : null); 214 | } 215 | 216 | void clearItemEntries() { 217 | if (mItemEntries != null) { 218 | mItemEntries.clear(); 219 | } 220 | } 221 | 222 | void invalidateItemLanesAfter(int position) { 223 | if (mItemEntries != null) { 224 | mItemEntries.invalidateItemLanesAfter(position); 225 | } 226 | } 227 | 228 | void offsetForAddition(int positionStart, int itemCount) { 229 | if (mItemEntries != null) { 230 | mItemEntries.offsetForAddition(positionStart, itemCount); 231 | } 232 | } 233 | 234 | void offsetForRemoval(int positionStart, int itemCount) { 235 | if (mItemEntries != null) { 236 | mItemEntries.offsetForRemoval(positionStart, itemCount); 237 | } 238 | } 239 | 240 | private void requestMoveLayout() { 241 | if (getPendingScrollPosition() != RecyclerView.NO_POSITION) { 242 | return; 243 | } 244 | 245 | final int position = getFirstVisiblePosition(); 246 | final View firstChild = findViewByPosition(position); 247 | final int offset = (firstChild != null ? getChildStart(firstChild) : 0); 248 | 249 | setPendingScrollPositionWithOffset(position, offset); 250 | } 251 | 252 | private boolean canUseLanes(Lanes lanes) { 253 | if (lanes == null) { 254 | return false; 255 | } 256 | 257 | final int laneCount = getLaneCount(); 258 | final int laneSize = calculateLaneSize(this, laneCount); 259 | 260 | return (lanes.getOrientation() == getOrientation() && 261 | lanes.getCount() == laneCount && 262 | lanes.getLaneSize() == laneSize); 263 | } 264 | 265 | private boolean ensureLayoutState() { 266 | final int laneCount = getLaneCount(); 267 | if (laneCount == 0 || getWidth() == 0 || getHeight() == 0 || canUseLanes(mLanes)) { 268 | return false; 269 | } 270 | 271 | final Lanes oldLanes = mLanes; 272 | mLanes = new Lanes(this, laneCount); 273 | 274 | requestMoveLayout(); 275 | 276 | if (mItemEntries == null) { 277 | mItemEntries = new ItemEntries(); 278 | } 279 | 280 | if (oldLanes != null && oldLanes.getOrientation() == mLanes.getOrientation() && 281 | oldLanes.getLaneSize() == mLanes.getLaneSize()) { 282 | invalidateItemLanesAfter(0); 283 | } else { 284 | mItemEntries.clear(); 285 | } 286 | 287 | return true; 288 | } 289 | 290 | private void handleUpdate(int positionStart, int itemCountOrToPosition, UpdateOp cmd) { 291 | invalidateItemLanesAfter(positionStart); 292 | 293 | switch (cmd) { 294 | case ADD: 295 | offsetForAddition(positionStart, itemCountOrToPosition); 296 | break; 297 | 298 | case REMOVE: 299 | offsetForRemoval(positionStart, itemCountOrToPosition); 300 | break; 301 | 302 | case MOVE: 303 | offsetForRemoval(positionStart, 1); 304 | offsetForAddition(itemCountOrToPosition, 1); 305 | break; 306 | } 307 | 308 | if (positionStart + itemCountOrToPosition <= getFirstVisiblePosition()) { 309 | return; 310 | } 311 | 312 | if (positionStart <= getLastVisiblePosition()) { 313 | requestLayout(); 314 | } 315 | } 316 | 317 | @Override 318 | public void offsetChildrenHorizontal(int offset) { 319 | if (!isVertical()) { 320 | mLanes.offset(offset); 321 | } 322 | 323 | super.offsetChildrenHorizontal(offset); 324 | } 325 | 326 | @Override 327 | public void offsetChildrenVertical(int offset) { 328 | super.offsetChildrenVertical(offset); 329 | 330 | if (isVertical()) { 331 | mLanes.offset(offset); 332 | } 333 | } 334 | 335 | @Override 336 | public void onLayoutChildren(Recycler recycler, State state) { 337 | final boolean restoringLanes = (mLanesToRestore != null); 338 | if (restoringLanes) { 339 | mLanes = mLanesToRestore; 340 | mItemEntries = mItemEntriesToRestore; 341 | 342 | mLanesToRestore = null; 343 | mItemEntriesToRestore = null; 344 | } 345 | 346 | final boolean refreshingLanes = ensureLayoutState(); 347 | 348 | // Still not able to create lanes, nothing we can do here, 349 | // just bail for now. 350 | if (mLanes == null) { 351 | return; 352 | } 353 | 354 | final int itemCount = state.getItemCount(); 355 | 356 | if (mItemEntries != null) { 357 | mItemEntries.setAdapterSize(itemCount); 358 | } 359 | 360 | final int anchorItemPosition = getAnchorItemPosition(state); 361 | 362 | // Only move layout if we're not restoring a layout state. 363 | if (anchorItemPosition > 0 && (refreshingLanes || !restoringLanes)) { 364 | moveLayoutToPosition(anchorItemPosition, getPendingScrollOffset(), recycler, state); 365 | } 366 | 367 | mLanes.reset(Direction.START); 368 | 369 | super.onLayoutChildren(recycler, state); 370 | } 371 | 372 | @Override 373 | protected void onLayoutScrapList(Recycler recycler, State state) { 374 | mLanes.save(); 375 | super.onLayoutScrapList(recycler, state); 376 | mLanes.restore(); 377 | } 378 | 379 | @Override 380 | public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 381 | handleUpdate(positionStart, itemCount, UpdateOp.ADD); 382 | super.onItemsAdded(recyclerView, positionStart, itemCount); 383 | } 384 | 385 | @Override 386 | public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { 387 | handleUpdate(positionStart, itemCount, UpdateOp.REMOVE); 388 | super.onItemsRemoved(recyclerView, positionStart, itemCount); 389 | } 390 | 391 | @Override 392 | public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) { 393 | handleUpdate(positionStart, itemCount, UpdateOp.UPDATE); 394 | super.onItemsUpdated(recyclerView, positionStart, itemCount); 395 | } 396 | 397 | @Override 398 | public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) { 399 | handleUpdate(from, to, UpdateOp.MOVE); 400 | super.onItemsMoved(recyclerView, from, to, itemCount); 401 | } 402 | 403 | @Override 404 | public void onItemsChanged(RecyclerView recyclerView) { 405 | clearItemEntries(); 406 | super.onItemsChanged(recyclerView); 407 | } 408 | 409 | @Override 410 | public Parcelable onSaveInstanceState() { 411 | final Parcelable superState = super.onSaveInstanceState(); 412 | final LanedSavedState state = new LanedSavedState(superState); 413 | 414 | final int laneCount = (mLanes != null ? mLanes.getCount() : 0); 415 | state.lanes = new Rect[laneCount]; 416 | for (int i = 0; i < laneCount; i++) { 417 | final Rect laneRect = new Rect(); 418 | mLanes.getLane(i, laneRect); 419 | state.lanes[i] = laneRect; 420 | } 421 | 422 | state.orientation = getOrientation(); 423 | state.laneSize = (mLanes != null ? mLanes.getLaneSize() : 0); 424 | state.itemEntries = mItemEntries; 425 | 426 | return state; 427 | } 428 | 429 | @Override 430 | public void onRestoreInstanceState(Parcelable state) { 431 | final LanedSavedState ss = (LanedSavedState) state; 432 | 433 | if (ss.lanes != null && ss.laneSize > 0) { 434 | mLanesToRestore = new Lanes(this, ss.orientation, ss.lanes, ss.laneSize); 435 | mItemEntriesToRestore = ss.itemEntries; 436 | } 437 | 438 | super.onRestoreInstanceState(ss.getSuperState()); 439 | } 440 | 441 | @Override 442 | protected boolean canAddMoreViews(Direction direction, int limit) { 443 | if (direction == Direction.START) { 444 | return (mLanes.getInnerStart() > limit); 445 | } else { 446 | return (mLanes.getInnerEnd() < limit); 447 | } 448 | } 449 | 450 | private int getWidthUsed(View child) { 451 | if (!isVertical()) { 452 | return 0; 453 | } 454 | 455 | final int size = getLanes().getLaneSize() * getLaneSpanForChild(child); 456 | return getWidth() - getPaddingLeft() - getPaddingRight() - size; 457 | } 458 | 459 | private int getHeightUsed(View child) { 460 | if (isVertical()) { 461 | return 0; 462 | } 463 | 464 | final int size = getLanes().getLaneSize() * getLaneSpanForChild(child); 465 | return getHeight() - getPaddingTop() - getPaddingBottom() - size; 466 | } 467 | 468 | void measureChildWithMargins(View child) { 469 | measureChildWithMargins(child, getWidthUsed(child), getHeightUsed(child)); 470 | } 471 | 472 | @Override 473 | protected void measureChild(View child, Direction direction) { 474 | cacheChildLaneAndSpan(child, direction); 475 | measureChildWithMargins(child); 476 | } 477 | 478 | @Override 479 | protected void layoutChild(View child, Direction direction) { 480 | getLaneForChild(mTempLaneInfo, child, direction); 481 | 482 | mLanes.getChildFrame(mChildFrame, getDecoratedMeasuredWidth(child), 483 | getDecoratedMeasuredHeight(child), mTempLaneInfo, direction); 484 | final ItemEntry entry = cacheChildFrame(child, mChildFrame); 485 | 486 | layoutDecorated(child, mChildFrame.left, mChildFrame.top, mChildFrame.right, 487 | mChildFrame.bottom); 488 | 489 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 490 | if (!lp.isItemRemoved()) { 491 | pushChildFrame(entry, mChildFrame, mTempLaneInfo.startLane, 492 | getLaneSpanForChild(child), direction); 493 | } 494 | } 495 | 496 | @Override 497 | protected void detachChild(View child, Direction direction) { 498 | final int position = getPosition(child); 499 | getLaneForPosition(mTempLaneInfo, position, direction); 500 | getDecoratedChildFrame(child, mChildFrame); 501 | 502 | popChildFrame(getItemEntryForPosition(position), mChildFrame, mTempLaneInfo.startLane, 503 | getLaneSpanForChild(child), direction); 504 | } 505 | 506 | void getLaneForChild(LaneInfo outInfo, View child, Direction direction) { 507 | getLaneForPosition(outInfo, getPosition(child), direction); 508 | } 509 | 510 | int getLaneSpanForChild(View child) { 511 | return 1; 512 | } 513 | 514 | int getLaneSpanForPosition(int position) { 515 | return 1; 516 | } 517 | 518 | ItemEntry cacheChildLaneAndSpan(View child, Direction direction) { 519 | // Do nothing by default. 520 | return null; 521 | } 522 | 523 | ItemEntry cacheChildFrame(View child, Rect childFrame) { 524 | // Do nothing by default. 525 | return null; 526 | } 527 | 528 | @Override 529 | public boolean checkLayoutParams(LayoutParams lp) { 530 | if (isVertical()) { 531 | return (lp.width == LayoutParams.MATCH_PARENT); 532 | } else { 533 | return (lp.height == LayoutParams.MATCH_PARENT); 534 | } 535 | } 536 | 537 | @Override 538 | public LayoutParams generateDefaultLayoutParams() { 539 | if (isVertical()) { 540 | return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 541 | } else { 542 | return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); 543 | } 544 | } 545 | 546 | @Override 547 | public LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 548 | final LayoutParams lanedLp = new LayoutParams((MarginLayoutParams) lp); 549 | if (isVertical()) { 550 | lanedLp.width = LayoutParams.MATCH_PARENT; 551 | lanedLp.height = lp.height; 552 | } else { 553 | lanedLp.width = lp.width; 554 | lanedLp.height = LayoutParams.MATCH_PARENT; 555 | } 556 | 557 | return lanedLp; 558 | } 559 | 560 | @Override 561 | public LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { 562 | return new LayoutParams(c, attrs); 563 | } 564 | 565 | abstract int getLaneCount(); 566 | abstract void getLaneForPosition(LaneInfo outInfo, int position, Direction direction); 567 | abstract void moveLayoutToPosition(int position, int offset, Recycler recycler, State state); 568 | 569 | protected static class LanedSavedState extends SavedState { 570 | private Orientation orientation; 571 | private Rect[] lanes; 572 | private int laneSize; 573 | private ItemEntries itemEntries; 574 | 575 | protected LanedSavedState(Parcelable superState) { 576 | super(superState); 577 | } 578 | 579 | private LanedSavedState(Parcel in) { 580 | super(in); 581 | 582 | orientation = Orientation.values()[in.readInt()]; 583 | laneSize = in.readInt(); 584 | 585 | final int laneCount = in.readInt(); 586 | if (laneCount > 0) { 587 | lanes = new Rect[laneCount]; 588 | for (int i = 0; i < laneCount; i++) { 589 | final Rect lane = new Rect(); 590 | lane.readFromParcel(in); 591 | lanes[i] = lane; 592 | } 593 | } 594 | 595 | final int itemEntriesCount = in.readInt(); 596 | if (itemEntriesCount > 0) { 597 | itemEntries = new ItemEntries(); 598 | for (int i = 0; i < itemEntriesCount; i++) { 599 | final ItemEntry entry = in.readParcelable(getClass().getClassLoader()); 600 | itemEntries.restoreItemEntry(i, entry); 601 | } 602 | } 603 | } 604 | 605 | @Override 606 | public void writeToParcel(Parcel out, int flags) { 607 | super.writeToParcel(out, flags); 608 | 609 | out.writeInt(orientation.ordinal()); 610 | out.writeInt(laneSize); 611 | 612 | final int laneCount = (lanes != null ? lanes.length : 0); 613 | out.writeInt(laneCount); 614 | 615 | for (int i = 0; i < laneCount; i++) { 616 | lanes[i].writeToParcel(out, Rect.PARCELABLE_WRITE_RETURN_VALUE); 617 | } 618 | 619 | final int itemEntriesCount = (itemEntries != null ? itemEntries.size() : 0); 620 | out.writeInt(itemEntriesCount); 621 | 622 | for (int i = 0; i < itemEntriesCount; i++) { 623 | out.writeParcelable(itemEntries.getItemEntry(i), flags); 624 | } 625 | } 626 | 627 | public static final Parcelable.Creator CREATOR 628 | = new Parcelable.Creator() { 629 | @Override 630 | public LanedSavedState createFromParcel(Parcel in) { 631 | return new LanedSavedState(in); 632 | } 633 | 634 | @Override 635 | public LanedSavedState[] newArray(int size) { 636 | return new LanedSavedState[size]; 637 | } 638 | }; 639 | } 640 | } 641 | -------------------------------------------------------------------------------- /layouts/src/main/java/org/lucasr/twowayview/widget/DividerItemDecoration.java: -------------------------------------------------------------------------------- 1 | package org.lucasr.twowayview.widget; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Rect; 7 | import android.graphics.drawable.Drawable; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.support.v7.widget.RecyclerView.ItemDecoration; 10 | import android.util.AttributeSet; 11 | import android.view.View; 12 | import android.view.ViewGroup.MarginLayoutParams; 13 | 14 | /** 15 | * {@link android.support.v7.widget.RecyclerView.ItemDecoration} that draws 16 | * vertical and horizontal dividers between the items of the target 17 | * {@link android.support.v7.widget.RecyclerView}. 18 | */ 19 | public class DividerItemDecoration extends ItemDecoration { 20 | private final ItemSpacingOffsets mItemSpacing; 21 | 22 | private final Drawable mVerticalDivider; 23 | private final Drawable mHorizontalDivider; 24 | 25 | public DividerItemDecoration(Context context, AttributeSet attrs) { 26 | this(context, attrs, 0); 27 | } 28 | 29 | public DividerItemDecoration(Context context, AttributeSet attrs, int defStyle) { 30 | final TypedArray a = 31 | context.obtainStyledAttributes(attrs, R.styleable.twowayview_DividerItemDecoration, defStyle, 0); 32 | 33 | final Drawable divider = a.getDrawable(R.styleable.twowayview_DividerItemDecoration_android_divider); 34 | if (divider != null) { 35 | mVerticalDivider = mHorizontalDivider = divider; 36 | } else { 37 | mVerticalDivider = a.getDrawable(R.styleable.twowayview_DividerItemDecoration_twowayview_verticalDivider); 38 | mHorizontalDivider = a.getDrawable(R.styleable.twowayview_DividerItemDecoration_twowayview_horizontalDivider); 39 | } 40 | 41 | a.recycle(); 42 | 43 | mItemSpacing = createSpacing(mVerticalDivider, mHorizontalDivider); 44 | } 45 | 46 | public DividerItemDecoration(Drawable divider) { 47 | this(divider, divider); 48 | } 49 | 50 | public DividerItemDecoration(Drawable verticalDivider, Drawable horizontalDivider) { 51 | mVerticalDivider = verticalDivider; 52 | mHorizontalDivider = horizontalDivider; 53 | mItemSpacing = createSpacing(mVerticalDivider, mHorizontalDivider); 54 | } 55 | 56 | private static ItemSpacingOffsets createSpacing(Drawable verticalDivider, 57 | Drawable horizontalDivider) { 58 | final int verticalSpacing; 59 | if (horizontalDivider != null) { 60 | verticalSpacing = horizontalDivider.getIntrinsicHeight(); 61 | } else { 62 | verticalSpacing = 0; 63 | } 64 | 65 | final int horizontalSpacing; 66 | if (verticalDivider != null) { 67 | horizontalSpacing = verticalDivider.getIntrinsicWidth(); 68 | } else { 69 | horizontalSpacing = 0; 70 | } 71 | 72 | final ItemSpacingOffsets spacing = new ItemSpacingOffsets(verticalSpacing, horizontalSpacing); 73 | spacing.setAddSpacingAtEnd(true); 74 | 75 | return spacing; 76 | } 77 | 78 | @Override 79 | public void onDrawOver(Canvas c, RecyclerView parent) { 80 | final BaseLayoutManager lm = (BaseLayoutManager) parent.getLayoutManager(); 81 | 82 | final int rightWithPadding = parent.getWidth() - parent.getPaddingRight(); 83 | final int bottomWithPadding = parent.getHeight() - parent.getPaddingBottom(); 84 | 85 | final int childCount = parent.getChildCount(); 86 | for (int i = 0; i < childCount; i++) { 87 | final View child = parent.getChildAt(i); 88 | 89 | final int childLeft = lm.getDecoratedLeft(child); 90 | final int childTop = lm.getDecoratedTop(child); 91 | final int childRight = lm.getDecoratedRight(child); 92 | final int childBottom = lm.getDecoratedBottom(child); 93 | 94 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 95 | 96 | final int bottomOffset = childBottom - child.getBottom() - lp.bottomMargin; 97 | if (bottomOffset > 0 && childBottom < bottomWithPadding) { 98 | final int left = childLeft; 99 | final int top = childBottom - bottomOffset; 100 | final int right = childRight; 101 | final int bottom = top + mHorizontalDivider.getIntrinsicHeight(); 102 | 103 | mHorizontalDivider.setBounds(left, top, right, bottom); 104 | mHorizontalDivider.draw(c); 105 | } 106 | 107 | final int rightOffset = childRight - child.getRight() - lp.rightMargin; 108 | if (rightOffset > 0 && childRight < rightWithPadding) { 109 | final int left = childRight - rightOffset; 110 | final int top = childTop; 111 | final int right = left + mVerticalDivider.getIntrinsicWidth(); 112 | final int bottom = childBottom; 113 | 114 | mVerticalDivider.setBounds(left, top, right, bottom); 115 | mVerticalDivider.draw(c); 116 | } 117 | } 118 | } 119 | 120 | @Override 121 | public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) { 122 | mItemSpacing.getItemOffsets(outRect, itemPosition, parent); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /layouts/src/main/java/org/lucasr/twowayview/widget/GridLayoutManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 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 | 17 | package org.lucasr.twowayview.widget; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.support.v7.widget.RecyclerView.Recycler; 22 | import android.support.v7.widget.RecyclerView.State; 23 | import android.util.AttributeSet; 24 | import android.view.View; 25 | 26 | import org.lucasr.twowayview.widget.Lanes.LaneInfo; 27 | 28 | public class GridLayoutManager extends BaseLayoutManager { 29 | private static final String LOGTAG = "GridLayoutManager"; 30 | 31 | private static final int DEFAULT_NUM_COLS = 2; 32 | private static final int DEFAULT_NUM_ROWS = 2; 33 | 34 | private int mNumColumns; 35 | private int mNumRows; 36 | 37 | public GridLayoutManager(Context context, AttributeSet attrs) { 38 | this(context, attrs, 0); 39 | } 40 | 41 | public GridLayoutManager(Context context, AttributeSet attrs, int defStyle) { 42 | this(context, attrs, defStyle, DEFAULT_NUM_COLS, DEFAULT_NUM_ROWS); 43 | } 44 | 45 | protected GridLayoutManager(Context context, AttributeSet attrs, int defStyle, 46 | int defaultNumColumns, int defaultNumRows) { 47 | super(context, attrs, defStyle); 48 | 49 | final TypedArray a = 50 | context.obtainStyledAttributes(attrs, R.styleable.twowayview_GridLayoutManager, defStyle, 0); 51 | 52 | mNumColumns = 53 | Math.max(1, a.getInt(R.styleable.twowayview_GridLayoutManager_twowayview_numColumns, defaultNumColumns)); 54 | mNumRows = 55 | Math.max(1, a.getInt(R.styleable.twowayview_GridLayoutManager_twowayview_numRows, defaultNumRows)); 56 | 57 | a.recycle(); 58 | } 59 | 60 | public GridLayoutManager(Orientation orientation, int numColumns, int numRows) { 61 | super(orientation); 62 | mNumColumns = numColumns; 63 | mNumRows = numRows; 64 | 65 | if (mNumColumns < 1) { 66 | throw new IllegalArgumentException("GridLayoutManager must have at least 1 column"); 67 | } 68 | 69 | if (mNumRows < 1) { 70 | throw new IllegalArgumentException("GridLayoutManager must have at least 1 row"); 71 | } 72 | } 73 | 74 | @Override 75 | int getLaneCount() { 76 | return (isVertical() ? mNumColumns : mNumRows); 77 | } 78 | 79 | @Override 80 | void getLaneForPosition(LaneInfo outInfo, int position, Direction direction) { 81 | final int lane = (position % getLaneCount()); 82 | outInfo.set(lane, lane); 83 | } 84 | 85 | @Override 86 | void moveLayoutToPosition(int position, int offset, Recycler recycler, State state) { 87 | final Lanes lanes = getLanes(); 88 | lanes.reset(offset); 89 | 90 | getLaneForPosition(mTempLaneInfo, position, Direction.END); 91 | final int lane = mTempLaneInfo.startLane; 92 | if (lane == 0) { 93 | return; 94 | } 95 | 96 | final View child = recycler.getViewForPosition(position); 97 | measureChild(child, Direction.END); 98 | 99 | final int dimension = 100 | (isVertical() ? getDecoratedMeasuredHeight(child) : getDecoratedMeasuredWidth(child)); 101 | 102 | for (int i = lane - 1; i >= 0; i--) { 103 | lanes.offset(i, dimension); 104 | } 105 | } 106 | 107 | public int getNumColumns() { 108 | return mNumColumns; 109 | } 110 | 111 | public void setNumColumns(int numColumns) { 112 | if (mNumColumns == numColumns) { 113 | return; 114 | } 115 | 116 | mNumColumns = numColumns; 117 | if (isVertical()) { 118 | requestLayout(); 119 | } 120 | } 121 | 122 | public int getNumRows() { 123 | return mNumRows; 124 | } 125 | 126 | public void setNumRows(int numRows) { 127 | if (mNumRows == numRows) { 128 | return; 129 | } 130 | 131 | mNumRows = numRows; 132 | if (!isVertical()) { 133 | requestLayout(); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /layouts/src/main/java/org/lucasr/twowayview/widget/ItemEntries.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * This code is based on Android's StaggeredLayoutManager's 5 | * LazySpanLookup class. 6 | * 7 | * Copyright (C) 2014 The Android Open Source Project 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | */ 21 | 22 | package org.lucasr.twowayview.widget; 23 | 24 | import android.util.Log; 25 | 26 | import java.util.Arrays; 27 | 28 | import org.lucasr.twowayview.widget.BaseLayoutManager.ItemEntry; 29 | 30 | class ItemEntries { 31 | private static final int MIN_SIZE = 10; 32 | 33 | private ItemEntry[] mItemEntries; 34 | private int mAdapterSize; 35 | private boolean mRestoringItem; 36 | 37 | private int sizeForPosition(int position) { 38 | int len = mItemEntries.length; 39 | while (len <= position) { 40 | len *= 2; 41 | } 42 | 43 | // We don't apply any constraints while restoring 44 | // item entries. 45 | if (!mRestoringItem && len > mAdapterSize) { 46 | len = mAdapterSize; 47 | } 48 | 49 | return len; 50 | } 51 | 52 | private void ensureSize(int position) { 53 | if (mItemEntries == null) { 54 | mItemEntries = new ItemEntry[Math.max(position, MIN_SIZE) + 1]; 55 | Arrays.fill(mItemEntries, null); 56 | } else if (position >= mItemEntries.length) { 57 | ItemEntry[] oldItemEntries = mItemEntries; 58 | mItemEntries = new ItemEntry[sizeForPosition(position)]; 59 | System.arraycopy(oldItemEntries, 0, mItemEntries, 0, oldItemEntries.length); 60 | Arrays.fill(mItemEntries, oldItemEntries.length, mItemEntries.length, null); 61 | } 62 | } 63 | 64 | public ItemEntry getItemEntry(int position) { 65 | if (mItemEntries == null || position >= mItemEntries.length) { 66 | return null; 67 | } 68 | 69 | return mItemEntries[position]; 70 | } 71 | 72 | public void putItemEntry(int position, ItemEntry entry) { 73 | ensureSize(position); 74 | mItemEntries[position] = entry; 75 | } 76 | 77 | public void restoreItemEntry(int position, ItemEntry entry) { 78 | mRestoringItem = true; 79 | putItemEntry(position, entry); 80 | mRestoringItem = false; 81 | } 82 | 83 | public int size() { 84 | return (mItemEntries != null ? mItemEntries.length : 0); 85 | } 86 | 87 | public void setAdapterSize(int adapterSize) { 88 | mAdapterSize = adapterSize; 89 | } 90 | 91 | public void invalidateItemLanesAfter(int position) { 92 | if (mItemEntries == null || position >= mItemEntries.length) { 93 | return; 94 | } 95 | 96 | for (int i = position; i < mItemEntries.length; i++) { 97 | final ItemEntry entry = mItemEntries[i]; 98 | if (entry != null) { 99 | entry.invalidateLane(); 100 | } 101 | } 102 | } 103 | 104 | public void clear() { 105 | if (mItemEntries != null) { 106 | Arrays.fill(mItemEntries, null); 107 | } 108 | } 109 | 110 | void offsetForRemoval(int positionStart, int itemCount) { 111 | if (mItemEntries == null || positionStart >= mItemEntries.length) { 112 | return; 113 | } 114 | 115 | ensureSize(positionStart + itemCount); 116 | 117 | System.arraycopy(mItemEntries, positionStart + itemCount, mItemEntries, positionStart, 118 | mItemEntries.length - positionStart - itemCount); 119 | Arrays.fill(mItemEntries, mItemEntries.length - itemCount, mItemEntries.length, null); 120 | } 121 | 122 | void offsetForAddition(int positionStart, int itemCount) { 123 | if (mItemEntries == null || positionStart >= mItemEntries.length) { 124 | return; 125 | } 126 | 127 | ensureSize(positionStart + itemCount); 128 | 129 | System.arraycopy(mItemEntries, positionStart, mItemEntries, positionStart + itemCount, 130 | mItemEntries.length - positionStart - itemCount); 131 | Arrays.fill(mItemEntries, positionStart, positionStart + itemCount, null); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /layouts/src/main/java/org/lucasr/twowayview/widget/ItemSpacingOffsets.java: -------------------------------------------------------------------------------- 1 | package org.lucasr.twowayview.widget; 2 | 3 | import android.graphics.Rect; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.View; 6 | 7 | import org.lucasr.twowayview.TwoWayLayoutManager.Direction; 8 | import org.lucasr.twowayview.widget.Lanes.LaneInfo; 9 | 10 | /** 11 | * Core logic for applying item vertical and horizontal spacings via item 12 | * offsets. Account for the item lane positions to only apply spacings within 13 | * the layout. 14 | */ 15 | class ItemSpacingOffsets { 16 | private final int mVerticalSpacing; 17 | private final int mHorizontalSpacing; 18 | 19 | private boolean mAddSpacingAtEnd; 20 | 21 | private final LaneInfo mTempLaneInfo = new LaneInfo(); 22 | 23 | public ItemSpacingOffsets(int verticalSpacing, int horizontalSpacing) { 24 | if (verticalSpacing < 0 || horizontalSpacing < 0) { 25 | throw new IllegalArgumentException("Spacings should be equal or greater than 0"); 26 | } 27 | 28 | mVerticalSpacing = verticalSpacing; 29 | mHorizontalSpacing = horizontalSpacing; 30 | } 31 | 32 | /** 33 | * Checks whether the given position is placed just after the item in the 34 | * first lane of the layout taking items spans into account. 35 | */ 36 | private boolean isSecondLane(BaseLayoutManager lm, int itemPosition, int lane) { 37 | if (lane == 0 || itemPosition == 0) { 38 | return false; 39 | } 40 | 41 | int previousLane = Lanes.NO_LANE; 42 | int previousPosition = itemPosition - 1; 43 | while (previousPosition >= 0) { 44 | lm.getLaneForPosition(mTempLaneInfo, previousPosition, Direction.END); 45 | previousLane = mTempLaneInfo.startLane; 46 | if (previousLane != lane) { 47 | break; 48 | } 49 | 50 | previousPosition--; 51 | } 52 | 53 | final int previousLaneSpan = lm.getLaneSpanForPosition(previousPosition); 54 | if (previousLane == 0) { 55 | return (lane == previousLane + previousLaneSpan); 56 | } 57 | 58 | return false; 59 | } 60 | 61 | /** 62 | * Checks whether the given position is placed at the start of a layout lane. 63 | */ 64 | private static boolean isFirstChildInLane(BaseLayoutManager lm, int itemPosition) { 65 | final int laneCount = lm.getLanes().getCount(); 66 | if (itemPosition >= laneCount) { 67 | return false; 68 | } 69 | 70 | int count = 0; 71 | for (int i = 0; i < itemPosition; i++) { 72 | count += lm.getLaneSpanForPosition(i); 73 | if (count >= laneCount) { 74 | return false; 75 | } 76 | } 77 | 78 | return true; 79 | } 80 | 81 | /** 82 | * Checks whether the given position is placed at the end of a layout lane. 83 | */ 84 | private static boolean isLastChildInLane(BaseLayoutManager lm, int itemPosition, int itemCount) { 85 | final int laneCount = lm.getLanes().getCount(); 86 | if (itemPosition < itemCount - laneCount) { 87 | return false; 88 | } 89 | 90 | // TODO: Figure out a robust way to compute this for layouts 91 | // that are dynamically placed and might span multiple lanes. 92 | if (lm instanceof SpannableGridLayoutManager || 93 | lm instanceof StaggeredGridLayoutManager) { 94 | return false; 95 | } 96 | 97 | return true; 98 | } 99 | 100 | public void setAddSpacingAtEnd(boolean spacingAtEnd) { 101 | mAddSpacingAtEnd = spacingAtEnd; 102 | } 103 | 104 | /** 105 | * Computes the offsets based on the vertical and horizontal spacing values. 106 | * The spacing computation has to ensure that the lane sizes are the same after 107 | * applying the offsets. This means we have to shift the spacing unevenly across 108 | * items depending on their position in the layout. 109 | */ 110 | public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) { 111 | final BaseLayoutManager lm = (BaseLayoutManager) parent.getLayoutManager(); 112 | 113 | lm.getLaneForPosition(mTempLaneInfo, itemPosition, Direction.END); 114 | final int lane = mTempLaneInfo.startLane; 115 | final int laneSpan = lm.getLaneSpanForPosition(itemPosition); 116 | final int laneCount = lm.getLanes().getCount(); 117 | final int itemCount = parent.getAdapter().getItemCount(); 118 | 119 | final boolean isVertical = lm.isVertical(); 120 | 121 | final boolean firstLane = (lane == 0); 122 | final boolean secondLane = isSecondLane(lm, itemPosition, lane); 123 | 124 | final boolean lastLane = (lane + laneSpan == laneCount); 125 | final boolean beforeLastLane = (lane + laneSpan == laneCount - 1); 126 | 127 | final int laneSpacing = (isVertical ? mHorizontalSpacing : mVerticalSpacing); 128 | 129 | final int laneOffsetStart; 130 | final int laneOffsetEnd; 131 | 132 | if (firstLane) { 133 | laneOffsetStart = 0; 134 | } else if (lastLane && !secondLane) { 135 | laneOffsetStart = (int) (laneSpacing * 0.75); 136 | } else if (secondLane && !lastLane) { 137 | laneOffsetStart = (int) (laneSpacing * 0.25); 138 | } else { 139 | laneOffsetStart = (int) (laneSpacing * 0.5); 140 | } 141 | 142 | if (lastLane) { 143 | laneOffsetEnd = 0; 144 | } else if (firstLane && !beforeLastLane) { 145 | laneOffsetEnd = (int) (laneSpacing * 0.75); 146 | } else if (beforeLastLane && !firstLane) { 147 | laneOffsetEnd = (int) (laneSpacing * 0.25); 148 | } else { 149 | laneOffsetEnd = (int) (laneSpacing * 0.5); 150 | } 151 | 152 | final boolean isFirstInLane = isFirstChildInLane(lm, itemPosition); 153 | final boolean isLastInLane = !mAddSpacingAtEnd && 154 | isLastChildInLane(lm, itemPosition, itemCount); 155 | 156 | if (isVertical) { 157 | outRect.left = laneOffsetStart; 158 | outRect.top = (isFirstInLane ? 0 : mVerticalSpacing / 2); 159 | outRect.right = laneOffsetEnd; 160 | outRect.bottom = (isLastInLane ? 0 : mVerticalSpacing / 2); 161 | } else { 162 | outRect.left = (isFirstInLane ? 0 : mHorizontalSpacing / 2); 163 | outRect.top = laneOffsetStart; 164 | outRect.right = (isLastInLane ? 0 : mHorizontalSpacing / 2); 165 | outRect.bottom = laneOffsetEnd; 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /layouts/src/main/java/org/lucasr/twowayview/widget/Lanes.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 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 | 17 | package org.lucasr.twowayview.widget; 18 | 19 | import android.graphics.Rect; 20 | 21 | import org.lucasr.twowayview.TwoWayLayoutManager.Direction; 22 | import org.lucasr.twowayview.TwoWayLayoutManager.Orientation; 23 | 24 | class Lanes { 25 | public static final int NO_LANE = -1; 26 | 27 | private final BaseLayoutManager mLayout; 28 | private final boolean mIsVertical; 29 | private final Rect[] mLanes; 30 | private final Rect[] mSavedLanes; 31 | private final int mLaneSize; 32 | 33 | private final Rect mTempRect = new Rect(); 34 | private final LaneInfo mTempLaneInfo = new LaneInfo(); 35 | 36 | private Integer mInnerStart; 37 | private Integer mInnerEnd; 38 | 39 | public static class LaneInfo { 40 | public int startLane; 41 | public int anchorLane; 42 | 43 | public boolean isUndefined() { 44 | return (startLane == NO_LANE || anchorLane == NO_LANE); 45 | } 46 | 47 | public void set(int startLane, int anchorLane) { 48 | this.startLane = startLane; 49 | this.anchorLane = anchorLane; 50 | } 51 | 52 | public void setUndefined() { 53 | startLane = NO_LANE; 54 | anchorLane = NO_LANE; 55 | } 56 | } 57 | 58 | public Lanes(BaseLayoutManager layout, Orientation orientation, Rect[] lanes, int laneSize) { 59 | mLayout = layout; 60 | mIsVertical = (orientation == Orientation.VERTICAL); 61 | mLanes = lanes; 62 | mLaneSize = laneSize; 63 | 64 | mSavedLanes = new Rect[mLanes.length]; 65 | for (int i = 0; i < mLanes.length; i++) { 66 | mSavedLanes[i] = new Rect(); 67 | } 68 | } 69 | 70 | public Lanes(BaseLayoutManager layout, int laneCount) { 71 | mLayout = layout; 72 | mIsVertical = layout.isVertical(); 73 | 74 | mLanes = new Rect[laneCount]; 75 | mSavedLanes = new Rect[laneCount]; 76 | for (int i = 0; i < laneCount; i++) { 77 | mLanes[i] = new Rect(); 78 | mSavedLanes[i] = new Rect(); 79 | } 80 | 81 | mLaneSize = calculateLaneSize(layout, laneCount); 82 | 83 | final int paddingLeft = layout.getPaddingLeft(); 84 | final int paddingTop = layout.getPaddingTop(); 85 | 86 | for (int i = 0; i < laneCount; i++) { 87 | final int laneStart = i * mLaneSize; 88 | 89 | final int l = paddingLeft + (mIsVertical ? laneStart : 0); 90 | final int t = paddingTop + (mIsVertical ? 0 : laneStart); 91 | final int r = (mIsVertical ? l + mLaneSize : l); 92 | final int b = (mIsVertical ? t : t + mLaneSize); 93 | 94 | mLanes[i].set(l, t, r, b); 95 | } 96 | } 97 | 98 | public static int calculateLaneSize(BaseLayoutManager layout, int laneCount) { 99 | if (layout.isVertical()) { 100 | final int paddingLeft = layout.getPaddingLeft(); 101 | final int paddingRight = layout.getPaddingRight(); 102 | final int width = layout.getWidth() - paddingLeft - paddingRight; 103 | return width / laneCount; 104 | } else { 105 | final int paddingTop = layout.getPaddingTop(); 106 | final int paddingBottom = layout.getPaddingBottom(); 107 | final int height = layout.getHeight() - paddingTop - paddingBottom; 108 | return height / laneCount; 109 | } 110 | } 111 | 112 | private void invalidateEdges() { 113 | mInnerStart = null; 114 | mInnerEnd = null; 115 | } 116 | 117 | public Orientation getOrientation() { 118 | return (mIsVertical ? Orientation.VERTICAL : Orientation.HORIZONTAL); 119 | } 120 | 121 | public void save() { 122 | for (int i = 0; i < mLanes.length; i++) { 123 | mSavedLanes[i].set(mLanes[i]); 124 | } 125 | } 126 | 127 | public void restore() { 128 | for (int i = 0; i < mLanes.length; i++) { 129 | mLanes[i].set(mSavedLanes[i]); 130 | } 131 | } 132 | 133 | public int getLaneSize() { 134 | return mLaneSize; 135 | } 136 | 137 | public int getCount() { 138 | return mLanes.length; 139 | } 140 | 141 | private void offsetLane(int lane, int offset) { 142 | mLanes[lane].offset(mIsVertical ? 0 : offset, 143 | mIsVertical ? offset : 0); 144 | } 145 | 146 | public void offset(int offset) { 147 | for (int i = 0; i < mLanes.length; i++) { 148 | offset(i, offset); 149 | } 150 | 151 | invalidateEdges(); 152 | } 153 | 154 | public void offset(int lane, int offset) { 155 | offsetLane(lane, offset); 156 | invalidateEdges(); 157 | } 158 | 159 | public void getLane(int lane, Rect laneRect) { 160 | laneRect.set(mLanes[lane]); 161 | } 162 | 163 | public int pushChildFrame(Rect outRect, int lane, int margin, Direction direction) { 164 | final int delta; 165 | 166 | final Rect laneRect = mLanes[lane]; 167 | if (mIsVertical) { 168 | if (direction == Direction.END) { 169 | delta = outRect.top - laneRect.bottom; 170 | laneRect.bottom = outRect.bottom + margin; 171 | } else { 172 | delta = outRect.bottom - laneRect.top; 173 | laneRect.top = outRect.top - margin; 174 | } 175 | } else { 176 | if (direction == Direction.END) { 177 | delta = outRect.left - laneRect.right; 178 | laneRect.right = outRect.right + margin; 179 | } else { 180 | delta = outRect.right - laneRect.left; 181 | laneRect.left = outRect.left - margin; 182 | } 183 | } 184 | 185 | invalidateEdges(); 186 | 187 | return delta; 188 | } 189 | 190 | public void popChildFrame(Rect outRect, int lane, int margin, Direction direction) { 191 | final Rect laneRect = mLanes[lane]; 192 | if (mIsVertical) { 193 | if (direction == Direction.END) { 194 | laneRect.top = outRect.bottom - margin; 195 | } else { 196 | laneRect.bottom = outRect.top + margin; 197 | } 198 | } else { 199 | if (direction == Direction.END) { 200 | laneRect.left = outRect.right - margin; 201 | } else { 202 | laneRect.right = outRect.left + margin; 203 | } 204 | } 205 | 206 | invalidateEdges(); 207 | } 208 | 209 | public void getChildFrame(Rect outRect, int childWidth, int childHeight, LaneInfo laneInfo, 210 | Direction direction) { 211 | final Rect startRect = mLanes[laneInfo.startLane]; 212 | 213 | // The anchor lane only applies when we're get child frame in the direction 214 | // of the forward scroll. We'll need to rethink this once we start working on 215 | // RTL support. 216 | final int anchorLane = 217 | (direction == Direction.END ? laneInfo.anchorLane : laneInfo.startLane); 218 | final Rect anchorRect = mLanes[anchorLane]; 219 | 220 | if (mIsVertical) { 221 | outRect.left = startRect.left; 222 | outRect.top = 223 | (direction == Direction.END ? anchorRect.bottom : anchorRect.top - childHeight); 224 | } else { 225 | outRect.top = startRect.top; 226 | outRect.left = 227 | (direction == Direction.END ? anchorRect.right : anchorRect.left - childWidth); 228 | } 229 | 230 | outRect.right = outRect.left + childWidth; 231 | outRect.bottom = outRect.top + childHeight; 232 | } 233 | 234 | private boolean intersects(int start, int count, Rect r) { 235 | for (int l = start; l < start + count; l++) { 236 | if (Rect.intersects(mLanes[l], r)) { 237 | return true; 238 | } 239 | } 240 | 241 | return false; 242 | } 243 | 244 | private int findLaneThatFitsSpan(int anchorLane, int laneSpan, Direction direction) { 245 | final int findStart = Math.max(0, anchorLane - laneSpan + 1); 246 | final int findEnd = Math.min(findStart + laneSpan, mLanes.length - laneSpan + 1); 247 | for (int l = findStart; l < findEnd; l++) { 248 | mTempLaneInfo.set(l, anchorLane); 249 | 250 | getChildFrame(mTempRect, mIsVertical ? laneSpan * mLaneSize : 1, 251 | mIsVertical ? 1 : laneSpan * mLaneSize, mTempLaneInfo, direction); 252 | 253 | if (!intersects(l, laneSpan, mTempRect)) { 254 | return l; 255 | } 256 | } 257 | 258 | return Lanes.NO_LANE; 259 | } 260 | 261 | public void findLane(LaneInfo outInfo, int laneSpan, Direction direction) { 262 | outInfo.setUndefined(); 263 | 264 | int targetEdge = (direction == Direction.END ? Integer.MAX_VALUE : Integer.MIN_VALUE); 265 | for (int l = 0; l < mLanes.length; l++) { 266 | final int laneEdge; 267 | if (mIsVertical) { 268 | laneEdge = (direction == Direction.END ? mLanes[l].bottom : mLanes[l].top); 269 | } else { 270 | laneEdge = (direction == Direction.END ? mLanes[l].right : mLanes[l].left); 271 | } 272 | 273 | if ((direction == Direction.END && laneEdge < targetEdge) || 274 | (direction == Direction.START && laneEdge > targetEdge)) { 275 | 276 | final int targetLane = findLaneThatFitsSpan(l, laneSpan, direction); 277 | if (targetLane != NO_LANE) { 278 | targetEdge = laneEdge; 279 | outInfo.set(targetLane, l); 280 | } 281 | } 282 | } 283 | } 284 | 285 | public void reset(Direction direction) { 286 | for (int i = 0; i < mLanes.length; i++) { 287 | final Rect laneRect = mLanes[i]; 288 | if (mIsVertical) { 289 | if (direction == Direction.START) { 290 | laneRect.bottom = laneRect.top; 291 | } else { 292 | laneRect.top = laneRect.bottom; 293 | } 294 | } else { 295 | if (direction == Direction.START) { 296 | laneRect.right = laneRect.left; 297 | } else { 298 | laneRect.left = laneRect.right; 299 | } 300 | } 301 | } 302 | 303 | invalidateEdges(); 304 | } 305 | 306 | public void reset(int offset) { 307 | for (int i = 0; i < mLanes.length; i++) { 308 | final Rect laneRect = mLanes[i]; 309 | 310 | laneRect.offsetTo(mIsVertical ? laneRect.left : offset, 311 | mIsVertical ? offset : laneRect.top); 312 | 313 | if (mIsVertical) { 314 | laneRect.bottom = laneRect.top; 315 | } else { 316 | laneRect.right = laneRect.left; 317 | } 318 | } 319 | 320 | invalidateEdges(); 321 | } 322 | 323 | public int getInnerStart() { 324 | if (mInnerStart != null) { 325 | return mInnerStart; 326 | } 327 | 328 | mInnerStart = Integer.MIN_VALUE; 329 | for (int i = 0; i < mLanes.length; i++) { 330 | final Rect laneRect = mLanes[i]; 331 | mInnerStart = Math.max(mInnerStart, mIsVertical ? laneRect.top : laneRect.left); 332 | } 333 | 334 | return mInnerStart; 335 | } 336 | 337 | public int getInnerEnd() { 338 | if (mInnerEnd != null) { 339 | return mInnerEnd; 340 | } 341 | 342 | mInnerEnd = Integer.MAX_VALUE; 343 | for (int i = 0; i < mLanes.length; i++) { 344 | final Rect laneRect = mLanes[i]; 345 | mInnerEnd = Math.min(mInnerEnd, mIsVertical ? laneRect.bottom : laneRect.right); 346 | } 347 | 348 | return mInnerEnd; 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /layouts/src/main/java/org/lucasr/twowayview/widget/ListLayoutManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 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 | 17 | package org.lucasr.twowayview.widget; 18 | 19 | import android.content.Context; 20 | import android.support.v7.widget.RecyclerView.Recycler; 21 | import android.support.v7.widget.RecyclerView.State; 22 | import android.util.AttributeSet; 23 | 24 | import org.lucasr.twowayview.widget.Lanes.LaneInfo; 25 | 26 | public class ListLayoutManager extends BaseLayoutManager { 27 | private static final String LOGTAG = "ListLayoutManager"; 28 | 29 | public ListLayoutManager(Context context, AttributeSet attrs) { 30 | this(context, attrs, 0); 31 | } 32 | 33 | public ListLayoutManager(Context context, AttributeSet attrs, int defStyle) { 34 | super(context, attrs, defStyle); 35 | } 36 | 37 | public ListLayoutManager(Context context, Orientation orientation) { 38 | super(orientation); 39 | } 40 | 41 | @Override 42 | int getLaneCount() { 43 | return 1; 44 | } 45 | 46 | @Override 47 | void getLaneForPosition(LaneInfo outInfo, int position, Direction direction) { 48 | outInfo.set(0, 0); 49 | } 50 | 51 | @Override 52 | void moveLayoutToPosition(int position, int offset, Recycler recycler, State state) { 53 | getLanes().reset(offset); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /layouts/src/main/java/org/lucasr/twowayview/widget/SpacingItemDecoration.java: -------------------------------------------------------------------------------- 1 | package org.lucasr.twowayview.widget; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Rect; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.support.v7.widget.RecyclerView.ItemDecoration; 8 | import android.util.AttributeSet; 9 | 10 | /** 11 | * {@link android.support.v7.widget.RecyclerView.ItemDecoration} that applies a 12 | * vertical and horizontal spacing between items of the target 13 | * {@link android.support.v7.widget.RecyclerView}. 14 | */ 15 | public class SpacingItemDecoration extends ItemDecoration { 16 | private final ItemSpacingOffsets mItemSpacing; 17 | 18 | public SpacingItemDecoration(Context context, AttributeSet attrs) { 19 | this(context, attrs, 0); 20 | } 21 | 22 | public SpacingItemDecoration(Context context, AttributeSet attrs, int defStyle) { 23 | final TypedArray a = 24 | context.obtainStyledAttributes(attrs, R.styleable.twowayview_SpacingItemDecoration, defStyle, 0); 25 | 26 | final int verticalSpacing = 27 | Math.max(0, a.getInt(R.styleable.twowayview_SpacingItemDecoration_android_verticalSpacing, 0)); 28 | final int horizontalSpacing = 29 | Math.max(0, a.getInt(R.styleable.twowayview_SpacingItemDecoration_android_horizontalSpacing, 0)); 30 | 31 | a.recycle(); 32 | 33 | mItemSpacing = new ItemSpacingOffsets(verticalSpacing, horizontalSpacing); 34 | } 35 | 36 | public SpacingItemDecoration(int verticalSpacing, int horizontalSpacing) { 37 | mItemSpacing = new ItemSpacingOffsets(verticalSpacing, horizontalSpacing); 38 | } 39 | 40 | @Override 41 | public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) { 42 | mItemSpacing.getItemOffsets(outRect, itemPosition, parent); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /layouts/src/main/java/org/lucasr/twowayview/widget/SpannableGridLayoutManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 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 | 17 | package org.lucasr.twowayview.widget; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.os.Parcel; 22 | import android.os.Parcelable; 23 | import android.support.v7.widget.RecyclerView; 24 | import android.support.v7.widget.RecyclerView.Recycler; 25 | import android.support.v7.widget.RecyclerView.State; 26 | import android.util.AttributeSet; 27 | import android.view.View; 28 | import android.view.ViewGroup; 29 | import android.view.ViewGroup.MarginLayoutParams; 30 | 31 | import org.lucasr.twowayview.widget.Lanes.LaneInfo; 32 | 33 | public class SpannableGridLayoutManager extends GridLayoutManager { 34 | private static final String LOGTAG = "SpannableGridLayoutManager"; 35 | 36 | private static final int DEFAULT_NUM_COLS = 3; 37 | private static final int DEFAULT_NUM_ROWS = 3; 38 | 39 | protected static class SpannableItemEntry extends BaseLayoutManager.ItemEntry { 40 | private final int colSpan; 41 | private final int rowSpan; 42 | 43 | public SpannableItemEntry(int startLane, int anchorLane, int colSpan, int rowSpan) { 44 | super(startLane, anchorLane); 45 | this.colSpan = colSpan; 46 | this.rowSpan = rowSpan; 47 | } 48 | 49 | public SpannableItemEntry(Parcel in) { 50 | super(in); 51 | this.colSpan = in.readInt(); 52 | this.rowSpan = in.readInt(); 53 | } 54 | 55 | @Override 56 | public void writeToParcel(Parcel out, int flags) { 57 | super.writeToParcel(out, flags); 58 | out.writeInt(colSpan); 59 | out.writeInt(rowSpan); 60 | } 61 | 62 | public static final Parcelable.Creator CREATOR 63 | = new Parcelable.Creator() { 64 | @Override 65 | public SpannableItemEntry createFromParcel(Parcel in) { 66 | return new SpannableItemEntry(in); 67 | } 68 | 69 | @Override 70 | public SpannableItemEntry[] newArray(int size) { 71 | return new SpannableItemEntry[size]; 72 | } 73 | }; 74 | } 75 | 76 | private boolean mMeasuring; 77 | 78 | public SpannableGridLayoutManager(Context context) { 79 | this(context, null); 80 | } 81 | 82 | public SpannableGridLayoutManager(Context context, AttributeSet attrs) { 83 | this(context, attrs, 0); 84 | } 85 | 86 | public SpannableGridLayoutManager(Context context, AttributeSet attrs, int defStyle) { 87 | super(context, attrs, defStyle, DEFAULT_NUM_COLS, DEFAULT_NUM_ROWS); 88 | } 89 | 90 | public SpannableGridLayoutManager(Orientation orientation, int numColumns, int numRows) { 91 | super(orientation, numColumns, numRows); 92 | } 93 | 94 | private int getChildWidth(int colSpan) { 95 | return getLanes().getLaneSize() * colSpan; 96 | } 97 | 98 | private int getChildHeight(int rowSpan) { 99 | return getLanes().getLaneSize() * rowSpan; 100 | } 101 | 102 | private static int getLaneSpan(LayoutParams lp, boolean isVertical) { 103 | return (isVertical ? lp.colSpan : lp.rowSpan); 104 | } 105 | 106 | private static int getLaneSpan(SpannableItemEntry entry, boolean isVertical) { 107 | return (isVertical ? entry.colSpan : entry.rowSpan); 108 | } 109 | 110 | @Override 111 | public boolean canScrollHorizontally() { 112 | return super.canScrollHorizontally() && !mMeasuring; 113 | } 114 | 115 | @Override 116 | public boolean canScrollVertically() { 117 | return super.canScrollVertically() && !mMeasuring; 118 | } 119 | 120 | @Override 121 | int getLaneSpanForChild(View child) { 122 | return getLaneSpan((LayoutParams) child.getLayoutParams(), isVertical()); 123 | } 124 | 125 | @Override 126 | int getLaneSpanForPosition(int position) { 127 | final SpannableItemEntry entry = (SpannableItemEntry) getItemEntryForPosition(position); 128 | if (entry == null) { 129 | throw new IllegalStateException("Could not find span for position " + position); 130 | } 131 | 132 | return getLaneSpan(entry, isVertical()); 133 | } 134 | 135 | @Override 136 | void getLaneForPosition(LaneInfo outInfo, int position, Direction direction) { 137 | final SpannableItemEntry entry = (SpannableItemEntry) getItemEntryForPosition(position); 138 | if (entry != null) { 139 | outInfo.set(entry.startLane, entry.anchorLane); 140 | return; 141 | } 142 | 143 | outInfo.setUndefined(); 144 | } 145 | 146 | @Override 147 | void getLaneForChild(LaneInfo outInfo, View child, Direction direction) { 148 | super.getLaneForChild(outInfo, child, direction); 149 | if (outInfo.isUndefined()) { 150 | getLanes().findLane(outInfo, getLaneSpanForChild(child), direction); 151 | } 152 | } 153 | 154 | private int getWidthUsed(View child) { 155 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 156 | return getWidth() - getPaddingLeft() - getPaddingRight() - getChildWidth(lp.colSpan); 157 | } 158 | 159 | private int getHeightUsed(View child) { 160 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 161 | return getHeight() - getPaddingTop() - getPaddingBottom() - getChildHeight(lp.rowSpan); 162 | } 163 | 164 | @Override 165 | void measureChildWithMargins(View child) { 166 | // XXX: This will disable scrolling while measuring this child to ensure that 167 | // both width and height can use MATCH_PARENT properly. 168 | mMeasuring = true; 169 | measureChildWithMargins(child, getWidthUsed(child), getHeightUsed(child)); 170 | mMeasuring = false; 171 | } 172 | 173 | @Override 174 | protected void moveLayoutToPosition(int position, int offset, Recycler recycler, State state) { 175 | final boolean isVertical = isVertical(); 176 | final Lanes lanes = getLanes(); 177 | 178 | lanes.reset(0); 179 | 180 | for (int i = 0; i <= position; i++) { 181 | SpannableItemEntry entry = (SpannableItemEntry) getItemEntryForPosition(i); 182 | if (entry == null) { 183 | final View child = recycler.getViewForPosition(i); 184 | entry = (SpannableItemEntry) cacheChildLaneAndSpan(child, Direction.END); 185 | } 186 | 187 | mTempLaneInfo.set(entry.startLane, entry.anchorLane); 188 | 189 | // The lanes might have been invalidated because an added or 190 | // removed item. See BaseLayoutManager.invalidateItemLanes(). 191 | if (mTempLaneInfo.isUndefined()) { 192 | lanes.findLane(mTempLaneInfo, getLaneSpanForPosition(i), Direction.END); 193 | entry.setLane(mTempLaneInfo); 194 | } 195 | 196 | lanes.getChildFrame(mTempRect, getChildWidth(entry.colSpan), 197 | getChildHeight(entry.rowSpan), mTempLaneInfo, Direction.END); 198 | 199 | if (i != position) { 200 | pushChildFrame(entry, mTempRect, entry.startLane, getLaneSpan(entry, isVertical), 201 | Direction.END); 202 | } 203 | } 204 | 205 | lanes.getLane(mTempLaneInfo.startLane, mTempRect); 206 | lanes.reset(Direction.END); 207 | lanes.offset(offset - (isVertical ? mTempRect.bottom : mTempRect.right)); 208 | } 209 | 210 | @Override 211 | ItemEntry cacheChildLaneAndSpan(View child, Direction direction) { 212 | final int position = getPosition(child); 213 | 214 | mTempLaneInfo.setUndefined(); 215 | 216 | SpannableItemEntry entry = (SpannableItemEntry) getItemEntryForPosition(position); 217 | if (entry != null) { 218 | mTempLaneInfo.set(entry.startLane, entry.anchorLane); 219 | } 220 | 221 | if (mTempLaneInfo.isUndefined()) { 222 | getLaneForChild(mTempLaneInfo, child, direction); 223 | } 224 | 225 | if (entry == null) { 226 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 227 | entry = new SpannableItemEntry(mTempLaneInfo.startLane, mTempLaneInfo.anchorLane, 228 | lp.colSpan, lp.rowSpan); 229 | setItemEntryForPosition(position, entry); 230 | } else { 231 | entry.setLane(mTempLaneInfo); 232 | } 233 | 234 | return entry; 235 | } 236 | 237 | @Override 238 | public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { 239 | if (lp.width != LayoutParams.MATCH_PARENT || 240 | lp.height != LayoutParams.MATCH_PARENT) { 241 | return false; 242 | } 243 | 244 | if (lp instanceof LayoutParams) { 245 | final LayoutParams spannableLp = (LayoutParams) lp; 246 | 247 | if (isVertical()) { 248 | return (spannableLp.rowSpan >= 1 && spannableLp.colSpan >= 1 && 249 | spannableLp.colSpan <= getLaneCount()); 250 | } else { 251 | return (spannableLp.colSpan >= 1 && spannableLp.rowSpan >= 1 && 252 | spannableLp.rowSpan <= getLaneCount()); 253 | } 254 | } 255 | 256 | return false; 257 | } 258 | 259 | @Override 260 | public LayoutParams generateDefaultLayoutParams() { 261 | return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 262 | } 263 | 264 | @Override 265 | public LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 266 | final LayoutParams spannableLp = new LayoutParams((MarginLayoutParams) lp); 267 | spannableLp.width = LayoutParams.MATCH_PARENT; 268 | spannableLp.height = LayoutParams.MATCH_PARENT; 269 | 270 | if (lp instanceof LayoutParams) { 271 | final LayoutParams other = (LayoutParams) lp; 272 | if (isVertical()) { 273 | spannableLp.colSpan = Math.max(1, Math.min(other.colSpan, getLaneCount())); 274 | spannableLp.rowSpan = Math.max(1, other.rowSpan); 275 | } else { 276 | spannableLp.colSpan = Math.max(1, other.colSpan); 277 | spannableLp.rowSpan = Math.max(1, Math.min(other.rowSpan, getLaneCount())); 278 | } 279 | } 280 | 281 | return spannableLp; 282 | } 283 | 284 | @Override 285 | public LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { 286 | return new LayoutParams(c, attrs); 287 | } 288 | 289 | public static class LayoutParams extends TwoWayView.LayoutParams { 290 | private static final int DEFAULT_SPAN = 1; 291 | 292 | public int rowSpan; 293 | public int colSpan; 294 | 295 | public LayoutParams(int width, int height) { 296 | super(width, height); 297 | rowSpan = DEFAULT_SPAN; 298 | colSpan = DEFAULT_SPAN; 299 | } 300 | 301 | public LayoutParams(Context c, AttributeSet attrs) { 302 | super(c, attrs); 303 | 304 | TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.twowayview_SpannableGridViewChild); 305 | colSpan = Math.max( 306 | DEFAULT_SPAN, a.getInt(R.styleable.twowayview_SpannableGridViewChild_twowayview_colSpan, -1)); 307 | rowSpan = Math.max( 308 | DEFAULT_SPAN, a.getInt(R.styleable.twowayview_SpannableGridViewChild_twowayview_rowSpan, -1)); 309 | a.recycle(); 310 | } 311 | 312 | public LayoutParams(ViewGroup.LayoutParams other) { 313 | super(other); 314 | init(other); 315 | } 316 | 317 | public LayoutParams(MarginLayoutParams other) { 318 | super(other); 319 | init(other); 320 | } 321 | 322 | private void init(ViewGroup.LayoutParams other) { 323 | if (other instanceof LayoutParams) { 324 | final LayoutParams lp = (LayoutParams) other; 325 | rowSpan = lp.rowSpan; 326 | colSpan = lp.colSpan; 327 | } else { 328 | rowSpan = DEFAULT_SPAN; 329 | colSpan = DEFAULT_SPAN; 330 | } 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /layouts/src/main/java/org/lucasr/twowayview/widget/StaggeredGridLayoutManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 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 | 17 | package org.lucasr.twowayview.widget; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.graphics.Rect; 22 | import android.os.Parcel; 23 | import android.os.Parcelable; 24 | import android.support.v7.widget.RecyclerView; 25 | import android.support.v7.widget.RecyclerView.Recycler; 26 | import android.support.v7.widget.RecyclerView.State; 27 | import android.util.AttributeSet; 28 | import android.view.View; 29 | import android.view.ViewGroup; 30 | 31 | import org.lucasr.twowayview.widget.Lanes.LaneInfo; 32 | 33 | public class StaggeredGridLayoutManager extends GridLayoutManager { 34 | private static final String LOGTAG = "StaggeredGridLayoutManager"; 35 | 36 | private static final int DEFAULT_NUM_COLS = 2; 37 | private static final int DEFAULT_NUM_ROWS = 2; 38 | 39 | protected static class StaggeredItemEntry extends BaseLayoutManager.ItemEntry { 40 | private final int span; 41 | private int width; 42 | private int height; 43 | 44 | public StaggeredItemEntry(int startLane, int anchorLane, int span) { 45 | super(startLane, anchorLane); 46 | this.span = span; 47 | } 48 | 49 | public StaggeredItemEntry(Parcel in) { 50 | super(in); 51 | this.span = in.readInt(); 52 | this.width = in.readInt(); 53 | this.height = in.readInt(); 54 | } 55 | 56 | @Override 57 | public void writeToParcel(Parcel out, int flags) { 58 | super.writeToParcel(out, flags); 59 | out.writeInt(span); 60 | out.writeInt(width); 61 | out.writeInt(height); 62 | } 63 | 64 | public static final Parcelable.Creator CREATOR 65 | = new Parcelable.Creator() { 66 | @Override 67 | public StaggeredItemEntry createFromParcel(Parcel in) { 68 | return new StaggeredItemEntry(in); 69 | } 70 | 71 | @Override 72 | public StaggeredItemEntry[] newArray(int size) { 73 | return new StaggeredItemEntry[size]; 74 | } 75 | }; 76 | } 77 | 78 | public StaggeredGridLayoutManager(Context context) { 79 | this(context, null); 80 | } 81 | 82 | public StaggeredGridLayoutManager(Context context, AttributeSet attrs) { 83 | this(context, attrs, 0); 84 | } 85 | 86 | public StaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyle) { 87 | super(context, attrs, defStyle, DEFAULT_NUM_COLS, DEFAULT_NUM_ROWS); 88 | } 89 | 90 | public StaggeredGridLayoutManager(Orientation orientation, int numColumns, int numRows) { 91 | super(orientation, numColumns, numRows); 92 | } 93 | 94 | @Override 95 | int getLaneSpanForChild(View child) { 96 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 97 | return lp.span; 98 | } 99 | 100 | @Override 101 | int getLaneSpanForPosition(int position) { 102 | final StaggeredItemEntry entry = (StaggeredItemEntry) getItemEntryForPosition(position); 103 | if (entry == null) { 104 | throw new IllegalStateException("Could not find span for position " + position); 105 | } 106 | 107 | return entry.span; 108 | } 109 | 110 | @Override 111 | void getLaneForPosition(LaneInfo outInfo, int position, Direction direction) { 112 | final StaggeredItemEntry entry = (StaggeredItemEntry) getItemEntryForPosition(position); 113 | if (entry != null) { 114 | outInfo.set(entry.startLane, entry.anchorLane); 115 | return; 116 | } 117 | 118 | outInfo.setUndefined(); 119 | } 120 | 121 | @Override 122 | void getLaneForChild(LaneInfo outInfo, View child, Direction direction) { 123 | super.getLaneForChild(outInfo, child, direction); 124 | if (outInfo.isUndefined()) { 125 | getLanes().findLane(outInfo, getLaneSpanForChild(child), direction); 126 | } 127 | } 128 | 129 | @Override 130 | void moveLayoutToPosition(int position, int offset, Recycler recycler, State state) { 131 | final boolean isVertical = isVertical(); 132 | final Lanes lanes = getLanes(); 133 | 134 | lanes.reset(0); 135 | 136 | for (int i = 0; i <= position; i++) { 137 | StaggeredItemEntry entry = (StaggeredItemEntry) getItemEntryForPosition(i); 138 | 139 | if (entry != null) { 140 | mTempLaneInfo.set(entry.startLane, entry.anchorLane); 141 | 142 | // The lanes might have been invalidated because an added or 143 | // removed item. See BaseLayoutManager.invalidateItemLanes(). 144 | if (mTempLaneInfo.isUndefined()) { 145 | lanes.findLane(mTempLaneInfo, getLaneSpanForPosition(i), Direction.END); 146 | entry.setLane(mTempLaneInfo); 147 | } 148 | 149 | lanes.getChildFrame(mTempRect, entry.width, entry.height, mTempLaneInfo, 150 | Direction.END); 151 | } else { 152 | final View child = recycler.getViewForPosition(i); 153 | 154 | // XXX: This might potentially cause stalls in the main 155 | // thread if the layout ends up having to measure tons of 156 | // child views. We might need to add different policies based 157 | // on known assumptions regarding certain layouts e.g. child 158 | // views have stable aspect ratio, lane size is fixed, etc. 159 | measureChild(child, Direction.END); 160 | 161 | // The measureChild() call ensures an entry is created for 162 | // this position. 163 | entry = (StaggeredItemEntry) getItemEntryForPosition(i); 164 | 165 | mTempLaneInfo.set(entry.startLane, entry.anchorLane); 166 | lanes.getChildFrame(mTempRect, getDecoratedMeasuredWidth(child), 167 | getDecoratedMeasuredHeight(child), mTempLaneInfo, Direction.END); 168 | 169 | cacheItemFrame(entry, mTempRect); 170 | } 171 | 172 | if (i != position) { 173 | pushChildFrame(entry, mTempRect, entry.startLane, entry.span, Direction.END); 174 | } 175 | } 176 | 177 | lanes.getLane(mTempLaneInfo.startLane, mTempRect); 178 | lanes.reset(Direction.END); 179 | lanes.offset(offset - (isVertical ? mTempRect.bottom : mTempRect.right)); 180 | } 181 | 182 | @Override 183 | ItemEntry cacheChildLaneAndSpan(View child, Direction direction) { 184 | final int position = getPosition(child); 185 | 186 | mTempLaneInfo.setUndefined(); 187 | 188 | StaggeredItemEntry entry = (StaggeredItemEntry) getItemEntryForPosition(position); 189 | if (entry != null) { 190 | mTempLaneInfo.set(entry.startLane, entry.anchorLane); 191 | } 192 | 193 | if (mTempLaneInfo.isUndefined()) { 194 | getLaneForChild(mTempLaneInfo, child, direction); 195 | } 196 | 197 | if (entry == null) { 198 | entry = new StaggeredItemEntry(mTempLaneInfo.startLane, mTempLaneInfo.anchorLane, 199 | getLaneSpanForChild(child)); 200 | setItemEntryForPosition(position, entry); 201 | } else { 202 | entry.setLane(mTempLaneInfo); 203 | } 204 | 205 | return entry; 206 | } 207 | 208 | void cacheItemFrame(StaggeredItemEntry entry, Rect childFrame) { 209 | entry.width = childFrame.right - childFrame.left; 210 | entry.height = childFrame.bottom - childFrame.top; 211 | } 212 | 213 | @Override 214 | ItemEntry cacheChildFrame(View child, Rect childFrame) { 215 | StaggeredItemEntry entry = (StaggeredItemEntry) getItemEntryForPosition(getPosition(child)); 216 | if (entry == null) { 217 | throw new IllegalStateException("Tried to cache frame on undefined item"); 218 | } 219 | 220 | cacheItemFrame(entry, childFrame); 221 | return entry; 222 | } 223 | 224 | @Override 225 | public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { 226 | boolean result = super.checkLayoutParams(lp); 227 | if (lp instanceof LayoutParams) { 228 | final LayoutParams staggeredLp = (LayoutParams) lp; 229 | result &= (staggeredLp.span >= 1 && staggeredLp.span <= getLaneCount()); 230 | } 231 | 232 | return result; 233 | } 234 | 235 | @Override 236 | public LayoutParams generateDefaultLayoutParams() { 237 | if (isVertical()) { 238 | return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 239 | } else { 240 | return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); 241 | } 242 | } 243 | 244 | @Override 245 | public LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 246 | final LayoutParams staggeredLp = new LayoutParams((ViewGroup.MarginLayoutParams) lp); 247 | if (isVertical()) { 248 | staggeredLp.width = LayoutParams.MATCH_PARENT; 249 | staggeredLp.height = lp.height; 250 | } else { 251 | staggeredLp.width = lp.width; 252 | staggeredLp.height = LayoutParams.MATCH_PARENT; 253 | } 254 | 255 | if (lp instanceof LayoutParams) { 256 | final LayoutParams other = (LayoutParams) lp; 257 | staggeredLp.span = Math.max(1, Math.min(other.span, getLaneCount())); 258 | } 259 | 260 | return staggeredLp; 261 | } 262 | 263 | @Override 264 | public LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { 265 | return new LayoutParams(c, attrs); 266 | } 267 | 268 | public static class LayoutParams extends TwoWayView.LayoutParams { 269 | private static final int DEFAULT_SPAN = 1; 270 | 271 | public int span; 272 | 273 | public LayoutParams(int width, int height) { 274 | super(width, height); 275 | span = DEFAULT_SPAN; 276 | } 277 | 278 | public LayoutParams(Context c, AttributeSet attrs) { 279 | super(c, attrs); 280 | 281 | TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.twowayview_StaggeredGridViewChild); 282 | span = Math.max(DEFAULT_SPAN, a.getInt(R.styleable.twowayview_StaggeredGridViewChild_twowayview_span, -1)); 283 | a.recycle(); 284 | } 285 | 286 | public LayoutParams(ViewGroup.LayoutParams other) { 287 | super(other); 288 | init(other); 289 | } 290 | 291 | public LayoutParams(ViewGroup.MarginLayoutParams other) { 292 | super(other); 293 | init(other); 294 | } 295 | 296 | private void init(ViewGroup.LayoutParams other) { 297 | if (other instanceof LayoutParams) { 298 | final LayoutParams lp = (LayoutParams) other; 299 | span = lp.span; 300 | } else { 301 | span = DEFAULT_SPAN; 302 | } 303 | } 304 | } 305 | } -------------------------------------------------------------------------------- /layouts/src/main/java/org/lucasr/twowayview/widget/TwoWayView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 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 | 17 | package org.lucasr.twowayview.widget; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.support.v7.widget.RecyclerView; 22 | import android.text.TextUtils; 23 | import android.util.AttributeSet; 24 | 25 | import org.lucasr.twowayview.TwoWayLayoutManager; 26 | import org.lucasr.twowayview.TwoWayLayoutManager.Orientation; 27 | 28 | import java.lang.reflect.Constructor; 29 | 30 | public class TwoWayView extends RecyclerView { 31 | private static final String LOGTAG = "TwoWayView"; 32 | 33 | private static final Class[] sConstructorSignature = new Class[] { 34 | Context.class, AttributeSet.class}; 35 | 36 | final Object[] sConstructorArgs = new Object[2]; 37 | 38 | public TwoWayView(Context context) { 39 | this(context, null); 40 | } 41 | 42 | public TwoWayView(Context context, AttributeSet attrs) { 43 | this(context, attrs, 0); 44 | } 45 | 46 | public TwoWayView(Context context, AttributeSet attrs, int defStyle) { 47 | super(context, attrs, defStyle); 48 | 49 | final TypedArray a = 50 | context.obtainStyledAttributes(attrs, R.styleable.twowayview_TwoWayView, defStyle, 0); 51 | 52 | final String name = a.getString(R.styleable.twowayview_TwoWayView_twowayview_layoutManager); 53 | if (!TextUtils.isEmpty(name)) { 54 | loadLayoutManagerFromName(context, attrs, name); 55 | } 56 | 57 | a.recycle(); 58 | } 59 | 60 | private void loadLayoutManagerFromName(Context context, AttributeSet attrs, String name) { 61 | try { 62 | final int dotIndex = name.indexOf('.'); 63 | if (dotIndex == -1) { 64 | name = "org.lucasr.twowayview.widget." + name; 65 | } else if (dotIndex == 0) { 66 | final String packageName = context.getPackageName(); 67 | name = packageName + "." + name; 68 | } 69 | 70 | Class clazz = 71 | context.getClassLoader().loadClass(name).asSubclass(TwoWayLayoutManager.class); 72 | 73 | Constructor constructor = 74 | clazz.getConstructor(sConstructorSignature); 75 | 76 | sConstructorArgs[0] = context; 77 | sConstructorArgs[1] = attrs; 78 | 79 | setLayoutManager(constructor.newInstance(sConstructorArgs)); 80 | } catch (Exception e) { 81 | throw new IllegalStateException("Could not load TwoWayLayoutManager from " + 82 | "class: " + name, e); 83 | } 84 | } 85 | 86 | @Override 87 | public void setLayoutManager(LayoutManager layout) { 88 | if (!(layout instanceof TwoWayLayoutManager)) { 89 | throw new IllegalArgumentException("TwoWayView can only use TwoWayLayoutManager " + 90 | "subclasses as its layout manager"); 91 | } 92 | 93 | super.setLayoutManager(layout); 94 | } 95 | 96 | public Orientation getOrientation() { 97 | TwoWayLayoutManager layout = (TwoWayLayoutManager) getLayoutManager(); 98 | return layout.getOrientation(); 99 | } 100 | 101 | public void setOrientation(Orientation orientation) { 102 | TwoWayLayoutManager layout = (TwoWayLayoutManager) getLayoutManager(); 103 | layout.setOrientation(orientation); 104 | } 105 | 106 | public int getFirstVisiblePosition() { 107 | TwoWayLayoutManager layout = (TwoWayLayoutManager) getLayoutManager(); 108 | return layout.getFirstVisiblePosition(); 109 | } 110 | 111 | public int getLastVisiblePosition() { 112 | TwoWayLayoutManager layout = (TwoWayLayoutManager) getLayoutManager(); 113 | return layout.getLastVisiblePosition(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /layouts/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | repositories { 4 | mavenCentral() 5 | } 6 | 7 | android { 8 | compileSdkVersion 21 9 | buildToolsVersion "21.1.2" 10 | 11 | defaultConfig { 12 | minSdkVersion 10 13 | targetSdkVersion 21 14 | versionCode 1 15 | versionName "1.0" 16 | } 17 | } 18 | 19 | dependencies { 20 | compile project(':core') 21 | compile project(':layouts') 22 | compile 'com.android.support:appcompat-v7:21.0.0' 23 | } 24 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /sample/src/main/java/org/lucasr/twowayview/sample/LayoutAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 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 | 17 | package org.lucasr.twowayview.sample; 18 | 19 | import android.content.Context; 20 | import android.support.v7.widget.RecyclerView; 21 | import android.view.LayoutInflater; 22 | import android.view.View; 23 | import android.view.ViewGroup; 24 | import android.widget.TextView; 25 | 26 | import org.lucasr.twowayview.TwoWayLayoutManager; 27 | import org.lucasr.twowayview.widget.TwoWayView; 28 | import org.lucasr.twowayview.widget.SpannableGridLayoutManager; 29 | import org.lucasr.twowayview.widget.StaggeredGridLayoutManager; 30 | 31 | import java.util.ArrayList; 32 | import java.util.List; 33 | 34 | public class LayoutAdapter extends RecyclerView.Adapter { 35 | private static final int COUNT = 100; 36 | 37 | private final Context mContext; 38 | private final TwoWayView mRecyclerView; 39 | private final List mItems; 40 | private final int mLayoutId; 41 | private int mCurrentItemId = 0; 42 | 43 | public static class SimpleViewHolder extends RecyclerView.ViewHolder { 44 | public final TextView title; 45 | 46 | public SimpleViewHolder(View view) { 47 | super(view); 48 | title = (TextView) view.findViewById(R.id.title); 49 | } 50 | } 51 | 52 | public LayoutAdapter(Context context, TwoWayView recyclerView, int layoutId) { 53 | mContext = context; 54 | mItems = new ArrayList(COUNT); 55 | for (int i = 0; i < COUNT; i++) { 56 | addItem(i); 57 | } 58 | 59 | mRecyclerView = recyclerView; 60 | mLayoutId = layoutId; 61 | } 62 | 63 | public void addItem(int position) { 64 | final int id = mCurrentItemId++; 65 | mItems.add(position, id); 66 | notifyItemInserted(position); 67 | } 68 | 69 | public void removeItem(int position) { 70 | mItems.remove(position); 71 | notifyItemRemoved(position); 72 | } 73 | 74 | @Override 75 | public SimpleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 76 | final View view = LayoutInflater.from(mContext).inflate(R.layout.item, parent, false); 77 | return new SimpleViewHolder(view); 78 | } 79 | 80 | @Override 81 | public void onBindViewHolder(SimpleViewHolder holder, int position) { 82 | holder.title.setText(mItems.get(position).toString()); 83 | 84 | boolean isVertical = (mRecyclerView.getOrientation() == TwoWayLayoutManager.Orientation.VERTICAL); 85 | final View itemView = holder.itemView; 86 | 87 | final int itemId = mItems.get(position); 88 | 89 | if (mLayoutId == R.layout.layout_staggered_grid) { 90 | final int dimenId; 91 | if (itemId % 3 == 0) { 92 | dimenId = R.dimen.staggered_child_medium; 93 | } else if (itemId % 5 == 0) { 94 | dimenId = R.dimen.staggered_child_large; 95 | } else if (itemId % 7 == 0) { 96 | dimenId = R.dimen.staggered_child_xlarge; 97 | } else { 98 | dimenId = R.dimen.staggered_child_small; 99 | } 100 | 101 | final int span; 102 | if (itemId == 2) { 103 | span = 2; 104 | } else { 105 | span = 1; 106 | } 107 | 108 | final int size = mContext.getResources().getDimensionPixelSize(dimenId); 109 | 110 | final StaggeredGridLayoutManager.LayoutParams lp = 111 | (StaggeredGridLayoutManager.LayoutParams) itemView.getLayoutParams(); 112 | 113 | if (!isVertical) { 114 | lp.span = span; 115 | lp.width = size; 116 | itemView.setLayoutParams(lp); 117 | } else { 118 | lp.span = span; 119 | lp.height = size; 120 | itemView.setLayoutParams(lp); 121 | } 122 | } else if (mLayoutId == R.layout.layout_spannable_grid) { 123 | final SpannableGridLayoutManager.LayoutParams lp = 124 | (SpannableGridLayoutManager.LayoutParams) itemView.getLayoutParams(); 125 | 126 | final int span1 = (itemId == 0 || itemId == 3 ? 2 : 1); 127 | final int span2 = (itemId == 0 ? 2 : (itemId == 3 ? 3 : 1)); 128 | 129 | final int colSpan = (isVertical ? span2 : span1); 130 | final int rowSpan = (isVertical ? span1 : span2); 131 | 132 | if (lp.rowSpan != rowSpan || lp.colSpan != colSpan) { 133 | lp.rowSpan = rowSpan; 134 | lp.colSpan = colSpan; 135 | itemView.setLayoutParams(lp); 136 | } 137 | } 138 | } 139 | 140 | @Override 141 | public int getItemCount() { 142 | return mItems.size(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /sample/src/main/java/org/lucasr/twowayview/sample/LayoutFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 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 | 17 | package org.lucasr.twowayview.sample; 18 | 19 | import android.app.Activity; 20 | import android.graphics.drawable.Drawable; 21 | import android.os.Bundle; 22 | import android.support.v4.app.Fragment; 23 | import android.support.v7.widget.RecyclerView; 24 | import android.view.Gravity; 25 | import android.view.LayoutInflater; 26 | import android.view.View; 27 | import android.view.ViewGroup; 28 | import android.widget.TextView; 29 | import android.widget.Toast; 30 | 31 | import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE; 32 | import static android.support.v7.widget.RecyclerView.SCROLL_STATE_DRAGGING; 33 | import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING; 34 | 35 | import org.lucasr.twowayview.ItemClickSupport; 36 | import org.lucasr.twowayview.ItemClickSupport.OnItemClickListener; 37 | import org.lucasr.twowayview.ItemClickSupport.OnItemLongClickListener; 38 | import org.lucasr.twowayview.widget.DividerItemDecoration; 39 | import org.lucasr.twowayview.widget.TwoWayView; 40 | 41 | public class LayoutFragment extends Fragment { 42 | private static final String ARG_LAYOUT_ID = "layout_id"; 43 | 44 | private TwoWayView mRecyclerView; 45 | private TextView mPositionText; 46 | private TextView mCountText; 47 | private TextView mStateText; 48 | private Toast mToast; 49 | 50 | private int mLayoutId; 51 | 52 | public static LayoutFragment newInstance(int layoutId) { 53 | LayoutFragment fragment = new LayoutFragment(); 54 | 55 | Bundle args = new Bundle(); 56 | args.putInt(ARG_LAYOUT_ID, layoutId); 57 | fragment.setArguments(args); 58 | 59 | return fragment; 60 | } 61 | 62 | @Override 63 | public void onCreate(Bundle savedInstanceState) { 64 | super.onCreate(savedInstanceState); 65 | mLayoutId = getArguments().getInt(ARG_LAYOUT_ID); 66 | } 67 | 68 | @Override 69 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 70 | Bundle savedInstanceState) { 71 | return inflater.inflate(mLayoutId, container, false); 72 | } 73 | 74 | @Override 75 | public void onViewCreated(View view, Bundle savedInstanceState) { 76 | super.onViewCreated(view, savedInstanceState); 77 | 78 | final Activity activity = getActivity(); 79 | 80 | mToast = Toast.makeText(activity, "", Toast.LENGTH_SHORT); 81 | mToast.setGravity(Gravity.CENTER, 0, 0); 82 | 83 | mRecyclerView = (TwoWayView) view.findViewById(R.id.list); 84 | mRecyclerView.setHasFixedSize(true); 85 | mRecyclerView.setLongClickable(true); 86 | 87 | mPositionText = (TextView) view.getRootView().findViewById(R.id.position); 88 | mCountText = (TextView) view.getRootView().findViewById(R.id.count); 89 | 90 | mStateText = (TextView) view.getRootView().findViewById(R.id.state); 91 | updateState(SCROLL_STATE_IDLE); 92 | 93 | final ItemClickSupport itemClick = ItemClickSupport.addTo(mRecyclerView); 94 | 95 | itemClick.setOnItemClickListener(new OnItemClickListener() { 96 | @Override 97 | public void onItemClick(RecyclerView parent, View child, int position, long id) { 98 | mToast.setText("Item clicked: " + position); 99 | mToast.show(); 100 | } 101 | }); 102 | 103 | itemClick.setOnItemLongClickListener(new OnItemLongClickListener() { 104 | @Override 105 | public boolean onItemLongClick(RecyclerView parent, View child, int position, long id) { 106 | mToast.setText("Item long pressed: " + position); 107 | mToast.show(); 108 | return true; 109 | } 110 | }); 111 | 112 | mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { 113 | @Override 114 | public void onScrollStateChanged(RecyclerView recyclerView, int scrollState) { 115 | updateState(scrollState); 116 | } 117 | 118 | @Override 119 | public void onScrolled(RecyclerView recyclerView, int i, int i2) { 120 | mPositionText.setText("First: " + mRecyclerView.getFirstVisiblePosition()); 121 | mCountText.setText("Count: " + mRecyclerView.getChildCount()); 122 | } 123 | }); 124 | 125 | final Drawable divider = getResources().getDrawable(R.drawable.divider); 126 | mRecyclerView.addItemDecoration(new DividerItemDecoration(divider)); 127 | 128 | mRecyclerView.setAdapter(new LayoutAdapter(activity, mRecyclerView, mLayoutId)); 129 | } 130 | 131 | private void updateState(int scrollState) { 132 | String stateName = "Undefined"; 133 | switch(scrollState) { 134 | case SCROLL_STATE_IDLE: 135 | stateName = "Idle"; 136 | break; 137 | 138 | case SCROLL_STATE_DRAGGING: 139 | stateName = "Dragging"; 140 | break; 141 | 142 | case SCROLL_STATE_SETTLING: 143 | stateName = "Flinging"; 144 | break; 145 | } 146 | 147 | mStateText.setText(stateName); 148 | } 149 | 150 | public int getLayoutId() { 151 | return getArguments().getInt(ARG_LAYOUT_ID); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /sample/src/main/java/org/lucasr/twowayview/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 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 | 17 | package org.lucasr.twowayview.sample; 18 | 19 | import android.os.Bundle; 20 | import android.support.v4.app.FragmentTransaction; 21 | import android.support.v7.app.ActionBar; 22 | import android.support.v7.app.ActionBarActivity; 23 | 24 | public class MainActivity extends ActionBarActivity { 25 | private final String ARG_SELECTED_LAYOUT_ID = "selectedLayoutId"; 26 | 27 | private final int DEFAULT_LAYOUT = R.layout.layout_list; 28 | 29 | private int mSelectedLayoutId; 30 | 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_main); 35 | 36 | ActionBar actionBar = getSupportActionBar(); 37 | actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); 38 | actionBar.setDisplayShowTitleEnabled(false); 39 | actionBar.setDisplayShowHomeEnabled(false); 40 | 41 | mSelectedLayoutId = DEFAULT_LAYOUT; 42 | if (savedInstanceState != null) { 43 | mSelectedLayoutId = savedInstanceState.getInt(ARG_SELECTED_LAYOUT_ID); 44 | } 45 | 46 | addLayoutTab( 47 | actionBar, R.layout.layout_list, R.drawable.ic_list, "list"); 48 | addLayoutTab( 49 | actionBar, R.layout.layout_grid, R.drawable.ic_grid, "grid"); 50 | addLayoutTab( 51 | actionBar, R.layout.layout_staggered_grid, R.drawable.ic_staggered, "staggered"); 52 | addLayoutTab( 53 | actionBar, R.layout.layout_spannable_grid, R.drawable.ic_spannable, "spannable"); 54 | } 55 | 56 | @Override 57 | protected void onSaveInstanceState(Bundle outState) { 58 | super.onSaveInstanceState(outState); 59 | outState.putInt(ARG_SELECTED_LAYOUT_ID, mSelectedLayoutId); 60 | } 61 | 62 | private void addLayoutTab(ActionBar actionBar, int layoutId, int iconId, String tag) { 63 | ActionBar.Tab tab = actionBar.newTab() 64 | .setText("") 65 | .setIcon(iconId) 66 | .setTabListener(new TabListener(layoutId, tag)); 67 | actionBar.addTab(tab, layoutId == mSelectedLayoutId); 68 | } 69 | 70 | public class TabListener implements ActionBar.TabListener { 71 | private LayoutFragment mFragment; 72 | private final int mLayoutId; 73 | private final String mTag; 74 | 75 | public TabListener(int layoutId, String tag) { 76 | mLayoutId = layoutId; 77 | mTag = tag; 78 | } 79 | 80 | @Override 81 | public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) { 82 | mFragment = (LayoutFragment) getSupportFragmentManager().findFragmentByTag(mTag); 83 | if (mFragment == null) { 84 | mFragment = (LayoutFragment) LayoutFragment.newInstance(mLayoutId); 85 | ft.add(R.id.content, mFragment, mTag); 86 | } else { 87 | ft.attach(mFragment); 88 | } 89 | 90 | mSelectedLayoutId = mFragment.getLayoutId(); 91 | } 92 | 93 | @Override 94 | public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) { 95 | if (mFragment != null) { 96 | ft.detach(mFragment); 97 | } 98 | } 99 | 100 | @Override 101 | public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) { 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-hdpi/ic_grid.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-hdpi/ic_list.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_spannable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-hdpi/ic_spannable.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_staggered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-hdpi/ic_staggered.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-mdpi/ic_grid.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-mdpi/ic_list.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_spannable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-mdpi/ic_spannable.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_staggered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-mdpi/ic_staggered.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-xhdpi/ic_grid.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-xhdpi/ic_list.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_spannable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-xhdpi/ic_spannable.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_staggered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-xhdpi/ic_staggered.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-xxhdpi/ic_grid.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-xxhdpi/ic_list.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_spannable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-xxhdpi/ic_spannable.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_staggered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/twoway-view/e563d319c9166a3d039ce43d5f36df67c1e8fc5a/sample/src/main/res/drawable-xxhdpi/ic_staggered.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/divider.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 20 | 21 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/item_background.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 21 | 22 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | 22 | 26 | 27 | 31 | 32 | 35 | 36 | 39 | 40 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/item.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/layout_grid.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 28 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/layout_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 26 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/layout_spannable_grid.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 28 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/layout_staggered_grid.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 28 | -------------------------------------------------------------------------------- /sample/src/main/res/values-land/styles.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample/src/main/res/values-port/styles.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | #3FBA91 20 | 21 | #DEDEDE 22 | #999999 23 | 24 | #666666 25 | #EEEEEE 26 | #CCCCCC 27 | #77CEEE 28 | 29 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 16dp 20 | 16dp 21 | 22 | 32dp 23 | 16sp 24 | 25 | 1dp 26 | 27 | 120dp 28 | 230dp 29 | 310dp 30 | 400dp 31 | 32 | 33 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | TwoWayView Sample 20 | 21 | 22 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 21 | 22 | 25 | 26 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':core' 2 | include ':layouts' 3 | include ':sample' 4 | --------------------------------------------------------------------------------