├── .github
└── workflows
│ └── android.yml
├── .gitignore
├── .idea
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── encodings.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jarRepositories.xml
├── misc.xml
└── runConfigurations.xml
├── LICENSE
├── README.md
├── RELEASE_NOTES.md
├── app
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── liuzhenlin
│ │ └── simrv
│ │ └── sample
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── liuzhenlin
│ │ │ └── simrv
│ │ │ └── sample
│ │ │ └── MainActivity.java
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ └── item_simrv.xml
│ │ ├── menu
│ │ └── menu_see_github.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── liuzhenlin
│ └── simrv
│ └── sample
│ └── ExampleUnitTest.java
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── jitpack.yml
├── settings.gradle
└── slidingitemmenu-recyclerview
├── build.gradle
└── src
├── androidTest
└── java
│ └── com
│ └── liuzhenlin
│ └── simrv
│ └── ExampleInstrumentedTest.java
├── main
├── AndroidManifest.xml
├── java
│ └── com
│ │ └── liuzhenlin
│ │ └── simrv
│ │ ├── SlidingItemMenuRecyclerView.java
│ │ ├── Utils.java
│ │ ├── ViscousFluidInterpolator.java
│ │ └── reservation
│ │ ├── ScrollerLinearLayout.java
│ │ ├── ScrollerView.java
│ │ └── TopWrappedDividerItemDecoration.java
└── res
│ ├── drawable-v21
│ └── default_selector_recycler_item.xml
│ ├── drawable
│ └── default_selector_recycler_item.xml
│ └── values
│ ├── attrs.xml
│ ├── colors.xml
│ └── tags.xml
└── test
└── java
└── com
└── liuzhenlin
└── simrv
└── ExampleUnitTest.java
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ developers ]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | build:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v3
17 | - name: set up JDK 11
18 | uses: actions/setup-java@v3
19 | with:
20 | java-version: '11'
21 | distribution: 'temurin'
22 | cache: gradle
23 |
24 | - name: Grant execute permission for gradlew
25 | run: chmod +x gradlew
26 | - name: Build with Gradle
27 | run: ./gradlew build
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 | *.aar
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | .idea/workspace.xml
42 | .idea/tasks.xml
43 | .idea/gradle.xml
44 | .idea/assetWizardSettings.xml
45 | .idea/dictionaries
46 | .idea/libraries
47 | # Android Studio 3 in .gitignore file.
48 | .idea/caches
49 | .idea/modules.xml
50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
51 | .idea/navEditor.xml
52 |
53 | # Keystore files
54 | # Uncomment the following lines if you do not want to check your keystore files in.
55 | #*.jks
56 | #*.keystore
57 |
58 | # External native build folder generated in Android Studio 2.2 and later
59 | .externalNativeBuild
60 | .cxx/
61 |
62 | # Google Services (e.g. APIs or Firebase)
63 | # google-services.json
64 |
65 | # Freeline
66 | freeline.py
67 | freeline/
68 | freeline_project_description.json
69 |
70 | # fastlane
71 | fastlane/report.xml
72 | fastlane/Preview.html
73 | fastlane/screenshots
74 | fastlane/test_output
75 | fastlane/readme.md
76 |
77 | # Version control
78 | vcs.xml
79 |
80 | # lint
81 | lint/intermediates/
82 | lint/generated/
83 | lint/outputs/
84 | lint/tmp/
85 | # lint/reports/
86 |
87 | # Desktop Services Store (Mac OS)
88 | .DS_Store
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
141 | * If unable to be dragged, they may be scrolled through the code like:
142 | * simrv.openItemAtPosition(0, true);
143 | */
144 | public void setItemDraggable(boolean draggable) {
145 | mIsItemDraggable = draggable;
146 | }
147 |
148 | /**
149 | * Gets the lasting time of the animator for opening/closing the item view to which
150 | * the animator associated.
151 | * The default duration is {@value DEFAULT_ITEM_SCROLL_DURATION} milliseconds.
152 | *
153 | * @return the duration of the animator
154 | */
155 | public int getItemScrollDuration() {
156 | return mItemScrollDuration;
157 | }
158 |
159 | /**
160 | * Sets the duration for the animators used to open/close the item views.
161 | *
162 | * @throws IllegalArgumentException if a negative 'duration' is passed in
163 | */
164 | public void setItemScrollDuration(int duration) {
165 | if (duration < 0) {
166 | throw new IllegalArgumentException("The animators for opening/closing the item views " +
167 | "cannot have negative duration: " + duration);
168 | }
169 | mItemScrollDuration = duration;
170 | }
171 |
172 | public SlidingItemMenuRecyclerView(Context context) {
173 | this(context, null);
174 | }
175 |
176 | public SlidingItemMenuRecyclerView(Context context, @Nullable AttributeSet attrs) {
177 | this(context, attrs, 0);
178 | }
179 |
180 | public SlidingItemMenuRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
181 | super(context, attrs, defStyle);
182 | mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
183 | mItemMinimumFlingVelocity = 200f * getResources().getDisplayMetrics().density;
184 |
185 | final TypedArray ta = context.obtainStyledAttributes(
186 | attrs, R.styleable.SlidingItemMenuRecyclerView, defStyle, 0);
187 | if (ta.hasValue(R.styleable.SlidingItemMenuRecyclerView_itemDraggable)) {
188 | setItemDraggable(ta.getBoolean(R.styleable
189 | .SlidingItemMenuRecyclerView_itemDraggable, true));
190 | } else {
191 | // Libraries with version code prior to 5 use the itemScrollingEnabled attr only.
192 | setItemDraggable(ta.getBoolean(R.styleable
193 | .SlidingItemMenuRecyclerView_itemScrollingEnabled /* deprecated */, true));
194 | }
195 | setItemScrollDuration(ta.getInteger(R.styleable
196 | .SlidingItemMenuRecyclerView_itemScrollDuration, DEFAULT_ITEM_SCROLL_DURATION));
197 | ta.recycle();
198 | }
199 |
200 | @Override
201 | public void setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled) {
202 | mIsVerticalScrollBarEnabled = verticalScrollBarEnabled;
203 | super.setVerticalScrollBarEnabled(verticalScrollBarEnabled);
204 | }
205 |
206 | private boolean childHasMenu(ViewGroup itemView) {
207 | if (itemView.getVisibility() != VISIBLE) return false;
208 |
209 | final int itemChildCount = itemView.getChildCount();
210 | final View itemLastChild = itemView.getChildAt(itemChildCount >= 2 ? itemChildCount - 1 : 1);
211 | if (!(itemLastChild instanceof FrameLayout)
212 | || itemLastChild.getVisibility() != View.VISIBLE)
213 | return false;
214 |
215 | final FrameLayout itemMenu = (FrameLayout) itemLastChild;
216 | final int menuItemCount = itemMenu.getChildCount();
217 | final int[] menuItemWidths = new int[menuItemCount];
218 | int itemMenuWidth = 0;
219 | for (int i = 0; i < menuItemCount; i++) {
220 | final FrameLayout menuItemBg = (FrameLayout) itemMenu.getChildAt(i);
221 | // We can not just add up the item menu width with the width of the menu item without
222 | // checking the visibilities of it and its parents, as the visibility of a view
223 | // changing from visible to gone will just exclude it from the subsequent layout passes
224 | // and therefore will usually not have its width and height properties updated.
225 | if (menuItemBg.getVisibility() == View.VISIBLE) {
226 | final View menuItem = menuItemBg.getChildAt(0);
227 | if (menuItem.getVisibility() == View.VISIBLE) {
228 | menuItemWidths[i] = menuItem.getWidth();
229 | itemMenuWidth += menuItemWidths[i];
230 | }
231 | }
232 | }
233 | if (itemMenuWidth > 0) {
234 | itemView.setTag(TAG_ITEM_MENU_WIDTH, itemMenuWidth);
235 | itemView.setTag(TAG_MENU_ITEM_WIDTHS, menuItemWidths);
236 | return true;
237 | }
238 | return false;
239 | }
240 |
241 | private void resolveActiveItemMenuBounds() {
242 | final int itemMenuWidth = (int) mActiveItem.getTag(TAG_ITEM_MENU_WIDTH);
243 | final int left = Utils.isLayoutRtl(mActiveItem) ? 0 : mActiveItem.getRight() - itemMenuWidth;
244 | final int right = left + itemMenuWidth;
245 | mActiveItemMenuBounds.set(left, mActiveItemBounds.top,
246 | right, mActiveItemBounds.bottom);
247 | }
248 |
249 | @Override
250 | public boolean onInterceptTouchEvent(MotionEvent e) {
251 | final int action = e.getAction();
252 | if (action == MotionEvent.ACTION_DOWN) {
253 | // Reset things for a new event stream, just in case we didn't get
254 | // the whole previous stream.
255 | resetTouch();
256 | }
257 |
258 | if (mVelocityTracker == null)
259 | mVelocityTracker = VelocityTracker.obtain();
260 | mVelocityTracker.addMovement(e);
261 |
262 | boolean intercept = false;
263 | switch (action) {
264 | case MotionEvent.ACTION_DOWN:
265 | mDownX = Utils.roundFloat(e.getX());
266 | mDownY = Utils.roundFloat(e.getY());
267 | markCurrTouchPoint(mDownX, mDownY);
268 |
269 | for (int i = getChildCount() - 1; i >= 0; i--) {
270 | final View child = getChildAt(i);
271 | if (!(child instanceof ViewGroup)) continue;
272 |
273 | final ViewGroup itemView = (ViewGroup) child;
274 | itemView.getHitRect(mActiveItemBounds);
275 | if (!mActiveItemBounds.contains(mDownX, mDownY)) continue;
276 |
277 | if (childHasMenu(itemView)) {
278 | mActiveItem = itemView;
279 | }
280 | break;
281 | }
282 |
283 | if (mOpenedItems.size() == 0) break;
284 | // Disallow our parent Views to intercept the touch events so long as there is
285 | // at least one item view in the open or being closed state.
286 | requestParentDisallowInterceptTouchEvent();
287 | if (mFullyOpenedItem != null) {
288 | mHasItemFullyOpenOnActionDown = true;
289 | if (mActiveItem == mFullyOpenedItem) {
290 | resolveActiveItemMenuBounds();
291 | // If the user's finger downs on the completely opened itemView's menu area,
292 | // do not intercept the subsequent touch events (ACTION_MOVE, ACTION_UP, etc.)
293 | // as we receive the ACTION_DOWN event.
294 | if (mActiveItemMenuBounds.contains(mDownX, mDownY)) {
295 | break;
296 | // If the user's finger downs on the fully opened itemView but not on
297 | // its menu, then we need to intercept them.
298 | } else if (mActiveItemBounds.contains(mDownX, mDownY)) {
299 | return true;
300 | }
301 | }
302 | // If 1) the fully opened itemView is not the current one or 2) the user's
303 | // finger downs outside of the area in which this view displays the itemViews,
304 | // make the itemView's menu hidden and intercept the subsequent touch events.
305 | releaseItemViewInternal(mFullyOpenedItem, mItemScrollDuration);
306 | }
307 | // Intercept the next touch events as long as there exists some item view open
308 | // (full open is not necessary for it). This prevents the onClick() method of
309 | // the pressed child from being called in the pending ACTION_UP event.
310 | return true;
311 |
312 | case MotionEvent.ACTION_MOVE:
313 | markCurrTouchPoint(e.getX(), e.getY());
314 |
315 | intercept = tryHandleItemScrollingEvent();
316 | // If the user initially put his/her finger down on the fully opened itemView's menu,
317 | // disallow our parent class to intercept the touch events since we will do that
318 | // as the user tends to scroll the current touched itemView horizontally.
319 | if (mHasItemFullyOpenOnActionDown && mActiveItemMenuBounds.contains(mDownX, mDownY)) {
320 | return intercept;
321 | }
322 | break;
323 |
324 | case MotionEvent.ACTION_UP:
325 | case MotionEvent.ACTION_CANCEL:
326 | // If the user initially placed his/her finger on the fully opened itemView's menu
327 | // and has clicked it or has not scrolled that itemView, hide it as his/her last
328 | // finger touching the screen lifts.
329 | if (mHasItemFullyOpenOnActionDown && mActiveItemMenuBounds.contains(mDownX, mDownY)) {
330 | releaseItemView(true);
331 | }
332 | clearTouch();
333 | break;
334 | }
335 | return intercept || super.onInterceptTouchEvent(e);
336 | }
337 |
338 | @SuppressLint("ClickableViewAccessibility")
339 | @Override
340 | public boolean onTouchEvent(MotionEvent e) {
341 | if (mIsVerticalScrollBarEnabled) {
342 | // Makes the vertical scroll bar disappear while an itemView is being dragged.
343 | super.setVerticalScrollBarEnabled(!mIsItemBeingDragged);
344 | }
345 |
346 | if (mVelocityTracker == null)
347 | mVelocityTracker = VelocityTracker.obtain();
348 | mVelocityTracker.addMovement(e);
349 |
350 | switch (e.getAction() & MotionEvent.ACTION_MASK) {
351 | case MotionEvent.ACTION_POINTER_DOWN:
352 | case MotionEvent.ACTION_POINTER_UP:
353 | if (mIsItemBeingDragged || mHasItemFullyOpenOnActionDown || mOpenedItems.size() > 0) {
354 | return true;
355 | }
356 | break;
357 |
358 | case MotionEvent.ACTION_MOVE:
359 | markCurrTouchPoint(e.getX(), e.getY());
360 |
361 | if (!mIsItemDraggable && cancelTouch()) {
362 | return true;
363 | }
364 | if (mIsItemBeingDragged) {
365 | // Positive when the user's finger slides towards the right.
366 | float dx = mTouchX[mTouchX.length - 1] - mTouchX[mTouchX.length - 2];
367 | // Positive when the itemView scrolls towards the right.
368 | final float translationX = mActiveItem.getChildAt(0).getTranslationX();
369 | final boolean rtl = Utils.isLayoutRtl(mActiveItem);
370 | final int finalXFromEndToStart =
371 | rtl ? (int) mActiveItem.getTag(TAG_ITEM_MENU_WIDTH)
372 | : -(int) (mActiveItem.getTag(TAG_ITEM_MENU_WIDTH));
373 | // Swipe the itemView towards the horizontal start over the width of
374 | // the itemView's menu.
375 | if (!rtl && dx + translationX < finalXFromEndToStart
376 | || rtl && dx + translationX > finalXFromEndToStart) {
377 | dx = dx / 3f;
378 | // Swipe the itemView towards the end of horizontal to (0,0).
379 | } else if (!rtl && dx + translationX > 0 || rtl && dx + translationX < 0) {
380 | dx = 0 - translationX;
381 | }
382 | translateItemViewXBy(mActiveItem, dx);
383 |
384 | // Consume this touch event and do not invoke the method onTouchEvent(e) of
385 | // the parent class to temporarily make this view unable to scroll up or down.
386 | return true;
387 | } else {
388 | // If there existed itemView whose menu was fully open when the user initially
389 | // put his/her finger down, always consume the touch event and only when the item
390 | // has a tend of scrolling horizontally will we handle the next events.
391 | if (mHasItemFullyOpenOnActionDown | tryHandleItemScrollingEvent()) {
392 | return true;
393 | }
394 | // Disallow current view to scroll while an/some item view(s) is/are scrolling.
395 | if (mOpenedItems.size() > 0) {
396 | return true;
397 | }
398 | }
399 | break;
400 |
401 | case MotionEvent.ACTION_UP:
402 | if (mIsItemDraggable && mIsItemBeingDragged) {
403 | final boolean rtl = Utils.isLayoutRtl(mActiveItem);
404 | final float translationX = mActiveItem.getChildAt(0).getTranslationX();
405 | final int itemMenuWidth = (int) mActiveItem.getTag(TAG_ITEM_MENU_WIDTH);
406 | //noinspection StatementWithEmptyBody
407 | if (translationX == 0) { // itemView's menu is closed
408 |
409 | // itemView's menu is totally opened
410 | } else if (!rtl && translationX == -itemMenuWidth
411 | || rtl && translationX == itemMenuWidth) {
412 | mFullyOpenedItem = mActiveItem;
413 |
414 | } else {
415 | final float dx =
416 | rtl ? mTouchX[mTouchX.length - 2] - mTouchX[mTouchX.length - 1]
417 | : mTouchX[mTouchX.length - 1] - mTouchX[mTouchX.length - 2];
418 | mVelocityTracker.computeCurrentVelocity(1000);
419 | final float velocityX = Math.abs(mVelocityTracker.getXVelocity());
420 | // If the speed at which the user's finger lifted is greater than 200 dp/s
421 | // while user was scrolling itemView towards the horizontal start,
422 | // make it automatically scroll to open and show its menu.
423 | if (dx < 0 && velocityX >= mItemMinimumFlingVelocity) {
424 | smoothTranslateItemViewXTo(
425 | mActiveItem,
426 | rtl ? itemMenuWidth : -itemMenuWidth,
427 | mItemScrollDuration);
428 | mFullyOpenedItem = mActiveItem;
429 | clearTouch();
430 | cancelParentTouch(e);
431 | return true;
432 |
433 | // If the speed at which the user's finger lifted is greater than 200 dp/s
434 | // while user was scrolling itemView towards the end of horizontal,
435 | // make its menu hidden.
436 | } else if (dx > 0 && velocityX >= mItemMinimumFlingVelocity) {
437 | releaseItemView(true);
438 | clearTouch();
439 | cancelParentTouch(e);
440 | return true;
441 | }
442 |
443 | final float middle = itemMenuWidth / 2f;
444 | // If the sliding distance is less than half of its slidable distance,
445 | // hide its menu,
446 | if (Math.abs(translationX) < middle) {
447 | releaseItemView(true);
448 |
449 | // else open its menu.
450 | } else {
451 | smoothTranslateItemViewXTo(
452 | mActiveItem,
453 | rtl ? itemMenuWidth : -itemMenuWidth,
454 | mItemScrollDuration);
455 | mFullyOpenedItem = mActiveItem;
456 | }
457 | }
458 | clearTouch();
459 | cancelParentTouch(e);
460 | return true; // Returns true here in case of a fling started in this up event.
461 | }
462 | case MotionEvent.ACTION_CANCEL:
463 | cancelTouch();
464 | break;
465 | }
466 |
467 | return super.onTouchEvent(e);
468 | }
469 |
470 | private void markCurrTouchPoint(float x, float y) {
471 | System.arraycopy(mTouchX, 1, mTouchX, 0, mTouchX.length - 1);
472 | mTouchX[mTouchX.length - 1] = x;
473 | System.arraycopy(mTouchY, 1, mTouchY, 0, mTouchY.length - 1);
474 | mTouchY[mTouchY.length - 1] = y;
475 | }
476 |
477 | private boolean tryHandleItemScrollingEvent() {
478 | if (mActiveItem == null /* There's no scrollable itemView being touched by user */
479 | || !mIsItemDraggable /* Unable to scroll it */
480 | || getScrollState() != SCROLL_STATE_IDLE /* The list may be currently scrolling */) {
481 | return false;
482 | }
483 | // The layout's orientation may not be vertical.
484 | //noinspection ConstantConditions
485 | if (getLayoutManager().canScrollHorizontally()) {
486 | return false;
487 | }
488 |
489 | final float absDy = Math.abs(mTouchY[mTouchY.length - 1] - mDownY);
490 | if (absDy <= mTouchSlop) {
491 | final float dx = mTouchX[mTouchX.length - 1] - mDownX;
492 | if (mOpenedItems.size() == 0) {
493 | final boolean rtl = Utils.isLayoutRtl(mActiveItem);
494 | mIsItemBeingDragged = rtl && dx > mTouchSlop || !rtl && dx < -mTouchSlop;
495 | } else {
496 | mIsItemBeingDragged = Math.abs(dx) > mTouchSlop;
497 | }
498 | if (mIsItemBeingDragged) {
499 | requestParentDisallowInterceptTouchEvent();
500 | return true;
501 | }
502 | }
503 | return false;
504 | }
505 |
506 | private void requestParentDisallowInterceptTouchEvent() {
507 | final ViewParent parent = getParent();
508 | if (parent != null) {
509 | parent.requestDisallowInterceptTouchEvent(true);
510 | }
511 | }
512 |
513 | private boolean cancelTouch() {
514 | return cancelTouch(true);
515 | }
516 |
517 | private boolean cancelTouch(boolean animate) {
518 | if (mIsItemBeingDragged) {
519 | releaseItemView(animate);
520 | clearTouch();
521 | return true;
522 | }
523 | // 1. If the itemView previously opened equals the current touched one and
524 | // the user hasn't scrolled it since he/she initially put his/her finger down,
525 | // hide it on the movements canceled.
526 | // 2. If the previously opened itemView differs from the one currently touched,
527 | // and the current one has not been scrolled at all, set 'mActiveItem' to null.
528 | if (mHasItemFullyOpenOnActionDown) {
529 | if (mActiveItem == mFullyOpenedItem) {
530 | releaseItemView(animate);
531 | }
532 | clearTouch();
533 | return true;
534 | }
535 | return false;
536 | }
537 |
538 | private void clearTouch() {
539 | if (mVelocityTracker != null) {
540 | mVelocityTracker.recycle();
541 | mVelocityTracker = null;
542 | }
543 | resetTouch();
544 | }
545 |
546 | private void resetTouch() {
547 | mActiveItem = null;
548 | mHasItemFullyOpenOnActionDown = false;
549 | mActiveItemBounds.setEmpty();
550 | mActiveItemMenuBounds.setEmpty();
551 | mIsItemBeingDragged = false;
552 | if (mVelocityTracker != null) {
553 | mVelocityTracker.clear();
554 | }
555 | }
556 |
557 | private void cancelParentTouch(MotionEvent e) {
558 | final int action = e.getAction();
559 | e.setAction(MotionEvent.ACTION_CANCEL);
560 | super.onTouchEvent(e);
561 | e.setAction(action);
562 | }
563 |
564 | /**
565 | * Smoothly scrolls the current item view whose menu is open back to its original position.
566 | *
567 | * @see #releaseItemView(boolean)
568 | */
569 | public void releaseItemView() {
570 | releaseItemView(true);
571 | }
572 |
573 | /**
574 | * Scrolls the current item view whose menu is open back to its original position.
575 | *
576 | * @param animate whether this scroll should be smooth
577 | */
578 | public void releaseItemView(boolean animate) {
579 | releaseItemViewInternal(mIsItemBeingDragged ? mActiveItem : mFullyOpenedItem,
580 | animate ? mItemScrollDuration : 0);
581 | }
582 |
583 | private void releaseItemViewInternal(ViewGroup itemView, int duration) {
584 | if (itemView != null) {
585 | if (duration > 0) {
586 | smoothTranslateItemViewXTo(itemView, 0, duration);
587 | } else {
588 | translateItemViewXTo(itemView, 0);
589 | }
590 | if (mFullyOpenedItem == itemView) {
591 | mFullyOpenedItem = null;
592 | }
593 | }
594 | }
595 |
596 | /**
597 | * Smoothly opens the menu of the item view at the specified adapter position
598 | *
599 | * @param position the position of the item in the data set of the adapter
600 | * @return true if the menu of the child view that represents the given position can be opened;
601 | * false if the position is not laid out or the item does not have a menu.
602 | * @see #openItemAtPosition(int, boolean)
603 | */
604 | public boolean openItemAtPosition(int position) {
605 | return openItemAtPosition(position, true);
606 | }
607 |
608 | /**
609 | * Opens the menu of the item view at the specified adapter position
610 | *
611 | * @param position the position of the item in the data set of the adapter
612 | * @param animate whether this scroll should be smooth
613 | * @return true if the menu of the child view that represents the given position can be opened;
614 | * false if the position is not laid out or the item does not have a menu.
615 | */
616 | public boolean openItemAtPosition(int position, boolean animate) {
617 | final LayoutManager lm = getLayoutManager();
618 | if (lm == null) return false;
619 |
620 | final View view = lm.findViewByPosition(position);
621 | if (!(view instanceof ViewGroup)) return false;
622 |
623 | final ViewGroup itemView = (ViewGroup) view;
624 | if (mFullyOpenedItem != itemView && childHasMenu(itemView)) {
625 | // First, cancels the item view being touched or previously fully opened (if any)
626 | if (!cancelTouch(animate)) {
627 | releaseItemView(animate);
628 | }
629 |
630 | smoothTranslateItemViewXTo(
631 | itemView,
632 | Utils.isLayoutRtl(itemView)
633 | ? (int) itemView.getTag(TAG_ITEM_MENU_WIDTH)
634 | : -(int) (itemView.getTag(TAG_ITEM_MENU_WIDTH)),
635 | animate ? mItemScrollDuration : 0);
636 | mFullyOpenedItem = itemView;
637 | return true;
638 | }
639 | return false;
640 | }
641 |
642 | private void smoothTranslateItemViewXTo(ViewGroup itemView, float x, int duration) {
643 | smoothTranslateItemViewXBy(itemView, x - itemView.getChildAt(0).getTranslationX(), duration);
644 | }
645 |
646 | private void smoothTranslateItemViewXBy(ViewGroup itemView, float dx, int duration) {
647 | TranslateItemViewXAnimator animator =
648 | (TranslateItemViewXAnimator) itemView.getTag(TAG_ITEM_ANIMATOR);
649 |
650 | if (dx != 0 && duration > 0) {
651 | boolean canceled = false;
652 | if (animator == null) {
653 | animator = new TranslateItemViewXAnimator(this, itemView);
654 | itemView.setTag(TAG_ITEM_ANIMATOR, animator);
655 |
656 | } else if (animator.isRunning()) {
657 | animator.removeListener(animator.listener);
658 | animator.cancel();
659 | canceled = true;
660 | }
661 | animator.setFloatValues(0, dx);
662 |
663 | final boolean rtl = Utils.isLayoutRtl(itemView);
664 | final Interpolator interpolator =
665 | !rtl && dx < 0 || rtl && dx > 0 ?
666 | sOvershootInterpolator : sViscousFluidInterpolator;
667 |
668 | animator.setInterpolator(interpolator);
669 | animator.setDuration(duration);
670 | animator.start();
671 | if (canceled) {
672 | animator.addListener(animator.listener);
673 | }
674 | } else {
675 | // Checks if there is an animator running for the given item view even if dx == 0
676 | if (animator != null && animator.isRunning()) {
677 | animator.cancel();
678 | }
679 | // If duration <= 0, then scroll the 'itemView' directly to prevent a redundant call
680 | // to the animator.
681 | baseTranslateItemViewXBy(itemView, dx);
682 | }
683 | }
684 |
685 | private void translateItemViewXTo(
686 | ViewGroup itemView, @SuppressWarnings("SameParameterValue") float x) {
687 | translateItemViewXBy(itemView, x - itemView.getChildAt(0).getTranslationX());
688 | }
689 |
690 | private void translateItemViewXBy(ViewGroup itemView, float dx) {
691 | final TranslateItemViewXAnimator animator =
692 | (TranslateItemViewXAnimator) itemView.getTag(TAG_ITEM_ANIMATOR);
693 | if (animator != null && animator.isRunning()) {
694 | // Cancels the running animator associated to the 'itemView' as we horizontally
695 | // scroll it to a position immediately to avoid inconsistencies in its translation X.
696 | animator.cancel();
697 | }
698 |
699 | baseTranslateItemViewXBy(itemView, dx);
700 | }
701 |
702 | /*
703 | * This method does not cancel the translation animator of the 'itemView', for which it is used
704 | * to update the item view's horizontal scrolled position.
705 | */
706 | /*synthetic*/ void baseTranslateItemViewXBy(ViewGroup itemView, float dx) {
707 | if (dx == 0) return;
708 |
709 | final float translationX = itemView.getChildAt(0).getTranslationX() + dx;
710 | final int itemMenuWidth = (int) itemView.getTag(TAG_ITEM_MENU_WIDTH);
711 |
712 | final boolean rtl = Utils.isLayoutRtl(itemView);
713 | if (!rtl && translationX > -itemMenuWidth * 0.05f
714 | || rtl && translationX < itemMenuWidth * 0.05f) {
715 | mOpenedItems.remove(itemView);
716 |
717 | } else if (!mOpenedItems.contains(itemView)) {
718 | mOpenedItems.add(itemView);
719 | }
720 |
721 | final int itemChildCount = itemView.getChildCount();
722 | for (int i = 0; i < itemChildCount; i++) {
723 | itemView.getChildAt(i).setTranslationX(translationX);
724 | }
725 |
726 | final FrameLayout itemMenu = (FrameLayout) itemView.getChildAt(itemChildCount - 1);
727 | final int[] menuItemWidths = (int[]) itemView.getTag(TAG_MENU_ITEM_WIDTHS);
728 | float menuItemFrameDx = 0;
729 | for (int i = 1, menuItemCount = itemMenu.getChildCount(); i < menuItemCount; i++) {
730 | final FrameLayout menuItemFrame = (FrameLayout) itemMenu.getChildAt(i);
731 | menuItemFrameDx -= dx * (float) menuItemWidths[i - 1] / (float) itemMenuWidth;
732 | menuItemFrame.setTranslationX(menuItemFrame.getTranslationX() + menuItemFrameDx);
733 | }
734 | }
735 |
736 | private static final class TranslateItemViewXAnimator extends ValueAnimator {
737 | final AnimatorListener listener;
738 |
739 | float cachedDeltaTransX;
740 |
741 | TranslateItemViewXAnimator(final SlidingItemMenuRecyclerView parent, final ViewGroup itemView) {
742 | listener = new AnimatorListenerAdapter() {
743 | final SimpleArrayMapx
, y
).
26 | *
27 | * @param x the x position to scroll to
28 | * @param y the y position to scroll to
29 | * @param duration duration of the scroll in milliseconds.
30 | */
31 | void smoothScrollTo(int x, int y, int duration);
32 | }
33 |
--------------------------------------------------------------------------------
/slidingitemmenu-recyclerview/src/main/java/com/liuzhenlin/simrv/reservation/TopWrappedDividerItemDecoration.java:
--------------------------------------------------------------------------------
1 | package com.liuzhenlin.simrv.reservation;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.content.res.TypedArray;
6 | import android.graphics.Canvas;
7 | import android.graphics.Rect;
8 | import android.graphics.drawable.Drawable;
9 | import android.util.Log;
10 | import android.view.View;
11 | import android.widget.LinearLayout;
12 |
13 | import androidx.annotation.NonNull;
14 | import androidx.recyclerview.widget.LinearLayoutManager;
15 | import androidx.recyclerview.widget.RecyclerView;
16 |
17 | import com.liuzhenlin.simrv.Utils;
18 |
19 | public class TopWrappedDividerItemDecoration extends RecyclerView.ItemDecoration {
20 | public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
21 | public static final int VERTICAL = LinearLayout.VERTICAL;
22 |
23 | private static final String TAG = "TopWrappedDividerItemDecoration";
24 | private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
25 |
26 | private Drawable mDivider;
27 |
28 | /**
29 | * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}.
30 | */
31 | private int mOrientation;
32 |
33 | private final Rect mBounds = new Rect();
34 |
35 | /**
36 | * Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a
37 | * {@link LinearLayoutManager}.
38 | *
39 | * @param context Current context, it will be used to access resources.
40 | * @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}.
41 | */
42 | @SuppressLint("LongLogTag")
43 | public TopWrappedDividerItemDecoration(Context context, int orientation) {
44 | final TypedArray a = context.obtainStyledAttributes(ATTRS);
45 | mDivider = a.getDrawable(0);
46 | if (mDivider == null) {
47 | Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this "
48 | + "DividerItemDecoration. Please set that attribute all call setDivider()");
49 | }
50 | a.recycle();
51 | setOrientation(orientation);
52 | }
53 |
54 | /**
55 | * @return the orientation of this divider, either {@link #HORIZONTAL} or {@link #VERTICAL}
56 | */
57 | public int getOrientation() {
58 | return mOrientation;
59 | }
60 |
61 | /**
62 | * Sets the orientation for this divider. This should be called if
63 | * {@link RecyclerView.LayoutManager} changes orientation.
64 | *
65 | * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
66 | */
67 | public void setOrientation(int orientation) {
68 | if (orientation != HORIZONTAL && orientation != VERTICAL) {
69 | throw new IllegalArgumentException(
70 | "Invalid orientation. It should be either HORIZONTAL or VERTICAL");
71 | }
72 | mOrientation = orientation;
73 | }
74 |
75 | /**
76 | * @return the Drawable for this divider
77 | */
78 | @NonNull
79 | public Drawable getDivider() {
80 | return mDivider;
81 | }
82 |
83 | /**
84 | * Sets the {@link Drawable} for this divider.
85 | *
86 | * @param divider Drawable that should be used as a divider.
87 | */
88 | public void setDivider(@NonNull Drawable divider) {
89 | mDivider = divider;
90 | }
91 |
92 | @Override
93 | public void onDraw(
94 | @NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
95 | if (parent.getLayoutManager() == null || mDivider == null)
96 | return;
97 | if (mOrientation == VERTICAL)
98 | drawVertical(c, parent);
99 | else
100 | drawHorizontal(c, parent);
101 | }
102 |
103 | private void drawVertical(Canvas canvas, RecyclerView parent) {
104 | canvas.save();
105 | final int left, right;
106 | if (parent.getClipToPadding()) {
107 | left = parent.getPaddingLeft();
108 | right = parent.getWidth() - parent.getPaddingRight();
109 | canvas.clipRect(left, parent.getPaddingTop(), right,
110 | parent.getHeight() - parent.getPaddingBottom());
111 | } else {
112 | left = 0;
113 | right = parent.getWidth();
114 | }
115 |
116 | final int childCount = parent.getChildCount();
117 | for (int i = 0; i < childCount; i++) {
118 | View child = parent.getChildAt(i);
119 | parent.getDecoratedBoundsWithMargins(child, mBounds);
120 | final int bottom = mBounds.bottom + Utils.roundFloat(child.getTranslationY());
121 | final int top = bottom - mDivider.getIntrinsicHeight();
122 | mDivider.setBounds(left, top, right, bottom);
123 | mDivider.draw(canvas);
124 | // Draw the divider for RecyclerView's top edge
125 | if (i == 0) {
126 | mDivider.setBounds(left, parent.getPaddingTop(), right,
127 | parent.getPaddingTop() + mDivider.getIntrinsicHeight());
128 | mDivider.draw(canvas);
129 | }
130 | }
131 | canvas.restore();
132 | }
133 |
134 | private void drawHorizontal(Canvas canvas, RecyclerView parent) {
135 | canvas.save();
136 | final int top, bottom;
137 | if (parent.getClipToPadding()) {
138 | top = parent.getPaddingTop();
139 | bottom = parent.getHeight() - parent.getPaddingBottom();
140 | canvas.clipRect(parent.getPaddingLeft(), top,
141 | parent.getWidth() - parent.getPaddingRight(), bottom);
142 | } else {
143 | top = 0;
144 | bottom = parent.getHeight();
145 | }
146 |
147 | final int childCount = parent.getChildCount();
148 | for (int i = 0; i < childCount; i++) {
149 | View child = parent.getChildAt(i);
150 | parent.getDecoratedBoundsWithMargins(child, mBounds);
151 | final int right = mBounds.right + Utils.roundFloat(child.getTranslationX());
152 | final int left = right - mDivider.getIntrinsicWidth();
153 | mDivider.setBounds(left, top, right, bottom);
154 | mDivider.draw(canvas);
155 | // Draw the divider for RecyclerView's horizontal start edge
156 | if (i == 0) {
157 | mDivider.setBounds(parent.getPaddingLeft(), top,
158 | parent.getPaddingLeft() + mDivider.getIntrinsicWidth(), bottom);
159 | mDivider.draw(canvas);
160 | }
161 | }
162 | canvas.restore();
163 | }
164 |
165 | @Override
166 | public void getItemOffsets(
167 | @NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent,
168 | @NonNull RecyclerView.State state) {
169 | if (mDivider == null) {
170 | outRect.set(0, 0, 0, 0);
171 | return;
172 | }
173 | if (mOrientation == VERTICAL) {
174 | final int dividerHeight = mDivider.getIntrinsicHeight();
175 | if (parent.getChildAdapterPosition(view) == 0) {
176 | outRect.set(0, dividerHeight, 0, dividerHeight);
177 | } else {
178 | outRect.set(0, 0, 0, dividerHeight);
179 | }
180 | } else {
181 | final int dividerWidth = mDivider.getIntrinsicWidth();
182 | if (parent.getChildAdapterPosition(view) == 0) {
183 | outRect.set(dividerWidth, 0, dividerWidth, 0);
184 | } else {
185 | outRect.set(0, 0, dividerWidth, 0);
186 | }
187 | }
188 | }
189 | }
--------------------------------------------------------------------------------
/slidingitemmenu-recyclerview/src/main/res/drawable-v21/default_selector_recycler_item.xml:
--------------------------------------------------------------------------------
1 |
2 |