termux-app/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionHandleView.java

346 lines
11 KiB
Java

package com.termux.view.textselection;
import android.annotation.SuppressLint;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.WindowManager;
import android.widget.PopupWindow;
import com.termux.view.R;
import com.termux.view.TerminalView;
@SuppressLint("ViewConstructor")
public class TextSelectionHandleView extends View {
private final TerminalView terminalView;
private PopupWindow mHandle;
private final CursorController mCursorController;
private final Drawable mHandleLeftDrawable;
private final Drawable mHandleRightDrawable;
private Drawable mHandleDrawable;
private boolean mIsDragging;
final int[] mTempCoords = new int[2];
Rect mTempRect;
private int mPointX;
private int mPointY;
private float mTouchToWindowOffsetX;
private float mTouchToWindowOffsetY;
private float mHotspotX;
private float mHotspotY;
private float mTouchOffsetY;
private int mLastParentX;
private int mLastParentY;
private int mHandleHeight;
private int mHandleWidth;
private final int mInitialOrientation;
private int mOrientation;
public static final int LEFT = 0;
public static final int RIGHT = 2;
private long mLastTime;
public TextSelectionHandleView(TerminalView terminalView, CursorController cursorController, int initialOrientation) {
super(terminalView.getContext());
this.terminalView = terminalView;
mCursorController = cursorController;
mInitialOrientation = initialOrientation;
mHandleLeftDrawable = getContext().getDrawable(R.drawable.text_select_handle_left_material);
mHandleRightDrawable = getContext().getDrawable(R.drawable.text_select_handle_right_material);
setOrientation(mInitialOrientation);
}
private void initHandle() {
mHandle = new PopupWindow(terminalView.getContext(), null,
android.R.attr.textSelectHandleWindowStyle);
mHandle.setSplitTouchEnabled(true);
mHandle.setClippingEnabled(false);
mHandle.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
mHandle.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
mHandle.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
mHandle.setBackgroundDrawable(null);
mHandle.setAnimationStyle(0);
mHandle.setEnterTransition(null);
mHandle.setExitTransition(null);
mHandle.setContentView(this);
}
public void setOrientation(int orientation) {
mOrientation = orientation;
int handleWidth = 0;
switch (orientation) {
case LEFT: {
mHandleDrawable = mHandleLeftDrawable;
handleWidth = mHandleDrawable.getIntrinsicWidth();
mHotspotX = (handleWidth * 3) / (float) 4;
break;
}
case RIGHT: {
mHandleDrawable = mHandleRightDrawable;
handleWidth = mHandleDrawable.getIntrinsicWidth();
mHotspotX = handleWidth / (float) 4;
break;
}
}
mHandleHeight = mHandleDrawable.getIntrinsicHeight();
mHandleWidth = handleWidth;
mTouchOffsetY = -mHandleHeight * 0.3f;
mHotspotY = 0;
invalidate();
}
public void show() {
if (!isPositionVisible()) {
hide();
return;
}
// We remove handle from its parent first otherwise the following exception may be thrown
// java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
removeFromParent();
initHandle(); // init the handle
invalidate(); // invalidate to make sure onDraw is called
final int[] coords = mTempCoords;
terminalView.getLocationInWindow(coords);
coords[0] += mPointX;
coords[1] += mPointY;
if (mHandle != null)
mHandle.showAtLocation(terminalView, 0, coords[0], coords[1]);
}
public void hide() {
mIsDragging = false;
if (mHandle != null) {
mHandle.dismiss();
// We remove handle from its parent, otherwise it may still be shown in some cases even after the dismiss call
removeFromParent();
mHandle = null; // garbage collect the handle
}
invalidate();
}
public void removeFromParent() {
if (!isParentNull()) {
((ViewGroup)this.getParent()).removeView(this);
}
}
public void positionAtCursor(final int cx, final int cy, boolean forceOrientationCheck) {
int x = terminalView.getPointX(cx);
int y = terminalView.getPointY(cy + 1);
moveTo(x, y, forceOrientationCheck);
}
private void moveTo(int x, int y, boolean forceOrientationCheck) {
float oldHotspotX = mHotspotX;
checkChangedOrientation(x, forceOrientationCheck);
mPointX = (int) (x - (isShowing() ? oldHotspotX : mHotspotX));
mPointY = y;
if (isPositionVisible()) {
int[] coords = null;
if (isShowing()) {
coords = mTempCoords;
terminalView.getLocationInWindow(coords);
int x1 = coords[0] + mPointX;
int y1 = coords[1] + mPointY;
if (mHandle != null)
mHandle.update(x1, y1, getWidth(), getHeight());
} else {
show();
}
if (mIsDragging) {
if (coords == null) {
coords = mTempCoords;
terminalView.getLocationInWindow(coords);
}
if (coords[0] != mLastParentX || coords[1] != mLastParentY) {
mTouchToWindowOffsetX += coords[0] - mLastParentX;
mTouchToWindowOffsetY += coords[1] - mLastParentY;
mLastParentX = coords[0];
mLastParentY = coords[1];
}
}
} else {
hide();
}
}
public void changeOrientation(int orientation) {
if (mOrientation != orientation) {
setOrientation(orientation);
}
}
private void checkChangedOrientation(int posX, boolean force) {
if (!mIsDragging && !force) {
return;
}
long millis = SystemClock.currentThreadTimeMillis();
if (millis - mLastTime < 50 && !force) {
return;
}
mLastTime = millis;
final TerminalView hostView = terminalView;
final int left = hostView.getLeft();
final int right = hostView.getWidth();
final int top = hostView.getTop();
final int bottom = hostView.getHeight();
if (mTempRect == null) {
mTempRect = new Rect();
}
final Rect clip = mTempRect;
clip.left = left + terminalView.getPaddingLeft();
clip.top = top + terminalView.getPaddingTop();
clip.right = right - terminalView.getPaddingRight();
clip.bottom = bottom - terminalView.getPaddingBottom();
final ViewParent parent = hostView.getParent();
if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) {
return;
}
if (posX - mHandleWidth < clip.left) {
changeOrientation(RIGHT);
} else if (posX + mHandleWidth > clip.right) {
changeOrientation(LEFT);
} else {
changeOrientation(mInitialOrientation);
}
}
private boolean isPositionVisible() {
// Always show a dragging handle.
if (mIsDragging) {
return true;
}
final TerminalView hostView = terminalView;
final int left = 0;
final int right = hostView.getWidth();
final int top = 0;
final int bottom = hostView.getHeight();
if (mTempRect == null) {
mTempRect = new Rect();
}
final Rect clip = mTempRect;
clip.left = left + terminalView.getPaddingLeft();
clip.top = top + terminalView.getPaddingTop();
clip.right = right - terminalView.getPaddingRight();
clip.bottom = bottom - terminalView.getPaddingBottom();
final ViewParent parent = hostView.getParent();
if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) {
return false;
}
final int[] coords = mTempCoords;
hostView.getLocationInWindow(coords);
final int posX = coords[0] + mPointX + (int) mHotspotX;
final int posY = coords[1] + mPointY + (int) mHotspotY;
return posX >= clip.left && posX <= clip.right &&
posY >= clip.top && posY <= clip.bottom;
}
@Override
public void onDraw(Canvas c) {
final int width = mHandleDrawable.getIntrinsicWidth();
int height = mHandleDrawable.getIntrinsicHeight();
mHandleDrawable.setBounds(0, 0, width, height);
mHandleDrawable.draw(c);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
terminalView.updateFloatingToolbarVisibility(event);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
final float rawX = event.getRawX();
final float rawY = event.getRawY();
mTouchToWindowOffsetX = rawX - mPointX;
mTouchToWindowOffsetY = rawY - mPointY;
final int[] coords = mTempCoords;
terminalView.getLocationInWindow(coords);
mLastParentX = coords[0];
mLastParentY = coords[1];
mIsDragging = true;
break;
}
case MotionEvent.ACTION_MOVE: {
final float rawX = event.getRawX();
final float rawY = event.getRawY();
final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
final float newPosY = rawY - mTouchToWindowOffsetY + mHotspotY + mTouchOffsetY;
mCursorController.updatePosition(this, Math.round(newPosX), Math.round(newPosY));
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsDragging = false;
}
return true;
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(mHandleDrawable.getIntrinsicWidth(),
mHandleDrawable.getIntrinsicHeight());
}
public int getHandleHeight() {
return mHandleHeight;
}
public int getHandleWidth() {
return mHandleWidth;
}
public boolean isShowing() {
if (mHandle != null)
return mHandle.isShowing();
else
return false;
}
public boolean isParentNull() {
return this.getParent() == null;
}
public boolean isDragging() {
return mIsDragging;
}
}