forked from mozilla-mobile/android-components
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix mozilla-mobile#9614 - Add a new InputResultDetail
This will serve the following purposes: - wrapper for all the new data from GeckoView's onTouchEventForDetailResult - filters out values not in range (eg: GV's INPUT_RESULT_IGNORED) - controls how the data can be updated and offers clear APIs for querying this data without needing to know about the implementation specifics.
- Loading branch information
Showing
2 changed files
with
795 additions
and
0 deletions.
There are no files selected for viewing
358 changes: 358 additions & 0 deletions
358
...nents/concept/engine/src/main/java/mozilla/components/concept/engine/InputResultDetail.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,358 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
package mozilla.components.concept.engine | ||
|
||
import android.view.MotionEvent | ||
import androidx.annotation.VisibleForTesting | ||
|
||
// The below top-level values are following the same from [org.mozilla.geckoview.PanZoomController] | ||
|
||
/** | ||
* The content has no scrollable element. | ||
* | ||
* @see [InputResultDetail.isTouchUnhandled] | ||
*/ | ||
@VisibleForTesting | ||
internal const val INPUT_UNHANDLED = 0 | ||
|
||
/** | ||
* The touch event is consumed by the [EngineView] | ||
* | ||
* @see [InputResultDetail.isTouchHandledByBrowser] | ||
*/ | ||
@VisibleForTesting | ||
internal const val INPUT_HANDLED = 1 | ||
|
||
/** | ||
* The touch event is consumed by the website through it's own touch listeners. | ||
* | ||
* @see [InputResultDetail.isTouchHandledByWebsite] | ||
*/ | ||
@VisibleForTesting | ||
internal const val INPUT_HANDLED_CONTENT = 2 | ||
|
||
/** | ||
* The website content is not scrollable. | ||
*/ | ||
@VisibleForTesting | ||
internal const val SCROLL_DIRECTIONS_NONE = 0 | ||
|
||
/** | ||
* The website content can be scrolled to the top. | ||
* | ||
* @see [InputResultDetail.canScrollToTop] | ||
*/ | ||
@VisibleForTesting | ||
internal const val SCROLL_DIRECTIONS_TOP = 1 shl 0 | ||
|
||
/** | ||
* The website content can be scrolled to the right. | ||
* | ||
* @see [InputResultDetail.canScrollToRight] | ||
*/ | ||
@VisibleForTesting | ||
internal const val SCROLL_DIRECTIONS_RIGHT = 1 shl 1 | ||
|
||
/** | ||
* The website content can be scrolled to the bottom. | ||
* | ||
* @see [InputResultDetail.canScrollToBottom] | ||
*/ | ||
@VisibleForTesting | ||
internal const val SCROLL_DIRECTIONS_BOTTOM = 1 shl 2 | ||
|
||
/** | ||
* The website content can be scrolled to the left. | ||
* | ||
* @see [InputResultDetail.canScrollToLeft] | ||
*/ | ||
@VisibleForTesting | ||
internal const val SCROLL_DIRECTIONS_LEFT = 1 shl 3 | ||
|
||
/** | ||
* The website content cannot be overscrolled. | ||
*/ | ||
@VisibleForTesting | ||
internal const val OVERSCROLL_DIRECTIONS_NONE = 0 | ||
|
||
/** | ||
* The website content can be overscrolled horizontally. | ||
* | ||
* @see [InputResultDetail.canOverscrollRight] | ||
* @see [InputResultDetail.canOverscrollLeft] | ||
*/ | ||
@VisibleForTesting | ||
internal const val OVERSCROLL_DIRECTIONS_HORIZONTAL = 1 shl 0 | ||
|
||
/** | ||
* The website content can be overscrolled vertically. | ||
* | ||
* @see [InputResultDetail.canOverscrollTop] | ||
* @see [InputResultDetail.canOverscrollBottom] | ||
*/ | ||
@VisibleForTesting | ||
internal const val OVERSCROLL_DIRECTIONS_VERTICAL = 1 shl 1 | ||
|
||
/** | ||
* All data about how a touch will be handled by the browser. | ||
* - whether the event is used for panning/zooming by the browser / by the website or will be ignored. | ||
* - whether the event can scroll the page and in what direction. | ||
* - whether the event can overscroll the page and in what direction. | ||
* | ||
* @param inputResult Indicates who will use the current [MotionEvent]. | ||
* Possible values: [[INPUT_UNHANDLED], [INPUT_HANDLED], [INPUT_HANDLED_CONTENT]]<br>. | ||
* | ||
* @param scrollDirections Bitwise ORed value of the directions the page can be scrolled to. | ||
* This is the same as GeckoView's [org.mozilla.geckoview.PanZoomController.ScrollableDirections]. | ||
* | ||
* @param overscrollDirections Bitwise ORed value of the directions the page can be overscrolled to. | ||
* This is the same as GeckoView's [org.mozilla.geckoview.PanZoomController.OverscrollDirections]. | ||
*/ | ||
@Suppress("TooManyFunctions") | ||
class InputResultDetail private constructor( | ||
val inputResult: Int = INPUT_UNHANDLED, | ||
val scrollDirections: Int = SCROLL_DIRECTIONS_NONE, | ||
val overscrollDirections: Int = OVERSCROLL_DIRECTIONS_NONE | ||
) { | ||
|
||
override fun equals(other: Any?): Boolean { | ||
return if (this !== other) { | ||
if (other is InputResultDetail) { | ||
return inputResult == other.inputResult && | ||
scrollDirections == other.scrollDirections && | ||
overscrollDirections == other.overscrollDirections | ||
} else { | ||
false | ||
} | ||
} else { | ||
true | ||
} | ||
} | ||
|
||
@Suppress("MagicNumber") | ||
override fun hashCode(): Int { | ||
var hash = inputResult.hashCode() * 31 | ||
hash += (scrollDirections.hashCode()) * 31 | ||
hash += (overscrollDirections.hashCode()) * 31 | ||
|
||
return hash | ||
} | ||
|
||
override fun toString(): String { | ||
return StringBuilder("InputResultDetail \$${hashCode()} (") | ||
.append("Input ${getInputResultHandledDescription()}. ") | ||
.append("Content ${getScrollDirectionsDescription()} and ${getOverscrollDirectionsDescription()}") | ||
.append(')') | ||
.toString() | ||
} | ||
|
||
/** | ||
* Create a new instance of [InputResultDetail] with the option of keep some of the current values. | ||
* | ||
* The provided new values will be filtered out if not recognized and could corrupt the current state. | ||
*/ | ||
fun copy( | ||
inputResult: Int? = this.inputResult, | ||
scrollDirections: Int? = this.scrollDirections, | ||
overscrollDirections: Int? = this.overscrollDirections | ||
): InputResultDetail { | ||
// Ensure this data will not get corrupted by users sending unknown arguments | ||
|
||
val newValidInputResult = if (inputResult in INPUT_UNHANDLED..INPUT_HANDLED_CONTENT) { | ||
inputResult | ||
} else { | ||
this.inputResult | ||
} | ||
val newValidScrollDirections = if (scrollDirections in | ||
SCROLL_DIRECTIONS_NONE..(SCROLL_DIRECTIONS_LEFT or (SCROLL_DIRECTIONS_LEFT - 1)) | ||
) { | ||
scrollDirections | ||
} else { | ||
this.scrollDirections | ||
} | ||
val newValidOverscrollDirections = if (overscrollDirections in | ||
OVERSCROLL_DIRECTIONS_NONE..(OVERSCROLL_DIRECTIONS_VERTICAL or (OVERSCROLL_DIRECTIONS_VERTICAL - 1)) | ||
) { | ||
overscrollDirections | ||
} else { | ||
this.overscrollDirections | ||
} | ||
|
||
// The range check automatically checks for null but doesn't yet have a contract to say so. | ||
// As such it it safe to use the not-null assertion operator. | ||
return InputResultDetail(newValidInputResult!!, newValidScrollDirections!!, newValidOverscrollDirections!!) | ||
} | ||
|
||
/** | ||
* The [EngineView] handled the last [MotionEvent] to pan or zoom the content. | ||
*/ | ||
fun isTouchHandledByBrowser() = inputResult == INPUT_HANDLED | ||
|
||
/** | ||
* The website handled the last [MotionEvent] through it's own touch listeners | ||
* and consumed it without the [EngineView] panning or zooming the website | ||
*/ | ||
fun isTouchHandledByWebsite() = inputResult == INPUT_HANDLED_CONTENT | ||
|
||
/** | ||
* Neither the [EngineView], nor the website will handle this [MotionEvent]. | ||
* | ||
* This might happen on a website without touch listeners that is not bigger than the screen | ||
* or when the content has no scrollable element. | ||
*/ | ||
fun isTouchUnhandled() = inputResult == INPUT_UNHANDLED | ||
|
||
/** | ||
* Whether the width of the webpage exceeds the display and the webpage can be scrolled to left. | ||
*/ | ||
fun canScrollToLeft(): Boolean = | ||
inputResult == INPUT_HANDLED && | ||
scrollDirections and SCROLL_DIRECTIONS_LEFT != 0 | ||
|
||
/** | ||
* Whether the height of the webpage exceeds the display and the webpage can be scrolled to top. | ||
*/ | ||
fun canScrollToTop(): Boolean = | ||
inputResult == INPUT_HANDLED && | ||
scrollDirections and SCROLL_DIRECTIONS_TOP != 0 | ||
|
||
/** | ||
* Whether the width of the webpage exceeds the display and the webpage can be scrolled to right. | ||
*/ | ||
fun canScrollToRight(): Boolean = | ||
inputResult == INPUT_HANDLED && | ||
scrollDirections and SCROLL_DIRECTIONS_RIGHT != 0 | ||
|
||
/** | ||
* Whether the height of the webpage exceeds the display and the webpage can be scrolled to bottom. | ||
*/ | ||
fun canScrollToBottom(): Boolean = | ||
inputResult == INPUT_HANDLED && | ||
scrollDirections and SCROLL_DIRECTIONS_BOTTOM != 0 | ||
|
||
/** | ||
* Whether the webpage can be overscrolled to the left. | ||
* | ||
* @return `true` if the page is already scrolled to the left most part | ||
* and the touch event is not handled by the webpage. | ||
*/ | ||
fun canOverscrollLeft(): Boolean = | ||
inputResult != INPUT_HANDLED_CONTENT && | ||
(scrollDirections and SCROLL_DIRECTIONS_LEFT == 0) && | ||
(overscrollDirections and OVERSCROLL_DIRECTIONS_HORIZONTAL != 0) | ||
|
||
/** | ||
* Whether the webpage can be overscrolled to the top. | ||
* | ||
* @return `true` if the page is already scrolled to the top most part | ||
* and the touch event is not handled by the webpage. | ||
*/ | ||
fun canOverscrollTop(): Boolean = | ||
inputResult != INPUT_HANDLED_CONTENT && | ||
(scrollDirections and SCROLL_DIRECTIONS_TOP == 0) && | ||
(overscrollDirections and OVERSCROLL_DIRECTIONS_VERTICAL != 0) | ||
|
||
/** | ||
* Whether the webpage can be overscrolled to the right. | ||
* | ||
* @return `true` if the page is already scrolled to the right most part | ||
* and the touch event is not handled by the webpage. | ||
*/ | ||
fun canOverscrollRight(): Boolean = | ||
inputResult != INPUT_HANDLED_CONTENT && | ||
(scrollDirections and SCROLL_DIRECTIONS_RIGHT == 0) && | ||
(overscrollDirections and OVERSCROLL_DIRECTIONS_HORIZONTAL != 0) | ||
|
||
/** | ||
* Whether the webpage can be overscrolled to the bottom. | ||
* | ||
* @return `true` if the page is already scrolled to the bottom most part | ||
* and the touch event is not handled by the webpage. | ||
*/ | ||
fun canOverscrollBottom(): Boolean = | ||
inputResult != INPUT_HANDLED_CONTENT && | ||
(scrollDirections and SCROLL_DIRECTIONS_BOTTOM == 0) && | ||
(overscrollDirections and OVERSCROLL_DIRECTIONS_VERTICAL != 0) | ||
|
||
@VisibleForTesting | ||
internal fun getInputResultHandledDescription() = when (inputResult) { | ||
INPUT_HANDLED -> INPUT_HANDLED_TOSTRING_DESCRIPTION | ||
INPUT_HANDLED_CONTENT -> INPUT_HANDLED_CONTENT_TOSTRING_DESCRIPTION | ||
else -> INPUT_UNHANDLED_TOSTRING_DESCRIPTION | ||
} | ||
|
||
@VisibleForTesting | ||
internal fun getScrollDirectionsDescription(): String { | ||
if (scrollDirections == SCROLL_DIRECTIONS_NONE) { | ||
return SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION | ||
} | ||
|
||
val scrollDirections = StringBuilder() | ||
.append(if (canScrollToLeft()) "$SCROLL_LEFT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "") | ||
.append(if (canScrollToTop()) "$SCROLL_TOP_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "") | ||
.append(if (canScrollToRight()) "$SCROLL_RIGHT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "") | ||
.append(if (canScrollToBottom()) SCROLL_BOTTOM_TOSTRING_DESCRIPTION else "") | ||
.removeSuffix(TOSTRING_SEPARATOR) | ||
.toString() | ||
|
||
return if (scrollDirections.trim().isEmpty()) { | ||
SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION | ||
} else { | ||
SCROLL_TOSTRING_DESCRIPTION + scrollDirections | ||
} | ||
} | ||
|
||
@VisibleForTesting | ||
internal fun getOverscrollDirectionsDescription(): String { | ||
if (overscrollDirections == OVERSCROLL_DIRECTIONS_NONE) { | ||
return OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION | ||
} | ||
|
||
val overscrollDirections = StringBuilder() | ||
.append(if (canOverscrollLeft()) "$SCROLL_LEFT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "") | ||
.append(if (canOverscrollTop()) "$SCROLL_TOP_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "") | ||
.append(if (canOverscrollRight()) "$SCROLL_RIGHT_TOSTRING_DESCRIPTION$TOSTRING_SEPARATOR" else "") | ||
.append(if (canOverscrollBottom()) SCROLL_BOTTOM_TOSTRING_DESCRIPTION else "") | ||
.removeSuffix(TOSTRING_SEPARATOR) | ||
.toString() | ||
|
||
return if (overscrollDirections.trim().isEmpty()) { | ||
OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION | ||
} else { | ||
OVERSCROLL_TOSTRING_DESCRIPTION + overscrollDirections | ||
} | ||
} | ||
|
||
companion object { | ||
/** | ||
* Create a new instance of [InputResultDetail]. | ||
* | ||
* @param verticalOverscrollInitiallyEnabled optional parameter for enabling pull to refresh | ||
* in the cases in which this class can be used before valid values being set and it helps more to have | ||
* overscroll vertically allowed and then stop depending on the values with which this class is updated | ||
* rather than start with a disabled overscroll functionality for the current gesture. | ||
*/ | ||
fun newInstance(verticalOverscrollInitiallyEnabled: Boolean = false) = InputResultDetail( | ||
overscrollDirections = if (verticalOverscrollInitiallyEnabled) { | ||
OVERSCROLL_DIRECTIONS_VERTICAL | ||
} else { | ||
OVERSCROLL_DIRECTIONS_NONE | ||
} | ||
) | ||
|
||
@VisibleForTesting internal const val TOSTRING_SEPARATOR = ", " | ||
@VisibleForTesting internal const val INPUT_HANDLED_TOSTRING_DESCRIPTION = "handled by the browser" | ||
@VisibleForTesting internal const val INPUT_HANDLED_CONTENT_TOSTRING_DESCRIPTION = "handled by the website" | ||
@VisibleForTesting internal const val INPUT_UNHANDLED_TOSTRING_DESCRIPTION = "unhandled" | ||
@VisibleForTesting internal const val SCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION = "cannot be scrolled" | ||
@VisibleForTesting internal const val OVERSCROLL_IMPOSSIBLE_TOSTRING_DESCRIPTION = "cannot be overscrolled" | ||
@VisibleForTesting internal const val SCROLL_TOSTRING_DESCRIPTION = "can be scrolled to " | ||
@VisibleForTesting internal const val OVERSCROLL_TOSTRING_DESCRIPTION = "can be overscrolled to " | ||
@VisibleForTesting internal const val SCROLL_LEFT_TOSTRING_DESCRIPTION = "left" | ||
@VisibleForTesting internal const val SCROLL_TOP_TOSTRING_DESCRIPTION = "top" | ||
@VisibleForTesting internal const val SCROLL_RIGHT_TOSTRING_DESCRIPTION = "right" | ||
@VisibleForTesting internal const val SCROLL_BOTTOM_TOSTRING_DESCRIPTION = "bottom" | ||
} | ||
} |
Oops, something went wrong.