[go: nahoru, domu]

blob: b38e6b483292e0e62b4cbb3f8dc4ee7a16f20e28 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.ui.core.gesture
import androidx.compose.remember
import androidx.ui.core.Modifier
import androidx.ui.core.PointerEventPass
import androidx.ui.core.PointerInputChange
import androidx.ui.core.changedToDown
import androidx.ui.core.changedToUp
import androidx.ui.core.composed
import androidx.ui.core.consumeDownChange
import androidx.ui.core.pointerinput.PointerInputFilter
import androidx.ui.testutils.consume
import androidx.compose.ui.unit.IntSize
/**
* Observes various events sent by [rawScaleGestureFilter]. Implement and pass into
* [rawScaleGestureFilter] so that [rawScaleGestureFilter] may call the functions when events
* occur.
*/
interface RawScaleObserver {
/**
* Override to be notified when scaling has started.
*
* This will be called when scaling occurs (and when the associated [rawScaleGestureFilter]
* is allowed to start). Always called just before [onScale] and isn't called again until
* after [onStop].
*
* @see rawScaleGestureFilter
* @see onScale
* @see onStop
*/
fun onStart() {}
/**
* Override to be notified when scaling has occurred.
*
* When overridden, return the amount of scaling, expressed as a scale factor, that should be
* consumed. For example, if the [scaleFactor] is 1.5 and the client wants to consume all of
* the scaling, it should return 1.5. If it wants to consume none of the scaling, it should
* return 1.
*
* Always called just after [onStart] (and for every subsequent scale).
*
* @param scaleFactor The ratio of newSize / oldSize that the scaling gesture has expressed
* between pointers last position and current position (this value is not cumulative over the
* lifetime of of the gesture). For example, if 2 fingers are 10 pixel apart, and then move
* such that they are 20 pixels apart, the scaleFactor will be 2. If 2 fingers that are 20
* pixels apart move such that they are 10 pixels apart, the scaleFactor will be .5.
*
* @return The amount scaling that was actually used. This value should also be a scaleFactor
* that is in the range of S to 1, where S is the value of [scaleFactor]. (If you are
* scaling an image, just return the scaleFactor that the image actually scaled. For
* example, if [scaleFactor] is 2, and the image can only scale to 1.5, return 1.5). If you
* don't want bother with "nested scaling" (you simply want to consume all of the pointer
* movement related to scaling), just return the value of [scaleFactor].
*/
fun onScale(scaleFactor: Float) = 1f
/**
* Override to be notified when scaling has stopped.
*
* This is called once no pointers remain.
*
* Only called after [onStart] and one or more calls to [onScale]
*/
fun onStop() {}
/**
* Override to be notified when the scale has been cancelled.
*
* This is called if [onStart] has ben called and then a cancellation event has occurs
* (for example, due to the gesture detector being removed from the tree) before [onStop] is
* called.
*/
fun onCancel() {}
}
/**
* This gesture detector detects scaling.
*
* Scaling is when the average distance between a set of pointers changes over time. It is
* also known as pinch, or pinch to zoom.
*
* Note: By default, this gesture detector will start as soon as the average distance between
* pointers changes by just a little bit. It is likely that you don't want to use this gesture
* detector directly, but instead use a scale gesture detector that is less aggressive about
* starting (such as [rawScaleGestureFilter] which waits for a pointer to have passed touch slop
* before starting).
*
* Scaling begins when the average distance between a set of pointers changes and either
* [canStartScaling] is null or returns true. When scaling begins, [RawScaleObserver.onStart] is
* called followed immediately by a call to [RawScaleObserver.onScale].
* [RawScaleObserver.onScale] is then continuously called whenever the movement of all pointers
* denotes scaling. The gesture stops with either a call to [RawScaleObserver.onStop] or
* [RawScaleObserver.onCancel], either of which will only be called if [RawScaleObserver.onStart]
* was previously called. [RawScaleObserver.onStop] is called when no pointers remain.
* [RawScaleObserver.onCancel] is called due to a system cancellation event.
*
* @param scaleObserver The callback interface to report all events related to scaling.
* @param canStartScaling If set, before scaling is started ([RawScaleObserver.onStart] is called),
* canStartScaling is called for each pointer event to check to see if it is allowed to start.
*/
fun Modifier.rawScaleGestureFilter(
scaleObserver: RawScaleObserver,
canStartScaling: (() -> Boolean)? = null
): Modifier = composed {
val filter = remember { RawScaleGestureFilter() }
// TODO(b/129784010): Consider also allowing onStart, onScale, and onEnd to be set individually.
filter.scaleObserver = scaleObserver
filter.canStartScaling = canStartScaling
PointerInputModifierImpl(filter)
}
internal class RawScaleGestureFilter : PointerInputFilter() {
private var active = false
lateinit var scaleObserver: RawScaleObserver
var canStartScaling: (() -> Boolean)? = null
override fun onPointerInput(
changes: List<PointerInputChange>,
pass: PointerEventPass,
bounds: IntSize
): List<PointerInputChange> {
var changesToReturn = changes
if (pass == PointerEventPass.InitialDown && active) {
// If we are currently scaling, we want to prevent any children from reacting to any
// down change.
changesToReturn = changesToReturn.map {
if (it.changedToDown() || it.changedToUp()) {
it.consumeDownChange()
} else {
it
}
}
}
if (pass == PointerEventPass.PostUp) {
var (currentlyDownChanges, otherChanges) = changesToReturn.partition {
it.current.down && it.previous.down
}
val scaleObserver = scaleObserver
if (currentlyDownChanges.isEmpty()) {
if (active) {
active = false
scaleObserver.onStop()
}
} else {
val dimensionInformation =
currentlyDownChanges.calculateAllDimensionInformation()
val scalePercentage = dimensionInformation.calculateScaleFactor()
// If all of the pointers somehow report the same number, scalePercentage could
// end up as NaN (because the average distance that each pointer is to the
// center of them all will be 0, which means that the scalePercentage should be
// infinite, which clearly isn't correct).
if (!scalePercentage.isNaN() && scalePercentage != 1f) {
if (!active && canStartScaling?.invoke() != false) {
active = true
scaleObserver.onStart()
}
if (active) {
val scalePercentageUsed = scaleObserver.onScale(scalePercentage)
val percentageOfChangeUsed =
(scalePercentageUsed - 1) / (scalePercentage - 1)
if (percentageOfChangeUsed > 0f) {
val newCurrentlyDownChanges = mutableListOf<PointerInputChange>()
for (i in currentlyDownChanges.indices) {
val xVectorToAverageChange =
getVectorToAverageChange(
dimensionInformation.previousX,
dimensionInformation.currentX,
i
) * percentageOfChangeUsed
val yVectorToAverageChange =
getVectorToAverageChange(
dimensionInformation.previousY,
dimensionInformation.currentY,
i
) * percentageOfChangeUsed
newCurrentlyDownChanges
.add(
currentlyDownChanges[i].consume(
xVectorToAverageChange,
yVectorToAverageChange
)
)
}
currentlyDownChanges = newCurrentlyDownChanges
}
}
}
}
changesToReturn = currentlyDownChanges + otherChanges
}
return changesToReturn
}
override fun onCancel() {
if (active) {
scaleObserver.onCancel()
active = false
}
}
}