├── .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 | 
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 extends TwoWayLayoutManager> clazz =
71 | context.getClassLoader().loadClass(name).asSubclass(TwoWayLayoutManager.class);
72 |
73 | Constructor extends TwoWayLayoutManager> 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 |
--------------------------------------------------------------------------------