|
18 | 18 | import androidx.core.view.ViewCompat;
|
19 | 19 | import androidx.core.text.TextUtilsCompat;
|
20 | 20 | import android.util.Log;
|
| 21 | +import android.view.FocusFinder; |
21 | 22 | import android.view.MotionEvent;
|
22 | 23 | import android.view.View;
|
23 | 24 | import android.view.ViewConfiguration;
|
|
37 | 38 | import java.util.List;
|
38 | 39 | import java.util.Locale;
|
39 | 40 | import javax.annotation.Nullable;
|
| 41 | +import java.util.ArrayList; |
| 42 | +import java.util.List; |
40 | 43 |
|
41 | 44 | /**
|
42 | 45 | * Similar to {@link ReactScrollView} but only supports horizontal scrolling.
|
@@ -72,6 +75,9 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
|
72 | 75 | private boolean mSnapToStart = true;
|
73 | 76 | private boolean mSnapToEnd = true;
|
74 | 77 | private ReactViewBackgroundManager mReactBackgroundManager;
|
| 78 | + private boolean mPagedArrowScrolling = false; |
| 79 | + |
| 80 | + private final Rect mTempRect = new Rect(); |
75 | 81 |
|
76 | 82 | public ReactHorizontalScrollView(Context context) {
|
77 | 83 | this(context, null);
|
@@ -221,6 +227,82 @@ protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
221 | 227 | scrollTo(getScrollX(), getScrollY());
|
222 | 228 | }
|
223 | 229 |
|
| 230 | + /** |
| 231 | + * Since ReactHorizontalScrollView handles layout changes on JS side, it does not call super.onlayout |
| 232 | + * due to which mIsLayoutDirty flag in HorizontalScrollView remains true and prevents scrolling to child |
| 233 | + * when requestChildFocus is called. |
| 234 | + * Overriding this method and scrolling to child without checking any layout dirty flag. This will fix |
| 235 | + * focus navigation issue for KeyEvents which are not handled in HorizontalScrollView, for example: KEYCODE_TAB. |
| 236 | + */ |
| 237 | + @Override |
| 238 | + public void requestChildFocus(View child, View focused) { |
| 239 | + if (focused != null && !mPagingEnabled) { |
| 240 | + scrollToChild(focused); |
| 241 | + } |
| 242 | + super.requestChildFocus(child, focused); |
| 243 | + } |
| 244 | + |
| 245 | + @Override |
| 246 | + public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { |
| 247 | + if (mPagingEnabled && !mPagedArrowScrolling) { |
| 248 | + // Only add elements within the current page to list of focusables |
| 249 | + ArrayList<View> candidateViews = new ArrayList<View>(); |
| 250 | + super.addFocusables(candidateViews, direction, focusableMode); |
| 251 | + for (View candidate : candidateViews) { |
| 252 | + // We must also include the currently focused in the focusables list or focus search will always |
| 253 | + // return the first element within the focusables list |
| 254 | + if (isScrolledInView(candidate) || isPartiallyScrolledInView(candidate) || candidate.isFocused()) { |
| 255 | + views.add(candidate); |
| 256 | + } |
| 257 | + } |
| 258 | + } else { |
| 259 | + super.addFocusables(views, direction, focusableMode); |
| 260 | + } |
| 261 | + } |
| 262 | + |
| 263 | + /** |
| 264 | + * Calculates the x delta required to scroll the given descendent into view |
| 265 | + */ |
| 266 | + private int getScrollDelta(View descendent) { |
| 267 | + descendent.getDrawingRect(mTempRect); |
| 268 | + offsetDescendantRectToMyCoords(descendent, mTempRect); |
| 269 | + return computeScrollDeltaToGetChildRectOnScreen(mTempRect); |
| 270 | + } |
| 271 | + |
| 272 | + /** |
| 273 | + * Returns whether the given descendent is scrolled fully in view |
| 274 | + */ |
| 275 | + private boolean isScrolledInView(View descendent) { |
| 276 | + return getScrollDelta(descendent) == 0; |
| 277 | + } |
| 278 | + |
| 279 | + |
| 280 | + /** |
| 281 | + * Returns whether the given descendent is partially scrolled in view |
| 282 | + */ |
| 283 | + private boolean isPartiallyScrolledInView(View descendent) { |
| 284 | + int scrollDelta = getScrollDelta(descendent); |
| 285 | + descendent.getDrawingRect(mTempRect); |
| 286 | + return scrollDelta != 0 && Math.abs(scrollDelta) < mTempRect.width(); |
| 287 | + } |
| 288 | + |
| 289 | + /** |
| 290 | + * Returns whether the given descendent is "mostly" (>50%) scrolled in view |
| 291 | + */ |
| 292 | + private boolean isMostlyScrolledInView(View descendent) { |
| 293 | + int scrollDelta = getScrollDelta(descendent); |
| 294 | + descendent.getDrawingRect(mTempRect); |
| 295 | + return scrollDelta != 0 && Math.abs(scrollDelta) < (mTempRect.width() / 2); |
| 296 | + } |
| 297 | + |
| 298 | + private void scrollToChild(View child) { |
| 299 | + int scrollDelta = getScrollDelta(child); |
| 300 | + |
| 301 | + if (scrollDelta != 0) { |
| 302 | + scrollBy(scrollDelta, 0); |
| 303 | + } |
| 304 | + } |
| 305 | + |
224 | 306 | @Override
|
225 | 307 | protected void onScrollChanged(int x, int y, int oldX, int oldY) {
|
226 | 308 | super.onScrollChanged(x, y, oldX, oldY);
|
@@ -263,6 +345,48 @@ public boolean onInterceptTouchEvent(MotionEvent ev) {
|
263 | 345 | return false;
|
264 | 346 | }
|
265 | 347 |
|
| 348 | + @Override |
| 349 | + public boolean pageScroll(int direction) { |
| 350 | + boolean handled = super.pageScroll(direction); |
| 351 | + |
| 352 | + if (mPagingEnabled && handled) { |
| 353 | + handlePostTouchScrolling(0, 0); |
| 354 | + } |
| 355 | + |
| 356 | + return handled; |
| 357 | + } |
| 358 | + |
| 359 | + @Override |
| 360 | + public boolean arrowScroll(int direction) { |
| 361 | + boolean handled = false; |
| 362 | + |
| 363 | + if (mPagingEnabled) { |
| 364 | + mPagedArrowScrolling = true; |
| 365 | + |
| 366 | + if (getChildCount() > 0) { |
| 367 | + View currentFocused = findFocus(); |
| 368 | + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); |
| 369 | + View rootChild = getChildAt(0); |
| 370 | + if (rootChild != null && nextFocused != null && nextFocused.getParent() == rootChild) { |
| 371 | + if (!isScrolledInView(nextFocused) && !isMostlyScrolledInView(nextFocused)) { |
| 372 | + smoothScrollToNextPage(direction); |
| 373 | + } |
| 374 | + nextFocused.requestFocus(); |
| 375 | + handled = true; |
| 376 | + } else { |
| 377 | + smoothScrollToNextPage(direction); |
| 378 | + handled = true; |
| 379 | + } |
| 380 | + } |
| 381 | + |
| 382 | + mPagedArrowScrolling = false; |
| 383 | + } else { |
| 384 | + handled = super.arrowScroll(direction); |
| 385 | + } |
| 386 | + |
| 387 | + return handled; |
| 388 | + } |
| 389 | + |
266 | 390 | @Override
|
267 | 391 | public boolean onTouchEvent(MotionEvent ev) {
|
268 | 392 | if (!mScrollEnabled) {
|
@@ -706,6 +830,29 @@ private void flingAndSnap(int velocityX) {
|
706 | 830 | }
|
707 | 831 | }
|
708 | 832 |
|
| 833 | + private void smoothScrollToNextPage(int direction) { |
| 834 | + int width = getWidth(); |
| 835 | + int currentX = getScrollX(); |
| 836 | + |
| 837 | + int page = currentX / width; |
| 838 | + if (currentX % width != 0) { |
| 839 | + page++; |
| 840 | + } |
| 841 | + |
| 842 | + if (direction == View.FOCUS_LEFT) { |
| 843 | + page = page - 1; |
| 844 | + } else { |
| 845 | + page = page + 1; |
| 846 | + } |
| 847 | + |
| 848 | + if (page < 0) { |
| 849 | + page = 0; |
| 850 | + } |
| 851 | + |
| 852 | + smoothScrollTo(page * width, getScrollY()); |
| 853 | + handlePostTouchScrolling(0, 0); |
| 854 | + } |
| 855 | + |
709 | 856 | @Override
|
710 | 857 | public void setBackgroundColor(int color) {
|
711 | 858 | mReactBackgroundManager.setBackgroundColor(color);
|
|
0 commit comments