TouchTracker.java
/*
* Copyright (C) 2018 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.media3.exoplayer.video.spherical;
import android.content.Context;
import android.graphics.PointF;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.BinderThread;
/**
* Basic touch input system.
*
* <p>Mixing touch input and gyro input results in a complicated UI so this should be used
* carefully. This touch system implements a basic (X, Y) -> (yaw, pitch) transform. This works for
* basic UI but fails in edge cases where the user tries to drag scene up or down. There is no good
* UX solution for this. The least bad solution is to disable pitch manipulation and only let the
* user adjust yaw. This example tries to limit the awkwardness by restricting pitch manipulation to
* +/- 45 degrees.
*
* <p>It is also important to get the order of operations correct. To match what users expect, touch
* interaction manipulates the scene by rotating the world by the yaw offset and tilting the camera
* by the pitch offset. If the order of operations is incorrect, the sensors & touch rotations will
* have strange interactions. The roll of the phone is also tracked so that the x & y are correctly
* mapped to yaw & pitch no matter how the user holds their phone.
*
* <p>This class doesn't handle any scrolling inertia but Android's
* com.google.vr.sdk.widgets.common.TouchTracker.FlingGestureListener can be used with this code for
* a nicer UI. An even more advanced UI would reproject the user's touch point into 3D and drag the
* Mesh as the user moves their finger. However, that requires quaternion interpolation.
*/
/* package */ final class TouchTracker extends GestureDetector.SimpleOnGestureListener
implements View.OnTouchListener, OrientationListener.Listener {
public interface Listener {
void onScrollChange(PointF scrollOffsetDegrees);
default boolean onSingleTapUp(MotionEvent event) {
return false;
}
}
// Touch input won't change the pitch beyond +/- 45 degrees. This reduces awkward situations
// where the touch-based pitch and gyro-based pitch interact badly near the poles.
/* package */ static final float MAX_PITCH_DEGREES = 45;
// With every touch event, update the accumulated degrees offset by the new pixel amount.
private final PointF previousTouchPointPx = new PointF();
private final PointF accumulatedTouchOffsetDegrees = new PointF();
private final Listener listener;
private final float pxPerDegrees;
private final GestureDetector gestureDetector;
// The conversion from touch to yaw & pitch requires compensating for device roll. This is set
// on the sensor thread and read on the UI thread.
private volatile float roll;
@SuppressWarnings({"nullness:assignment", "nullness:argument"})
public TouchTracker(Context context, Listener listener, float pxPerDegrees) {
this.listener = listener;
this.pxPerDegrees = pxPerDegrees;
gestureDetector = new GestureDetector(context, this);
roll = SphericalGLSurfaceView.UPRIGHT_ROLL;
}
/**
* Converts ACTION_MOVE events to pitch & yaw events while compensating for device roll.
*
* @return true if we handled the event
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
@Override
public boolean onDown(MotionEvent e) {
// Initialize drag gesture.
previousTouchPointPx.set(e.getX(), e.getY());
return true;
}
// Incompatible parameter type for e1.
@SuppressWarnings("nullness:override.param.invalid")
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// Calculate the touch delta in screen space.
float touchX = (e2.getX() - previousTouchPointPx.x) / pxPerDegrees;
float touchY = (e2.getY() - previousTouchPointPx.y) / pxPerDegrees;
previousTouchPointPx.set(e2.getX(), e2.getY());
float r = roll; // Copy volatile state.
float cr = (float) Math.cos(r);
float sr = (float) Math.sin(r);
// To convert from screen space to the 3D space, we need to adjust the drag vector based
// on the roll of the phone. This is standard rotationMatrix(roll) * vector math but has
// an inverted y-axis due to the screen-space coordinates vs GL coordinates.
// Handle yaw.
accumulatedTouchOffsetDegrees.x -= cr * touchX - sr * touchY;
// Handle pitch and limit it to 45 degrees.
accumulatedTouchOffsetDegrees.y += sr * touchX + cr * touchY;
accumulatedTouchOffsetDegrees.y =
Math.max(-MAX_PITCH_DEGREES, Math.min(MAX_PITCH_DEGREES, accumulatedTouchOffsetDegrees.y));
listener.onScrollChange(accumulatedTouchOffsetDegrees);
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return listener.onSingleTapUp(e);
}
@Override
@BinderThread
public void onOrientationChange(float[] deviceOrientationMatrix, float roll) {
// We compensate for roll by rotating in the opposite direction.
this.roll = -roll;
}
}