0
0
mirror of https://github.com/ankidroid/Anki-Android.git synced 2024-09-20 12:02:16 +02:00

Add multitouch capabilities to whiteboard for interfacing a card under it (#4728)

This makes the whiteboard more ergonomic (in my subjective opinion).
Related issue: https://github.com/ankidroid/Anki-Android/issues/4522
Summing up, when keeping one finger on the whiteboard while it is activated, it is now possible to use a second finger to scroll the card behind the whiteboard or tap onto one of its elements.
This commit is contained in:
LeopoldKellers 2017-12-02 05:40:55 +01:00 committed by Tim Rae
parent 3e74c3d895
commit 8223d04a91
2 changed files with 174 additions and 32 deletions

View File

@ -2075,6 +2075,36 @@ public abstract class AbstractFlashcardViewer extends NavigationDrawerActivity {
}
/** Scroll the currently shown flashcard vertically
*
* @param dy amount to be scrolled
*/
public void scrollCurrentCardBy(int dy) {
if (dy != 0 && mCard.canScrollVertically(dy)) {
mCard.scrollBy(0, dy);
}
}
/** Tap onto the currently shown flashcard at position x and y
*
* @param x horizontal position of the event
* @param y vertical position of the event
*/
public void tapOnCurrentCard(int x, int y) {
// assemble suitable ACTION_DOWN and ACTION_UP events and forward them to the card's handler
MotionEvent eDown = MotionEvent.obtain(SystemClock.uptimeMillis(),
SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, x, y,
1, 1, 0, 1, 1, 0, 0);
mCard.dispatchTouchEvent(eDown);
MotionEvent eUp = MotionEvent.obtain(eDown.getDownTime(),
SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y,
1, 1, 0, 1, 1, 0, 0);
mCard.dispatchTouchEvent(eUp);
}
/**
* getAnswerFormat returns the answer part of this card's template as entered by user, without any parsing
*/

View File

@ -48,19 +48,27 @@ public class Whiteboard extends View {
private Canvas mCanvas;
private Path mPath;
private Paint mBitmapPaint;
private WeakReference<AnkiActivity> mActivity;
private WeakReference<AbstractFlashcardViewer> mCardViewer;
private float mX;
private float mY;
private float mSecondFingerX0;
private float mSecondFingerY0;
private float mSecondFingerX;
private float mSecondFingerY;
private int mSecondFingerPointerId;
private boolean mSecondFingerWithinTapTolerance;
private boolean mCurrentlyDrawing = false;
private boolean mInvertedColors = false;
private boolean mMonochrome = true;
private boolean mUndoModeActive = false;
public Whiteboard(AnkiActivity context, boolean inverted, boolean monochrome) {
super(context, null);
mActivity = new WeakReference<>(context);
public Whiteboard(AbstractFlashcardViewer cardViewer, boolean inverted, boolean monochrome) {
super(cardViewer, null);
mCardViewer = new WeakReference<>(cardViewer);
mInvertedColors = inverted;
mMonochrome = monochrome;
@ -69,13 +77,13 @@ public class Whiteboard extends View {
if (mMonochrome) {
foregroundColor = Color.BLACK;
} else {
foregroundColor = ContextCompat.getColor(context, R.color.wb_fg_color);
foregroundColor = ContextCompat.getColor(cardViewer, R.color.wb_fg_color);
}
} else {
if (mMonochrome) {
foregroundColor = Color.WHITE;
} else {
foregroundColor = ContextCompat.getColor(context, R.color.wb_fg_color_inv);
foregroundColor = ContextCompat.getColor(cardViewer, R.color.wb_fg_color_inv);
}
}
@ -86,7 +94,7 @@ public class Whiteboard extends View {
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
int wbStrokeWidth = AnkiDroidApp.getSharedPrefs(context).getInt("whiteBoardStrokeWidth", 6);
int wbStrokeWidth = AnkiDroidApp.getSharedPrefs(cardViewer).getInt("whiteBoardStrokeWidth", 6);
mPaint.setStrokeWidth((float) wbStrokeWidth);
createBitmap();
mPath = new Path();
@ -103,37 +111,79 @@ public class Whiteboard extends View {
}
/**
* Handle touch screen motion events.
*
/** Handle motion events to draw using the touch screen or to interact with the flashcard behind
* the whiteboard by using a second finger.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
* @return True if the event was handled, false otherwise
*/
public boolean handleTouchEvent(MotionEvent event) {
return handleDrawEvent(event) || handleMultiTouchEvent(event);
}
/**
* Handle motion events to draw using the touch screen. Only simple touch events are processed,
* a multitouch event aborts to current stroke.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise or when drawing was aborted due to
* detection of a multitouch event.
*/
private boolean handleDrawEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
touchStart(x, y);
drawStart(x, y);
invalidate();
return true;
case MotionEvent.ACTION_MOVE:
for (int i = 0; i < event.getHistorySize(); i++) {
touchMove(event.getHistoricalX(i), event.getHistoricalY(i));
if (mCurrentlyDrawing) {
for (int i = 0; i < event.getHistorySize(); i++) {
drawAlong(event.getHistoricalX(i), event.getHistoricalY(i));
}
drawAlong(x, y);
invalidate();
return true;
}
touchMove(x, y);
invalidate();
return true;
return false;
case MotionEvent.ACTION_UP:
touchUp();
invalidate();
return true;
if (mCurrentlyDrawing) {
drawFinish();
invalidate();
return true;
}
return false;
case MotionEvent.ACTION_POINTER_DOWN:
if (mCurrentlyDrawing) {
drawAbort();
}
return false;
default:
return false;
}
}
// Parse multitouch input to scroll the card behind the whiteboard or click on elements
private boolean handleMultiTouchEvent(MotionEvent event) {
if (event.getPointerCount() == 2) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_POINTER_DOWN:
reinitializeSecondFinger(event);
return true;
case MotionEvent.ACTION_MOVE:
return trySecondFingerScroll(event);
case MotionEvent.ACTION_POINTER_UP:
return trySecondFingerClick(event);
default:
return false;
}
}
return false;
}
/**
* Clear the whiteboard.
@ -143,8 +193,8 @@ public class Whiteboard extends View {
mBitmap.eraseColor(0);
mUndo.clear();
invalidate();
if (mActivity.get() != null) {
mActivity.get().supportInvalidateOptionsMenu();
if (mCardViewer.get() != null) {
mCardViewer.get().supportInvalidateOptionsMenu();
}
}
@ -155,8 +205,8 @@ public class Whiteboard extends View {
public void undo() {
mUndo.pop();
mUndo.apply();
if (undoSize() == 0 && mActivity.get() != null) {
mActivity.get().supportInvalidateOptionsMenu();
if (undoSize() == 0 && mCardViewer.get() != null) {
mCardViewer.get().supportInvalidateOptionsMenu();
}
}
@ -191,8 +241,8 @@ public class Whiteboard extends View {
}
}
private void touchStart(float x, float y) {
private void drawStart(float x, float y) {
mCurrentlyDrawing = true;
mPath.reset();
mPath.moveTo(x, y);
mX = x;
@ -200,7 +250,7 @@ public class Whiteboard extends View {
}
private void touchMove(float x, float y) {
private void drawAlong(float x, float y) {
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
@ -211,10 +261,11 @@ public class Whiteboard extends View {
}
private void touchUp() {
private void drawFinish() {
mCurrentlyDrawing = false;
PathMeasure pm = new PathMeasure(mPath, false);
mPath.lineTo(mX, mY);
if(pm.getLength() > 0){
if (pm.getLength() > 0) {
mCanvas.drawPath(mPath, mPaint);
mUndo.add(mPath);
} else {
@ -224,12 +275,73 @@ public class Whiteboard extends View {
mUndoModeActive = true;
// kill the path so we don't double draw
mPath.reset();
if (mUndo.size() == 1 && mActivity.get() != null) {
mActivity.get().supportInvalidateOptionsMenu();
if (mUndo.size() == 1 && mCardViewer.get() != null) {
mCardViewer.get().supportInvalidateOptionsMenu();
}
}
private void drawAbort() {
drawFinish();
undo();
}
// call this with an ACTION_POINTER_DOWN event to start a new round of detecting drag or tap with
// a second finger
private void reinitializeSecondFinger(MotionEvent event) {
mSecondFingerWithinTapTolerance = true;
mSecondFingerPointerId = event.getPointerId(event.getActionIndex());
mSecondFingerX0 = event.getX(event.findPointerIndex(mSecondFingerPointerId));
mSecondFingerY0 = event.getY(event.findPointerIndex(mSecondFingerPointerId));
}
private boolean updateSecondFinger(MotionEvent event) {
int pointerIndex = event.findPointerIndex(mSecondFingerPointerId);
if (pointerIndex > -1) {
mSecondFingerX = event.getX(pointerIndex);
mSecondFingerY = event.getY(pointerIndex);
float dx = Math.abs(mSecondFingerX0 - mSecondFingerX);
float dy = Math.abs(mSecondFingerY0 - mSecondFingerY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
mSecondFingerWithinTapTolerance = false;
}
return true;
}
return false;
}
// call this with an ACTION_POINTER_UP event to check whether it matches a tap of the second finger
// if so, forward a click action and return true
private boolean trySecondFingerClick(MotionEvent event) {
if (mSecondFingerPointerId == event.getPointerId(event.getActionIndex())) {
updateSecondFinger(event);
AbstractFlashcardViewer cardViewer = mCardViewer.get();
if (mSecondFingerWithinTapTolerance && cardViewer != null) {
cardViewer.tapOnCurrentCard((int) mSecondFingerX, (int) mSecondFingerY);
return true;
}
}
return false;
}
// call this with an ACTION_MOVE event to check whether it is within the threshold for a tap of the second finger
// in this case perform a scroll action
private boolean trySecondFingerScroll(MotionEvent event) {
if (updateSecondFinger(event) && !mSecondFingerWithinTapTolerance) {
int dy = (int) (mSecondFingerY0 - mSecondFingerY);
AbstractFlashcardViewer cardViewer = mCardViewer.get();
if (dy != 0 && cardViewer != null) {
cardViewer.scrollCurrentCardBy(dy);
mSecondFingerX0 = mSecondFingerX;
mSecondFingerY0 = mSecondFingerY;
}
return true;
}
return false;
}
private static int getDisplayHeight() {
Display display = ((WindowManager) AnkiDroidApp.getInstance().getApplicationContext().
getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();