I find listeners for onClick and onLongClick and even onPress but there is no event/listener for something like buttonDown and buttonUp, or onPress and onRelease.
Am I missing something? My current use case is that when a user presses a button I increment a count and when the user releases it I decrease the count. But in general I want something to start happening as soon as the user presses the button and stop when the user releases it. (For a real life example, see how Facebook Messenger records a video, you keep the button pressed to start and it stops when you release it.
I am using Jetpack Compose on Android.
You can use the InteractionSource.collectIsPressedAsState to know if the Button is pressed.
You can add a side effect to know when the Button is released.
Something like:
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
var currentStateTxt by remember { mutableStateOf("Not Pressed") }
var currentCount by remember { mutableStateOf(0) }
if (isPressed){
//Pressed
currentStateTxt = "Pressed"
currentCount += 1
//Use if + DisposableEffect to wait for the press action is completed
DisposableEffect(Unit) {
onDispose {
//released
currentStateTxt = "Released"
}
}
}
Button(onClick={},
interactionSource = interactionSource
){
Text("Current state = $currentStateTxt")
Text("Count = $currentCount")
}
Use .pointerInput modifier:
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitFirstDown()
//onPress actions here
do {
val event = awaitPointerEvent()
//Track other pointer evenst, like Drag etc...
} while (event.changes.any { it.pressed })
//onRelease actions here
}
}
}
these codes may be helpful for you
var isPressed by remember {
mutableStateOf(false)
}
.pointerInput(Unit) {
detectTapGestures(
onPress = {
try {
isPressed = true
isPlaying = true
sampleSong.start()
awaitRelease()
}
finally {
isPressed = false
isPlaying = false
sampleSong.pause()
}
I think you use Touch Listener on button , its easily detect button touch or untouch example
override fun onTouchEvent(e: MotionEvent): Boolean {
val x: Float = e.x
val y: Float = e.y
when (e.action) {
MotionEvent.ACTION_MOVE -> {
var dx: Float = x - previousX
var dy: Float = y - previousY
// reverse direction of rotation above the mid-line
if (y > height / 2) {
dx *= -1
}
// reverse direction of rotation to left of the mid-line
if (x < width / 2) {
dy *= -1
}
renderer.angle += (dx + dy) * TOUCH_SCALE_FACTOR
requestRender()
}
}
previousX = x
previousY = y
return true
}
More info of Touch Listener in this link Android Touch Listener
Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = {
//start
val released = try {
tryAwaitRelease()
} catch (c: CancellationException) {
false
}
if (released) {
//ACTION_UP
} else {
//CANCELED
}
},
onTap = {
// onTap
},
onDoubleTap = {
//onDoubleTap
},
onLongPress = {
//onLongPress
}
)
}
Related
I want to rotate my view by dragging a helper button on the edge of the view.
Here Image Here is video
In this case, when the view is standing still, only the change on event.Y should be affected. As the view becomes horizontal, the event.X change should increase and only the event.X change should affect the view when the view is fully horizontal
Can anyone know how to do this?
current code is, but this worked incorrectly:
emojiBinding.btnRotate.setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
boolArray[0] = false
boolArray[2] = false
startPositionOfRotation.x = event.x
startPositionOfRotation.y = event.y
}
MotionEvent.ACTION_MOVE -> {
val degree = getRotationDegree(emojiBinding, event.x, event.y)
val rotState = emojiBinding.root.rotation
var mult = 0f
if (abs(rotState)%90 < 45 ){
mult = if (event.x >=0)1f else -1f
}
else{
mult = if (event.y >=0)1f else -1f
}
emojiBinding.root.rotation += mult * degree
Timber.d("Degree $degree Rotation ${emojiBinding.root.rotation}")
}
MotionEvent.ACTION_UP -> {
startPositionOfRotation.x = 0f
startPositionOfRotation.y = 0f
boolArray[0] = true
boolArray[2] = true
}
}
boolArray[1]
}
I am able to detect transform gestures just fine using Modifier.detectTransformGesture(), as per the below simplified example:
Box(
Modifier
.pointerInput(Unit) {
detectTransformGestures(
onGesture = { _, pan, gestureZoom, gestureRotate ->
// do something
}
)
}
)
But I want to know when the gesture has been completed by the user, so that I can perform some more (computationally intensive) operations.
I couldn't find any leads for that. I tried with Modifier.transformable and TransformableState which does have a property called isTransformInProgress, but I couldn't figure out how to access it in the callback:
val state = rememberTransformableState {
// How do I access state.isTransformInProgress ?
}
// I can access it here
Text(if(state.isTransformInProgress) "transforming" else "not transforming")
If you need to check the end of transform when using Modifier.transformable, you can access isTransformInProgress using LaunchedEffect:
val state = rememberTransformableState {
}
LaunchedEffect(state.isTransformInProgress) {
if (!state.isTransformInProgress) {
// do what you need
}
}
I can think of two variants, both came after investigating the source code:
Subscribe to events in parallel the same way detectTransformGestures does:
fun Modifier.pointerInputDetectTransformGestures(
panZoomLock: Boolean = false,
isTransformInProgressChanged: (Boolean) -> Unit,
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
): Modifier {
return pointerInput(Unit) {
detectTransformGestures(
panZoomLock = panZoomLock,
onGesture = { offset, pan, gestureZoom, gestureRotate ->
isTransformInProgressChanged(true)
onGesture(offset, pan, gestureZoom, gestureRotate)
}
)
}
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitFirstDown(requireUnconsumed = false)
do {
val event = awaitPointerEvent()
val canceled = event.changes.any { it.consumed.positionChange }
} while (!canceled && event.changes.any { it.pressed })
isTransformInProgressChanged(false)
}
}
}
}
Copy implementation of pointerInputDetectTransformGestures(it has no internal references), and add needed logic there, like this:
suspend fun PointerInputScope.detectTransformGestures(
panZoomLock: Boolean = false,
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit,
isTransformInProgressChanged: (Boolean) -> Unit,
) {
forEachGesture {
awaitPointerEventScope {
var rotation = 0f
var zoom = 1f
var pan = Offset.Zero
var pastTouchSlop = false
val touchSlop = viewConfiguration.touchSlop
var lockedToPanZoom = false
var startGestureNotified = false // added
awaitFirstDown(requireUnconsumed = false)
do {
val event = awaitPointerEvent()
val canceled = event.changes.fastAny { it.positionChangeConsumed() }
if (!canceled) {
val zoomChange = event.calculateZoom()
val rotationChange = event.calculateRotation()
val panChange = event.calculatePan()
if (!pastTouchSlop) {
zoom *= zoomChange
rotation += rotationChange
pan += panChange
val centroidSize = event.calculateCentroidSize(useCurrent = false)
val zoomMotion = abs(1 - zoom) * centroidSize
val rotationMotion = abs(rotation * PI.toFloat() * centroidSize / 180f)
val panMotion = pan.getDistance()
if (zoomMotion > touchSlop ||
rotationMotion > touchSlop ||
panMotion > touchSlop
) {
pastTouchSlop = true
lockedToPanZoom = panZoomLock && rotationMotion < touchSlop
}
}
if (pastTouchSlop) {
val centroid = event.calculateCentroid(useCurrent = false)
val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
if (effectiveRotation != 0f ||
zoomChange != 1f ||
panChange != Offset.Zero
) {
onGesture(centroid, panChange, zoomChange, effectiveRotation)
if (!startGestureNotified) { // notify first gesture sent
isTransformInProgressChanged(true)
startGestureNotified = true
}
}
event.changes.fastForEach {
if (it.positionChanged()) {
it.consumeAllChanges()
}
}
}
}
} while (!canceled && event.changes.fastAny { it.pressed })
isTransformInProgressChanged(false) // notify last finger is up
}
}
}
In any case you would have to check out of any changes in new versions of compose to update your code(have no idea how likely it'll be).
With the second method at least you can be sure it won't just broke, because you're not depending on their implementation.
You can use this to detect when user starts transform gesture, when it ends and have option to consume transform events to enable gesture propagation to scroll or other drag events.
/**
* A gesture detector for rotation, panning, and zoom. Once touch slop has been reached, the
* user can use rotation, panning and zoom gestures. [onGesture] will be called when any of the
* rotation, zoom or pan occurs, passing the rotation angle in degrees, zoom in scale factor and
* pan as an offset in pixels. Each of these changes is a difference between the previous call
* and the current gesture. This will consume all position changes after touch slop has
* been reached. [onGesture] will also provide centroid of all the pointers that are down.
*
* After gesture started when last pointer is up [onGestureEnd] is triggered.
*
* #param consume flag consume [PointerInputChange]s this gesture uses. Consuming
* returns [PointerInputChange.isConsumed] true which is observed by other gestures such
* as drag, scroll and transform. When this flag is true other gesture don't receive events
* #param onGestureStart callback for notifying transform gesture has started with initial
* pointer
* #param onGesture callback for passing centroid, pan, zoom, rotation and main pointer and
* pointer size to caller. Main pointer is the one that touches screen first. If it's lifted
* next one that is down is the main pointer.
* #param onGestureEnd callback that notifies last pointer is up and gesture is ended
*
*/
suspend fun PointerInputScope.detectTransformGestures(
panZoomLock: Boolean = false,
consume: Boolean = true,
onGestureStart: (PointerInputChange) -> Unit = {},
onGesture: (
centroid: Offset,
pan: Offset,
zoom: Float,
rotation: Float,
mainPointer: PointerInputChange,
changes: List<PointerInputChange>
) -> Unit,
onGestureEnd: (PointerInputChange) -> Unit = {}
) {
forEachGesture {
awaitPointerEventScope {
var rotation = 0f
var zoom = 1f
var pan = Offset.Zero
var pastTouchSlop = false
val touchSlop = viewConfiguration.touchSlop
var lockedToPanZoom = false
// Wait for at least one pointer to press down, and set first contact position
val down: PointerInputChange = awaitFirstDown(requireUnconsumed = false)
onGestureStart(down)
var pointer = down
// Main pointer is the one that is down initially
var pointerId = down.id
do {
val event = awaitPointerEvent()
// If any position change is consumed from another PointerInputChange
val canceled =
event.changes.any { it.isConsumed }
if (!canceled) {
// Get pointer that is down, if first pointer is up
// get another and use it if other pointers are also down
// event.changes.first() doesn't return same order
val pointerInputChange =
event.changes.firstOrNull { it.id == pointerId }
?: event.changes.first()
// Next time will check same pointer with this id
pointerId = pointerInputChange.id
pointer = pointerInputChange
val zoomChange = event.calculateZoom()
val rotationChange = event.calculateRotation()
val panChange = event.calculatePan()
if (!pastTouchSlop) {
zoom *= zoomChange
rotation += rotationChange
pan += panChange
val centroidSize = event.calculateCentroidSize(useCurrent = false)
val zoomMotion = abs(1 - zoom) * centroidSize
val rotationMotion =
abs(rotation * PI.toFloat() * centroidSize / 180f)
val panMotion = pan.getDistance()
if (zoomMotion > touchSlop ||
rotationMotion > touchSlop ||
panMotion > touchSlop
) {
pastTouchSlop = true
lockedToPanZoom = panZoomLock && rotationMotion < touchSlop
}
}
if (pastTouchSlop) {
val centroid = event.calculateCentroid(useCurrent = false)
val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
if (effectiveRotation != 0f ||
zoomChange != 1f ||
panChange != Offset.Zero
) {
onGesture(
centroid,
panChange,
zoomChange,
effectiveRotation,
pointer,
event.changes
)
}
if (consume) {
event.changes.forEach {
if (it.positionChanged()) {
it.consume()
}
}
}
}
}
} while (!canceled && event.changes.any { it.pressed })
onGestureEnd(pointer)
}
}
}
also it let's you to get mainPointer, first pointer initially then the one that is returned from pointer list with id, and all pointers that are down
which let's you to detect how many fingers user invoking gesture with
Modifier.pointerInput(Unit) {
detectTransformGestures(
onGestureStart = {
transformDetailText = "GESTURE START"
},
onGesture = { gestureCentroid: Offset,
gesturePan: Offset,
gestureZoom: Float,
gestureRotate: Float,
mainPointerInputChange: PointerInputChange,
pointerList: List<PointerInputChange> ->
},
onGestureEnd = {
borderColor = Color.LightGray
transformDetailText = "GESTURE END"
}
)
}
Full library code that has onEvent counterpart, touch delegate and demo is available in github repository.
In normal view, we can have onTouchEvent
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {}
MotionEvent.ACTION_MOVE -> {}
MotionEvent.ACTION_UP -> {}
else -> return false
}
invalidate()
return true
}
In Jetpack Compose, I can only find we have the tapGestureFilter in the modifier, which only takes the action from the ACTION_UP only.
Modifier
.tapGestureFilter { Log.d("Track", "Tap ${it.x} | ${it.y}") }
.doubleTapGestureFilter { Log.d("Track", "DoubleTap ${it.x} | ${it.y}") }
Is there an equivalent onTouchEvent for Jetpack Compose?
We have a separate package for that, which is pretty useful.
There are two main extension functions that would be suitable for you:
pointerInput - docs
pointerInteropFilter - docs
If you want to handle and process the event I recommend using pointerInteropFilter which is the analogue of View.onTouchEvent. It's used along with modifier:
Column(modifier = Modifier.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {}
MotionEvent.ACTION_MOVE -> {}
MotionEvent.ACTION_UP -> {}
else -> false
}
true
})
That will be Compose adjusted code to your specified View.onTouchEvent sample.
P.S. Don't forget about #ExperimentalPointerInput annotation.
pointerInteropFilter is not described as preferred way to use if you are not using touch api with interoperation with existing View code.
A special PointerInputModifier that provides access to the underlying
MotionEvents originally dispatched to Compose. Prefer pointerInput and
use this only for interoperation with existing code that consumes
MotionEvents. While the main intent of this Modifier is to allow
arbitrary code to access the original MotionEvent dispatched to
Compose, for completeness, analogs are provided to allow arbitrary
code to interact with the system as if it were an Android View.
You can use pointerInput , awaitTouchDown for MotionEvent.ACTION_DOWN, and awaitPointerEvent for MotionEvent.ACTION_MOVE and MotionEvent.ACTION_UP
val pointerModifier = Modifier
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitFirstDown()
// ACTION_DOWN here
do {
//This PointerEvent contains details including
// event, id, position and more
val event: PointerEvent = awaitPointerEvent()
// ACTION_MOVE loop
// Consuming event prevents other gestures or scroll to intercept
event.changes.forEach { pointerInputChange: PointerInputChange ->
pointerInputChange.consumePositionChange()
}
} while (event.changes.any { it.pressed })
// ACTION_UP is here
}
}
}
Some key notes about gestures
pointerInput propagation is when you have more than one goes from
bottom one to top one.
If children and parent are listening for input changes it propagates
from inner children to outer then parent. Unlike touch events from
parent to children
If you don't consume events other events like scroll drag can
interfere or consume events, most of the events check if it's
consumed before propagating to them
detectDragGestures source code for instance
val down = awaitFirstDown(requireUnconsumed = false)
var drag: PointerInputChange?
var overSlop = Offset.Zero
do {
drag = awaitPointerSlopOrCancellation(
down.id,
down.type
) { change, over ->
change.consumePositionChange()
overSlop = over
}
} while (drag != null && !drag.positionChangeConsumed())
So when you need to prevent other events to intercept
call pointerInputChange.consumeDown() after awaitFirstDown, call
pointerInputChange.consumePositionChange() after awaitPointerEvent
and awaitFirstDown() has requireUnconsumed parameter which is
true by default. If you set it to false even if a pointerInput consumes
down before your gesture you still get it. This is also how events like drag use it to get first down no matter what.
Every available event you see detectDragGestures,
detectTapGestures even awaitFirstDown use awaitPointerEvent
for implementation, so using awaitFirstDown, awaitPointerEvent
and consuming changes you can configure your own gestures.
For instance, this is a function i customized from original detectTransformGestures only to be invoked with specific number of pointers down.
suspend fun PointerInputScope.detectMultiplePointerTransformGestures(
panZoomLock: Boolean = false,
numberOfPointersRequired: Int = 2,
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit,
) {
forEachGesture {
awaitPointerEventScope {
var rotation = 0f
var zoom = 1f
var pan = Offset.Zero
var pastTouchSlop = false
val touchSlop = viewConfiguration.touchSlop
var lockedToPanZoom = false
awaitFirstDown(requireUnconsumed = false)
do {
val event = awaitPointerEvent()
val downPointerCount = event.changes.size
// If any position change is consumed from another pointer or pointer
// count that is pressed is not equal to pointerCount cancel this gesture
val canceled = event.changes.any { it.positionChangeConsumed() } || (
downPointerCount != numberOfPointersRequired)
if (!canceled) {
val zoomChange = event.calculateZoom()
val rotationChange = event.calculateRotation()
val panChange = event.calculatePan()
if (!pastTouchSlop) {
zoom *= zoomChange
rotation += rotationChange
pan += panChange
val centroidSize = event.calculateCentroidSize(useCurrent = false)
val zoomMotion = abs(1 - zoom) * centroidSize
val rotationMotion =
abs(rotation * PI.toFloat() * centroidSize / 180f)
val panMotion = pan.getDistance()
if (zoomMotion > touchSlop ||
rotationMotion > touchSlop ||
panMotion > touchSlop
) {
pastTouchSlop = true
lockedToPanZoom = panZoomLock && rotationMotion < touchSlop
}
}
if (pastTouchSlop) {
val centroid = event.calculateCentroid(useCurrent = false)
val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
if (effectiveRotation != 0f ||
zoomChange != 1f ||
panChange != Offset.Zero
) {
onGesture(centroid, panChange, zoomChange, effectiveRotation)
}
event.changes.forEach {
if (it.positionChanged()) {
it.consumeAllChanges()
}
}
}
}
} while (!canceled && event.changes.any { it.pressed })
}
}
}
Edit
As of 1.2.0-beta01, partial consumes like
PointerInputChange.consemePositionChange(),
PointerInputChange.consumeDownChange(), and one for consuming all changes PointerInputChange.consumeAllChanges() are deprecated
PointerInputChange.consume()
is the only one to be used preventing other gestures/event.
Also i have a tutorial here that covers gestures in detail
Maybe a bit late, but since compose is constantly updating, this is how I do it as of today:
Modifier
.pointerInput(Unit) {
detectTapGestures {...}
}
.pointerInput(Unit) {
detectDragGestures { change, dragAmount -> ...}
})
We also have detectHorizontalDragGestures and detectVerticalDragGestures among others to help us.
ps: 1.0.0-beta03
After did some research, looks like can use dragGestureFilter, mixed with tapGestureFilter
Modifier
.dragGestureFilter(object: DragObserver {
override fun onDrag(dragDistance: Offset): Offset {
Log.d("Track", "onActionMove ${dragDistance.x} | ${dragDistance.y}")
return super.onDrag(dragDistance)
}
override fun onStart(downPosition: Offset) {
Log.d("Track", "onActionDown ${downPosition.x} | ${downPosition.y}")
super.onStart(downPosition)
}
override fun onStop(velocity: Offset) {
Log.d("Track", "onStop ${velocity.x} | ${velocity.y}")
super.onStop(velocity)
}
}, { true })
.tapGestureFilter {
Log.d("NGVL", "onActionUp ${it.x} | ${it.y}")
}
The reason still use tagGestureFilter, is because the onStop doesn't provide the position, but just the velocity, hence the tapGestureFilter does help provide the last position (if needed)
I merged some CameraX tutorials so that it has pinch to zoom and tap to focus.
By themselves, they work well, but together, the OnClickListener and OnTouchListeners are interfering with each other.
I thought merging them under a single OnClickListener method where the pinch to zoom is executed on the ACTION_DOWN, and the tap to focus on the ACTION_UP, but only the tap to focus is running. Even if it did work, this feels a bit clunky and I'd appreciate some more advanced guidance.
zoomAndFocus is triggered by: "viewFinder.setOnClickListener{zoomAndFocus()}" in onCreate.
private fun zoomAndFocus(){
Log.d("onclick","detecting clck?")
viewFinder.setOnTouchListener { _, event ->
return#setOnTouchListener when (event.action) {
MotionEvent.ACTION_DOWN -> {
val listener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
val zoomRatio = camera?.cameraInfo?.zoomState?.value?.zoomRatio ?: 0f
val scale = zoomRatio * detector.scaleFactor
camera!!.cameraControl.setZoomRatio(scale)
return true
}
}
val scaleGestureDetector = ScaleGestureDetector(this, listener)
scaleGestureDetector.onTouchEvent(event)
true
}
MotionEvent.ACTION_UP -> {
val factory: MeteringPointFactory = SurfaceOrientedMeteringPointFactory(
viewFinder.width.toFloat(), viewFinder.height.toFloat()
)
val autoFocusPoint = factory.createPoint(event.x, event.y)
try {
camera?.cameraControl?.startFocusAndMetering(
FocusMeteringAction.Builder(
autoFocusPoint,
FocusMeteringAction.FLAG_AF
).apply {
//focus only when the user tap the preview
disableAutoCancel()
}.build()
)
} catch (e: CameraInfoUnavailableException) {
Log.d("ERROR", "cannot access camera", e)
}
viewFinder.performClick()
true
}
else -> false // Unhandled event.
}
}
Use this to add pinch to zoom and tap to focus together:
private fun setupZoomAndTapToFocus() {
val listener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
val currentZoomRatio: Float = cameraInfo.zoomState.value?.zoomRatio ?: 1F
val delta = detector.scaleFactor
cameraControl.setZoomRatio(currentZoomRatio * delta)
return true
}
}
val scaleGestureDetector = ScaleGestureDetector(viewFinder.context, listener)
viewFinder.setOnTouchListener { _, event ->
scaleGestureDetector.onTouchEvent(event)
if (event.action == MotionEvent.ACTION_DOWN) {
val factory = viewFinder.createMeteringPointFactory(cameraSelector)
val point = factory.createPoint(event.x, event.y)
val action = FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF)
.setAutoCancelDuration(5, TimeUnit.SECONDS)
.build()
cameraControl.startFocusAndMetering(action)
}
true
}
}
Android has released a new API camerax in recent months. I'm trying to understand how to get auto-focusing for the camera to work.
https://groups.google.com/a/android.com/forum/#!searchin/camerax-developers/auto$20focus|sort:date/camerax-developers/IQ3KZd8iOIY/LIbrRIqEBgAJ
Here is a discussion on the topic but there is almost no specific documentation on it.
https://github.com/android/camera-samples/tree/master/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic
Here is also the basic camerax app but I couldn't find any file dealing with the auto focusing.
Any tips or points to documentation is helpful. Also I'm fairly new to android so its very possible I'm missing something that makes the above links more useful.
With the current CameraX 1.0.0, you can proceed in this 2 ways:
Auto-focus every X seconds:
previewView.afterMeasured {
val autoFocusPoint = SurfaceOrientedMeteringPointFactory(1f, 1f)
.createPoint(.5f, .5f)
try {
val autoFocusAction = FocusMeteringAction.Builder(
autoFocusPoint,
FocusMeteringAction.FLAG_AF
).apply {
//start auto-focusing after 2 seconds
setAutoCancelDuration(2, TimeUnit.SECONDS)
}.build()
camera.cameraControl.startFocusAndMetering(autoFocusAction)
} catch (e: CameraInfoUnavailableException) {
Log.d("ERROR", "cannot access camera", e)
}
}
Focus on-tap:
previewView.afterMeasured {
previewView.setOnTouchListener { _, event ->
return#setOnTouchListener when (event.action) {
MotionEvent.ACTION_DOWN -> {
true
}
MotionEvent.ACTION_UP -> {
val factory: MeteringPointFactory = SurfaceOrientedMeteringPointFactory(
previewView.width.toFloat(), previewView.height.toFloat()
)
val autoFocusPoint = factory.createPoint(event.x, event.y)
try {
camera.cameraControl.startFocusAndMetering(
FocusMeteringAction.Builder(
autoFocusPoint,
FocusMeteringAction.FLAG_AF
).apply {
//focus only when the user tap the preview
disableAutoCancel()
}.build()
)
} catch (e: CameraInfoUnavailableException) {
Log.d("ERROR", "cannot access camera", e)
}
true
}
else -> false // Unhandled event.
}
}
}
afterMeasured extension function is a simple utility: (thanks ch271828n for improving it)
inline fun View.afterMeasured(crossinline block: () -> Unit) {
if (measuredWidth > 0 && measuredHeight > 0) {
block()
} else {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.GlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
block()
}
}
})
}
}
A Camera object can be obtained with
val camera = cameraProvider.bindToLifecycle(
this#Activity, cameraSelector, previewView //this is a PreviewView
)
Just point out, to get the "Tap to focus" working with PreviewView, you need to use
DisplayOrientedMeteringPointFactory. Otherwise you'll get messed up coordinates.
val factory = DisplayOrientedMeteringPointFactory(activity.display, camera.cameraInfo, previewView.width.toFloat(), previewView.height.toFloat())
For the rest use the MatPag's answer.
There is an issue with some android devices where the camera's aren't auto-focusing with CameraX. The CameraX team is aware of it and are tracking it with an internal ticket and hopefully will have a fix soon.
You can find the doc here about Focus as it was added in "1.0.0-alpha05"
https://developer.android.com/jetpack/androidx/releases/camera#camera2-core-1.0.0-alpha05
Basically you have to set a touch listener on your view and grab the clicked position
private boolean onTouchToFocus(View viewA, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_UP:
return focus(event);
break;
default:
// Unhandled event.
return false;
}
return true;
}
And translate this position into point
private boolean focus(MotionEvent event) {
final float x = (event != null) ? event.getX() : getView().getX() + getView().getWidth() / 2f;
final float y = (event != null) ? event.getY() : getView().getY() + getView().getHeight() / 2f;
TextureViewMeteringPointFactory pointFactory = new TextureViewMeteringPointFactory(textureView);
float afPointWidth = 1.0f / 6.0f; // 1/6 total area
float aePointWidth = afPointWidth * 1.5f;
MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth, 1.0f);
MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth, 1.0f);
try {
CameraX.getCameraControl(lensFacing).startFocusAndMetering(
FocusMeteringAction.Builder.from(afPoint, FocusMeteringAction.MeteringMode.AF_ONLY)
.addPoint(aePoint, FocusMeteringAction.MeteringMode.AE_ONLY)
.build());
} catch (CameraInfoUnavailableException e) {
Log.d(TAG, "cannot access camera", e);
}
return true;
}
With current 1.0.0-rc03 and 1.0.0-alpha22 artifacts
This solution assumes that camera is already setup including bindToLifecycle. After that we need to check whether previewView streamState is STREAMING before trying to focus the camera
previewView.getPreviewStreamState().observe(getActivity(), value -> {
if (value.equals(STREAMING)) {
setUpCameraAutoFocus();
}
});
private void setUpCameraAutoFocus() {
final float x = previewView.getX() + previewView.getWidth() / 2f;
final float y = previewView.getY() + previewView.getHeight() / 2f;
MeteringPointFactory pointFactory = previewView.getMeteringPointFactory();
float afPointWidth = 1.0f / 6.0f; // 1/6 total area
float aePointWidth = afPointWidth * 1.5f;
MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth);
MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth);
ListenableFuture<FocusMeteringResult> future = cameraControl.startFocusAndMetering(
new FocusMeteringAction.Builder(afPoint,
FocusMeteringAction.FLAG_AF).addPoint(aePoint,
FocusMeteringAction.FLAG_AE).build());
Futures.addCallback(future, new FutureCallback<FocusMeteringResult>() {
#Override
public void onSuccess(#Nullable FocusMeteringResult result) {
}
#Override
public void onFailure(Throwable t) {
// Throw the unexpected error.
throw new RuntimeException(t);
}
}, CameraXExecutors.directExecutor());
}
The afterMeasured function in highest voted answer has a serious bug: Frequently its callback is never called.
The very simple fix:
inline fun View.afterMeasured(crossinline block: () -> Unit) {
if (measuredWidth > 0 && measuredHeight > 0) {
block()
} else {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
block()
}
}
})
}
}
Explanation: I have observed (in an app in production) that, sometimes the view is already measured and no ui changes so onGlobalLayout will never be called later. Then the afterMeasured's callback will never be called, so the camera is not initialized.
I ran into the same issue and I set up this solution (even if it looks pretty dumb).
val displayMetrics = resources.displayMetrics
val factory = SurfaceOrientedMeteringPointFactory(
displayMetrics.widthPixels.toFloat(),
displayMetrics.heightPixels.toFloat()
)
val point = factory.createPoint(
displayMetrics.widthPixels / 2f,
displayMetrics.heightPixels / 2f
)
val action = FocusMeteringAction
.Builder(point, FocusMeteringAction.FLAG_AF)
.build()
try {
camera = cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageAnalyzer
)
GlobalScope.launch(Dispatchers.Default) {
while (workflowModel.isCameraLive) {
camera?.cameraControl?.startFocusAndMetering(action)?
delay(3000)
}
}
} catch (e: Exception) {
Log.e(mTag, "Use case binding failed", e)
}
Basically, I restart the focusing action every 3s in a while loop.
isCameraLive is a boolean variable I store in my viewModel and I set true when I start the camera and false when I stop it by calling cameraProvider.unbindAll().