이 문서에서는 터치 동작을 사용하여 화면에서 드래그하고 크기를 조정하는 방법을 설명합니다.
객체에 대해
onTouchEvent()
사용하여 터치 이벤트를 가로챕니다.
객체 드래그
<ph type="x-smartling-placeholder">터치 동작의 일반적인 작업은 터치 동작으로 객체를 가로질러 화면
드래그 또는 스크롤 작업 시 앱은 원래 다른 손가락이 화면을 터치하더라도 예를 들어 이미지를 드래그하는 동안 사용자가 터치스크린에 두 번째 손가락을 올려놓으면 첫 번째 손가락을 뗍니다. 앱이 개별 포인터만 추적하는 경우 두 번째 포인터를 기본값으로 간주하고 이미지를 볼 수 있습니다
이를 방지하려면 앱에서
모든 후속 포인터에 적용됩니다. 이를 위해 BERT 모델은
ACTION_POINTER_DOWN
및
ACTION_POINTER_UP
멀티 터치 동작 처리에 설명된 대로 이벤트를 처리합니다.
ACTION_POINTER_DOWN
및 ACTION_POINTER_UP
가 전달됨
보조 포인터가 이동할 때마다 onTouchEvent()
콜백에 전달
위쪽 또는 아래쪽
좌우로 이동합니다
ACTION_POINTER_UP
의 경우 이 색인을 추출하고
활성 포인터 ID가 더 이상 존재하지 않는 포인터를 참조하지 않도록 함
할 수 있습니다. 이 경우 다른 포인터를 활성 상태로 선택할 수 있습니다.
현재 X와 Y 위치를 저장합니다. 이 저장된 위치를
ACTION_MOVE
케이스를 사용하여 화면상의 객체를 이동할 거리를 계산합니다. 이렇게 하면
는 항상 올바른 포인터의 데이터를 사용하여 이동할 거리를 계산합니다.
다음 코드 스니펫을 사용하면 사용자가 화면에서 객체를 드래그할 수 있습니다. 그것은 활성 포인터의 초기 위치를 기록하고 포인터가 이동하고 객체를 새 위치로 이동합니다. 또한 추가 포인터의 가능성을 관리합니다.
이 스니펫은
getActionMasked()
메서드를 사용하여 축소하도록 요청합니다. 항상 이 메서드를 사용하여
MotionEvent
// The "active pointer" is the one moving the object. private var mActivePointerId = INVALID_POINTER_ID override fun onTouchEvent(ev: MotionEvent): Boolean { // Let the ScaleGestureDetector inspect all events. mScaleDetector.onTouchEvent(ev) val action = MotionEventCompat.getActionMasked(ev) when (action) { MotionEvent.ACTION_DOWN -> { MotionEventCompat.getActionIndex(ev).also { pointerIndex -> // Remember where you start for dragging. mLastTouchX = MotionEventCompat.getX(ev, pointerIndex) mLastTouchY = MotionEventCompat.getY(ev, pointerIndex) } // Save the ID of this pointer for dragging. mActivePointerId = MotionEventCompat.getPointerId(ev, 0) } MotionEvent.ACTION_MOVE -> { // Find the index of the active pointer and fetch its position. val (x: Float, y: Float) = MotionEventCompat.findPointerIndex(ev, mActivePointerId).let { pointerIndex -> // Calculate the distance moved. MotionEventCompat.getX(ev, pointerIndex) to MotionEventCompat.getY(ev, pointerIndex) } mPosX += x - mLastTouchX mPosY += y - mLastTouchY invalidate() // Remember this touch position for the next move event. mLastTouchX = x mLastTouchY = y } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { mActivePointerId = INVALID_POINTER_ID } MotionEvent.ACTION_POINTER_UP -> { MotionEventCompat.getActionIndex(ev).also { pointerIndex -> MotionEventCompat.getPointerId(ev, pointerIndex) .takeIf { it == mActivePointerId } ?.run { // This is the active pointer going up. Choose a new // active pointer and adjust it accordingly. val newPointerIndex = if (pointerIndex == 0) 1 else 0 mLastTouchX = MotionEventCompat.getX(ev, newPointerIndex) mLastTouchY = MotionEventCompat.getY(ev, newPointerIndex) mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex) } } } } return true }
// The "active pointer" is the one moving the object. private int mActivePointerId = INVALID_POINTER_ID; @Override public boolean onTouchEvent(MotionEvent ev) { // Let the ScaleGestureDetector inspect all events. mScaleDetector.onTouchEvent(ev); final int action = MotionEventCompat.getActionMasked(ev); switch (action) { case MotionEvent.ACTION_DOWN: { final int pointerIndex = MotionEventCompat.getActionIndex(ev); final float x = MotionEventCompat.getX(ev, pointerIndex); final float y = MotionEventCompat.getY(ev, pointerIndex); // Remember the starting position of the pointer. mLastTouchX = x; mLastTouchY = y; // Save the ID of this pointer for dragging. mActivePointerId = MotionEventCompat.getPointerId(ev, 0); break; } case MotionEvent.ACTION_MOVE: { // Find the index of the active pointer and fetch its position. final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); final float x = MotionEventCompat.getX(ev, pointerIndex); final float y = MotionEventCompat.getY(ev, pointerIndex); // Calculate the distance moved. final float dx = x - mLastTouchX; final float dy = y - mLastTouchY; mPosX += dx; mPosY += dy; invalidate(); // Remember this touch position for the next move event. mLastTouchX = x; mLastTouchY = y; break; } case MotionEvent.ACTION_UP: { mActivePointerId = INVALID_POINTER_ID; break; } case MotionEvent.ACTION_CANCEL: { mActivePointerId = INVALID_POINTER_ID; break; } case MotionEvent.ACTION_POINTER_UP: { final int pointerIndex = MotionEventCompat.getActionIndex(ev); final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); if (pointerId == mActivePointerId) { // This is the active pointer going up. Choose a new // active pointer and adjust it accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mLastTouchX = MotionEventCompat.getX(ev, newPointerIndex); mLastTouchY = MotionEventCompat.getY(ev, newPointerIndex); mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); } break; } } return true; }
드래그하여 이동
이전 섹션에서는 화면에서 객체를 드래그하는 예를 보여줍니다.
또 다른 일반적인 시나리오는 화면 이동입니다. 화면 이동은 사용자가
X축과 Y축을 모두 스크롤하게 됩니다. 위의 스니펫은
MotionEvent
작업을 가로채서 드래그를 구현합니다. 이
이 섹션의 스니펫은 플랫폼의 기본 제공
일반적인 동작을 재정의하여
onScroll()
인치
GestureDetector.SimpleOnGestureListener
입니다.
더 많은 컨텍스트를 제공하기 위해 사용자가 드래그할 때 onScroll()
가 호출됩니다.
콘텐츠를 이동합니다 onScroll()
는
있습니다. 화면에서 손가락을 떼자마자
손가락이 움직이면 동작 종료 또는 플링 동작이 시작될 때
속도를 높여야 합니다. 스크롤과 스크롤에 관해
스크롤 동작 애니메이션 처리를 참고하세요.
다음은 onScroll()
의 코드 스니펫입니다.
// The current viewport. This rectangle represents the visible // chart domain and range. private val mCurrentViewport = RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX) // The current destination rectangle, in pixel coordinates, into which the // chart data must be drawn. private val mContentRect: Rect? = null private val mGestureListener = object : GestureDetector.SimpleOnGestureListener() { ... override fun onScroll( e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float ): Boolean { // Scrolling uses math based on the viewport, as opposed to math using // pixels. mContentRect?.apply { // Pixel offset is the offset in screen pixels, while viewport offset is the // offset within the current viewport. val viewportOffsetX = distanceX * mCurrentViewport.width() / width() val viewportOffsetY = -distanceY * mCurrentViewport.height() / height() // Updates the viewport and refreshes the display. setViewportBottomLeft( mCurrentViewport.left + viewportOffsetX, mCurrentViewport.bottom + viewportOffsetY ) } return true } }
// The current viewport. This rectangle represents the visible // chart domain and range. private RectF mCurrentViewport = new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX); // The current destination rectangle, in pixel coordinates, into which the // chart data must be drawn. private Rect mContentRect; private final GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() { ... @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // Scrolling uses math based on the viewport, as opposed to math using // pixels. // Pixel offset is the offset in screen pixels, while viewport offset is the // offset within the current viewport. float viewportOffsetX = distanceX * mCurrentViewport.width() / mContentRect.width(); float viewportOffsetY = -distanceY * mCurrentViewport.height() / mContentRect.height(); ... // Updates the viewport, refreshes the display. setViewportBottomLeft( mCurrentViewport.left + viewportOffsetX, mCurrentViewport.bottom + viewportOffsetY); ... return true; }
onScroll()
의 구현은 표시 영역을 스크롤하며
다음과 같습니다.
/** * Sets the current viewport, defined by mCurrentViewport, to the given * X and Y positions. The Y value represents the topmost pixel position, * and thus the bottom of the mCurrentViewport rectangle. */ private fun setViewportBottomLeft(x: Float, y: Float) { /* * Constrains within the scroll range. The scroll range is the viewport * extremes, such as AXIS_X_MAX, minus the viewport size. For example, if * the extremes are 0 and 10 and the viewport size is 2, the scroll range * is 0 to 8. */ val curWidth: Float = mCurrentViewport.width() val curHeight: Float = mCurrentViewport.height() val newX: Float = Math.max(AXIS_X_MIN, Math.min(x, AXIS_X_MAX - curWidth)) val newY: Float = Math.max(AXIS_Y_MIN + curHeight, Math.min(y, AXIS_Y_MAX)) mCurrentViewport.set(newX, newY - curHeight, newX + curWidth, newY) // Invalidates the View to update the display. ViewCompat.postInvalidateOnAnimation(this) }
/** * Sets the current viewport (defined by mCurrentViewport) to the given * X and Y positions. Note that the Y value represents the topmost pixel * position, and thus the bottom of the mCurrentViewport rectangle. */ private void setViewportBottomLeft(float x, float y) { /* * Constrains within the scroll range. The scroll range is the viewport * extremes, such as AXIS_X_MAX, minus the viewport size. For example, if * the extremes are 0 and 10 and the viewport size is 2, the scroll range * is 0 to 8. */ float curWidth = mCurrentViewport.width(); float curHeight = mCurrentViewport.height(); x = Math.max(AXIS_X_MIN, Math.min(x, AXIS_X_MAX - curWidth)); y = Math.max(AXIS_Y_MIN + curHeight, Math.min(y, AXIS_Y_MAX)); mCurrentViewport.set(x, y - curHeight, x + curWidth, y); // Invalidates the View to update the display. ViewCompat.postInvalidateOnAnimation(this); }
터치를 사용하여 확장
일반 동작 감지에 설명된 대로
사용
<ph type="x-smartling-placeholder">GestureDetector
</ph>
Android에서 사용하는 일반적인 동작(예: 스크롤, 살짝 튕기기,
길게 터치합니다. 확장을 위해 Android는
ScaleGestureDetector
GestureDetector
및 ScaleGestureDetector
를 사용할 수 있습니다.
뷰가 추가 동작을 인식하도록 하고 싶을 때 함께 사용할 수도 있습니다.
감지된 동작 이벤트를 보고하기 위해 동작 감지기는 리스너 객체를 사용합니다.
생성자에 전달됩니다. ScaleGestureDetector
사용
ScaleGestureDetector.OnScaleGestureListener
입니다.
Android 제공 기능
ScaleGestureDetector.SimpleOnScaleGestureListener
보고된 클래스 중 일부가 필요하지 않은 경우 확장할 수 있는 도우미 클래스로
이벤트를 수신합니다.
기본적인 확장의 예
다음 스니펫은 확장과 관련된 기본 요소를 보여줍니다.
private var mScaleFactor = 1f private val scaleListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { mScaleFactor *= detector.scaleFactor // Don't let the object get too small or too large. mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f)) invalidate() return true } } private val mScaleDetector = ScaleGestureDetector(context, scaleListener) override fun onTouchEvent(ev: MotionEvent): Boolean { // Let the ScaleGestureDetector inspect all events. mScaleDetector.onTouchEvent(ev) return true } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.apply { save() scale(mScaleFactor, mScaleFactor) // onDraw() code goes here. restore() } }
private ScaleGestureDetector mScaleDetector; private float mScaleFactor = 1.f; public MyCustomView(Context mContext){ ... // View code goes here. ... mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); } @Override public boolean onTouchEvent(MotionEvent ev) { // Let the ScaleGestureDetector inspect all events. mScaleDetector.onTouchEvent(ev); return true; } @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); canvas.scale(mScaleFactor, mScaleFactor); ... // onDraw() code goes here. ... canvas.restore(); } private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScale(ScaleGestureDetector detector) { mScaleFactor *= detector.getScaleFactor(); // Don't let the object get too small or too large. mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f)); invalidate(); return true; } }
더 복잡한 확장의 예
다음은
InteractiveChart
개 샘플 표시
스크롤 동작 애니메이션 처리
이
스크롤, 화면 이동, 크기 조정을 지원하는 InteractiveChart
샘플
여러 손가락으로(ScaleGestureDetector
스팬 사용)
(getCurrentSpanX
개
및
getCurrentSpanY
)
및 'focus'
(getFocusX
)
및 getFocusY
)
기능을 살펴보겠습니다
private val mCurrentViewport = RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX) private val mContentRect: Rect? = null ... override fun onTouchEvent(event: MotionEvent): Boolean { return mScaleGestureDetector.onTouchEvent(event) || mGestureDetector.onTouchEvent(event) || super.onTouchEvent(event) } /** * The scale listener, used for handling multi-finger scale gestures. */ private val mScaleGestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() { /** * This is the active focal point in terms of the viewport. It can be a * local variable, but keep it here to minimize per-frame allocations. */ private val viewportFocus = PointF() private var lastSpanX: Float = 0f private var lastSpanY: Float = 0f // Detects new pointers are going down. override fun onScaleBegin(scaleGestureDetector: ScaleGestureDetector): Boolean { lastSpanX = scaleGestureDetector.currentSpanX lastSpanY = scaleGestureDetector.currentSpanY return true } override fun onScale(scaleGestureDetector: ScaleGestureDetector): Boolean { val spanX: Float = scaleGestureDetector.currentSpanX val spanY: Float = scaleGestureDetector.currentSpanY val newWidth: Float = lastSpanX / spanX * mCurrentViewport.width() val newHeight: Float = lastSpanY / spanY * mCurrentViewport.height() val focusX: Float = scaleGestureDetector.focusX val focusY: Float = scaleGestureDetector.focusY // Ensures the chart point is within the chart region. // See the sample for the implementation of hitTest(). hitTest(focusX, focusY, viewportFocus) mContentRect?.apply { mCurrentViewport.set( viewportFocus.x - newWidth * (focusX - left) / width(), viewportFocus.y - newHeight * (bottom - focusY) / height(), 0f, 0f ) } mCurrentViewport.right = mCurrentViewport.left + newWidth mCurrentViewport.bottom = mCurrentViewport.top + newHeight // Invalidates the View to update the display. ViewCompat.postInvalidateOnAnimation(this@InteractiveLineGraphView) lastSpanX = spanX lastSpanY = spanY return true } }
private RectF mCurrentViewport = new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX); private Rect mContentRect; private ScaleGestureDetector mScaleGestureDetector; ... @Override public boolean onTouchEvent(MotionEvent event) { boolean retVal = mScaleGestureDetector.onTouchEvent(event); retVal = mGestureDetector.onTouchEvent(event) || retVal; return retVal || super.onTouchEvent(event); } /** * The scale listener, used for handling multi-finger scale gestures. */ private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() { /** * This is the active focal point in terms of the viewport. It can be a * local variable, but keep it here to minimize per-frame allocations. */ private PointF viewportFocus = new PointF(); private float lastSpanX; private float lastSpanY; // Detects new pointers are going down. @Override public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) { lastSpanX = ScaleGestureDetectorCompat. getCurrentSpanX(scaleGestureDetector); lastSpanY = ScaleGestureDetectorCompat. getCurrentSpanY(scaleGestureDetector); return true; } @Override public boolean onScale(ScaleGestureDetector scaleGestureDetector) { float spanX = ScaleGestureDetectorCompat. getCurrentSpanX(scaleGestureDetector); float spanY = ScaleGestureDetectorCompat. getCurrentSpanY(scaleGestureDetector); float newWidth = lastSpanX / spanX * mCurrentViewport.width(); float newHeight = lastSpanY / spanY * mCurrentViewport.height(); float focusX = scaleGestureDetector.getFocusX(); float focusY = scaleGestureDetector.getFocusY(); // Ensures the chart point is within the chart region. // See the sample for the implementation of hitTest(). hitTest(scaleGestureDetector.getFocusX(), scaleGestureDetector.getFocusY(), viewportFocus); mCurrentViewport.set( viewportFocus.x - newWidth * (focusX - mContentRect.left) / mContentRect.width(), viewportFocus.y - newHeight * (mContentRect.bottom - focusY) / mContentRect.height(), 0, 0); mCurrentViewport.right = mCurrentViewport.left + newWidth; mCurrentViewport.bottom = mCurrentViewport.top + newHeight; ... // Invalidates the View to update the display. ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this); lastSpanX = spanX; lastSpanY = spanY; return true; } };
추가 리소스
입력 이벤트에 관한 자세한 내용은 다음 참조를 확인하세요. 사용자 지정 뷰를 대화형으로 만들 수 있습니다.