+ * All rights reserved. + *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the distribution. + *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ +public abstract class BaseGestureDetector { + /** + * This value is the threshold ratio between the previous combined pressure + * and the current combined pressure. When pressure decreases rapidly + * between events the position values can often be imprecise, as it usually + * indicates that the user is in the process of lifting a pointer off of the + * device. This value was tuned experimentally. + */ + protected static final float PRESSURE_THRESHOLD = 0.67f; + protected final Context mContext; + protected boolean mGestureInProgress; + protected MotionEvent mPrevEvent; + protected MotionEvent mCurrEvent; + protected float mCurrPressure; + protected float mPrevPressure; + protected long mTimeDelta; + + + public BaseGestureDetector(Context context) { + mContext = context; + } + + /** + * All gesture detectors need to be called through this method to be able to + * detect gestures. This method delegates work to handler methods + * (handleStartProgressEvent, handleInProgressEvent) implemented in + * extending classes. + * + * @param event + * @return + */ + public boolean onTouchEvent(MotionEvent event) { + final int actionCode = event.getAction() & MotionEvent.ACTION_MASK; + if (!mGestureInProgress) { + handleStartProgressEvent(actionCode, event); + } else { + handleInProgressEvent(actionCode, event); + } + return true; + } + + /** + * Called when the current event occurred when NO gesture is in progress + * yet. The handling in this implementation may set the gesture in progress + * (via mGestureInProgress) or out of progress + * + * @param actionCode + * @param event + */ + protected abstract void handleStartProgressEvent(int actionCode, MotionEvent event); + + /** + * Called when the current event occurred when a gesture IS in progress. The + * handling in this implementation may set the gesture out of progress (via + * mGestureInProgress). + * + * @param actionCode + * @param event + */ + protected abstract void handleInProgressEvent(int actionCode, MotionEvent event); + + + protected void updateStateByEvent(MotionEvent curr) { + final MotionEvent prev = mPrevEvent; + + // Reset mCurrEvent + if (mCurrEvent != null) { + mCurrEvent.recycle(); + mCurrEvent = null; + } + mCurrEvent = MotionEvent.obtain(curr); + + + // Delta time + mTimeDelta = curr.getEventTime() - prev.getEventTime(); + + // Pressure + mCurrPressure = curr.getPressure(curr.getActionIndex()); + mPrevPressure = prev.getPressure(prev.getActionIndex()); + } + + protected void resetState() { + if (mPrevEvent != null) { + mPrevEvent.recycle(); + mPrevEvent = null; + } + if (mCurrEvent != null) { + mCurrEvent.recycle(); + mCurrEvent = null; + } + mGestureInProgress = false; + } + + + /** + * Returns {@code true} if a gesture is currently in progress. + * + * @return {@code true} if a gesture is currently in progress, {@code false} otherwise. + */ + public boolean isInProgress() { + return mGestureInProgress; + } + + /** + * Return the time difference in milliseconds between the previous accepted + * GestureDetector event and the current GestureDetector event. + * + * @return Time difference since the last move event in milliseconds. + */ + public long getTimeDelta() { + return mTimeDelta; + } + + /** + * Return the event time of the current GestureDetector event being + * processed. + * + * @return Current GestureDetector event time in milliseconds. + */ + public long getEventTime() { + return mCurrEvent.getEventTime(); + } + +} diff --git a/src/org/thoughtcrime/securesms/scribbles/multitouch/MoveGestureDetector.java b/src/org/thoughtcrime/securesms/scribbles/multitouch/MoveGestureDetector.java new file mode 100644 index 0000000000..f623a202a6 --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/multitouch/MoveGestureDetector.java @@ -0,0 +1,170 @@ +package org.thoughtcrime.securesms.scribbles.multitouch; + +import android.content.Context; +import android.graphics.PointF; +import android.view.MotionEvent; + +/** + * @author Almer Thie (code.almeros.com) + * Copyright (c) 2013, Almer Thie (code.almeros.com) + *
+ * All rights reserved. + *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the distribution. + *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ +public class MoveGestureDetector extends BaseGestureDetector { + + private static final PointF FOCUS_DELTA_ZERO = new PointF(); + private final OnMoveGestureListener mListener; + private PointF mCurrFocusInternal; + private PointF mPrevFocusInternal; + private PointF mFocusExternal = new PointF(); + private PointF mFocusDeltaExternal = new PointF(); + public MoveGestureDetector(Context context, OnMoveGestureListener listener) { + super(context); + mListener = listener; + } + + @Override + protected void handleStartProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_DOWN: + resetState(); // In case we missed an UP/CANCEL event + + mPrevEvent = MotionEvent.obtain(event); + mTimeDelta = 0; + + updateStateByEvent(event); + break; + + case MotionEvent.ACTION_MOVE: + mGestureInProgress = mListener.onMoveBegin(this); + break; + } + } + + @Override + protected void handleInProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mListener.onMoveEnd(this); + resetState(); + break; + + case MotionEvent.ACTION_MOVE: + updateStateByEvent(event); + + // Only accept the event if our relative pressure is within + // a certain limit. This can help filter shaky data as a + // finger is lifted. + if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) { + final boolean updatePrevious = mListener.onMove(this); + if (updatePrevious) { + mPrevEvent.recycle(); + mPrevEvent = MotionEvent.obtain(event); + } + } + break; + } + } + + protected void updateStateByEvent(MotionEvent curr) { + super.updateStateByEvent(curr); + + final MotionEvent prev = mPrevEvent; + + // Focus intenal + mCurrFocusInternal = determineFocalPoint(curr); + mPrevFocusInternal = determineFocalPoint(prev); + + // Focus external + // - Prevent skipping of focus delta when a finger is added or removed + boolean mSkipNextMoveEvent = prev.getPointerCount() != curr.getPointerCount(); + mFocusDeltaExternal = mSkipNextMoveEvent ? FOCUS_DELTA_ZERO : new PointF(mCurrFocusInternal.x - mPrevFocusInternal.x, mCurrFocusInternal.y - mPrevFocusInternal.y); + + // - Don't directly use mFocusInternal (or skipping will occur). Add + // unskipped delta values to mFocusExternal instead. + mFocusExternal.x += mFocusDeltaExternal.x; + mFocusExternal.y += mFocusDeltaExternal.y; + } + + /** + * Determine (multi)finger focal point (a.k.a. center point between all + * fingers) + * + * @return PointF focal point + */ + private PointF determineFocalPoint(MotionEvent e) { + // Number of fingers on screen + final int pCount = e.getPointerCount(); + float x = 0f; + float y = 0f; + + for (int i = 0; i < pCount; i++) { + x += e.getX(i); + y += e.getY(i); + } + + return new PointF(x / pCount, y / pCount); + } + + public float getFocusX() { + return mFocusExternal.x; + } + + public float getFocusY() { + return mFocusExternal.y; + } + + public PointF getFocusDelta() { + return mFocusDeltaExternal; + } + + /** + * Listener which must be implemented which is used by MoveGestureDetector + * to perform callbacks to any implementing class which is registered to a + * MoveGestureDetector via the constructor. + * + * @see MoveGestureDetector.SimpleOnMoveGestureListener + */ + public interface OnMoveGestureListener { + public boolean onMove(MoveGestureDetector detector); + + public boolean onMoveBegin(MoveGestureDetector detector); + + public void onMoveEnd(MoveGestureDetector detector); + } + + /** + * Helper class which may be extended and where the methods may be + * implemented. This way it is not necessary to implement all methods + * of OnMoveGestureListener. + */ + public static class SimpleOnMoveGestureListener implements OnMoveGestureListener { + public boolean onMove(MoveGestureDetector detector) { + return false; + } + + public boolean onMoveBegin(MoveGestureDetector detector) { + return true; + } + + public void onMoveEnd(MoveGestureDetector detector) { + // Do nothing, overridden implementation may be used + } + } + +} diff --git a/src/org/thoughtcrime/securesms/scribbles/multitouch/RotateGestureDetector.java b/src/org/thoughtcrime/securesms/scribbles/multitouch/RotateGestureDetector.java new file mode 100644 index 0000000000..839f7b658b --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/multitouch/RotateGestureDetector.java @@ -0,0 +1,170 @@ +package org.thoughtcrime.securesms.scribbles.multitouch; + +import android.content.Context; +import android.view.MotionEvent; + +/** + * @author Almer Thie (code.almeros.com) + * Copyright (c) 2013, Almer Thie (code.almeros.com) + *
+ * All rights reserved. + *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the distribution. + *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ +public class RotateGestureDetector extends TwoFingerGestureDetector { + + private static final String TAG = RotateGestureDetector.class.getName(); + private final OnRotateGestureListener mListener; + private boolean mSloppyGesture; + + + public RotateGestureDetector(Context context, OnRotateGestureListener listener) { + super(context); + mListener = listener; + } + + @Override + protected void handleStartProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_POINTER_DOWN: + // At least the second finger is on screen now + + resetState(); // In case we missed an UP/CANCEL event + mPrevEvent = MotionEvent.obtain(event); + mTimeDelta = 0; + + updateStateByEvent(event); + + // See if we have a sloppy gesture + mSloppyGesture = isSloppyGesture(event); + if (!mSloppyGesture) { + // No, start gesture now + mGestureInProgress = mListener.onRotateBegin(this); + } + break; + + case MotionEvent.ACTION_MOVE: + if (!mSloppyGesture) { + break; + } + + // See if we still have a sloppy gesture + mSloppyGesture = isSloppyGesture(event); + if (!mSloppyGesture) { + // No, start normal gesture now + mGestureInProgress = mListener.onRotateBegin(this); + } + + break; + + case MotionEvent.ACTION_POINTER_UP: + if (!mSloppyGesture) { + break; + } + + break; + } + } + + @Override + protected void handleInProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_POINTER_UP: + // Gesture ended but + updateStateByEvent(event); + + if (!mSloppyGesture) { + mListener.onRotateEnd(this); + } + + resetState(); + break; + + case MotionEvent.ACTION_CANCEL: + if (!mSloppyGesture) { + mListener.onRotateEnd(this); + } + + resetState(); + break; + + case MotionEvent.ACTION_MOVE: + updateStateByEvent(event); + + // Only accept the event if our relative pressure is within + // a certain limit. This can help filter shaky data as a + // finger is lifted. + if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) { + final boolean updatePrevious = mListener.onRotate(this); + if (updatePrevious) { + mPrevEvent.recycle(); + mPrevEvent = MotionEvent.obtain(event); + } + } + break; + } + } + + @Override + protected void resetState() { + super.resetState(); + mSloppyGesture = false; + } + + /** + * Return the rotation difference from the previous rotate event to the current + * event. + * + * @return The current rotation //difference in degrees. + */ + public float getRotationDegreesDelta() { + double diffRadians = Math.atan2(mPrevFingerDiffY, mPrevFingerDiffX) - Math.atan2(mCurrFingerDiffY, mCurrFingerDiffX); + return (float) (diffRadians * 180 / Math.PI); + } + + /** + * Listener which must be implemented which is used by RotateGestureDetector + * to perform callbacks to any implementing class which is registered to a + * RotateGestureDetector via the constructor. + * + * @see RotateGestureDetector.SimpleOnRotateGestureListener + */ + public interface OnRotateGestureListener { + public boolean onRotate(RotateGestureDetector detector); + + public boolean onRotateBegin(RotateGestureDetector detector); + + public void onRotateEnd(RotateGestureDetector detector); + } + + /** + * Helper class which may be extended and where the methods may be + * implemented. This way it is not necessary to implement all methods + * of OnRotateGestureListener. + */ + public static class SimpleOnRotateGestureListener implements OnRotateGestureListener { + public boolean onRotate(RotateGestureDetector detector) { + return false; + } + + public boolean onRotateBegin(RotateGestureDetector detector) { + return true; + } + + public void onRotateEnd(RotateGestureDetector detector) { + // Do nothing, overridden implementation may be used + } + } +} diff --git a/src/org/thoughtcrime/securesms/scribbles/multitouch/ShoveGestureDetector.java b/src/org/thoughtcrime/securesms/scribbles/multitouch/ShoveGestureDetector.java new file mode 100644 index 0000000000..cb4eb339e1 --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/multitouch/ShoveGestureDetector.java @@ -0,0 +1,201 @@ +package org.thoughtcrime.securesms.scribbles.multitouch; + +import android.content.Context; +import android.view.MotionEvent; + +/** + * @author Robert Nordan (robert.nordan@norkart.no) + *
+ * Copyright (c) 2013, Norkart AS + *
+ * All rights reserved. + *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the distribution. + *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ +public class ShoveGestureDetector extends TwoFingerGestureDetector { + + private final OnShoveGestureListener mListener; + private float mPrevAverageY; + private float mCurrAverageY; + private boolean mSloppyGesture; + + public ShoveGestureDetector(Context context, OnShoveGestureListener listener) { + super(context); + mListener = listener; + } + + @Override + protected void handleStartProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_POINTER_DOWN: + // At least the second finger is on screen now + + resetState(); // In case we missed an UP/CANCEL event + mPrevEvent = MotionEvent.obtain(event); + mTimeDelta = 0; + + updateStateByEvent(event); + + // See if we have a sloppy gesture + mSloppyGesture = isSloppyGesture(event); + if (!mSloppyGesture) { + // No, start gesture now + mGestureInProgress = mListener.onShoveBegin(this); + } + break; + + case MotionEvent.ACTION_MOVE: + if (!mSloppyGesture) { + break; + } + + // See if we still have a sloppy gesture + mSloppyGesture = isSloppyGesture(event); + if (!mSloppyGesture) { + // No, start normal gesture now + mGestureInProgress = mListener.onShoveBegin(this); + } + + break; + + case MotionEvent.ACTION_POINTER_UP: + if (!mSloppyGesture) { + break; + } + + break; + } + } + + @Override + protected void handleInProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_POINTER_UP: + // Gesture ended but + updateStateByEvent(event); + + if (!mSloppyGesture) { + mListener.onShoveEnd(this); + } + + resetState(); + break; + + case MotionEvent.ACTION_CANCEL: + if (!mSloppyGesture) { + mListener.onShoveEnd(this); + } + + resetState(); + break; + + case MotionEvent.ACTION_MOVE: + updateStateByEvent(event); + + // Only accept the event if our relative pressure is within + // a certain limit. This can help filter shaky data as a + // finger is lifted. Also check that shove is meaningful. + if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD + && Math.abs(getShovePixelsDelta()) > 0.5f) { + final boolean updatePrevious = mListener.onShove(this); + if (updatePrevious) { + mPrevEvent.recycle(); + mPrevEvent = MotionEvent.obtain(event); + } + } + break; + } + } + + @Override + protected void updateStateByEvent(MotionEvent curr) { + super.updateStateByEvent(curr); + + final MotionEvent prev = mPrevEvent; + float py0 = prev.getY(0); + float py1 = prev.getY(1); + mPrevAverageY = (py0 + py1) / 2.0f; + + float cy0 = curr.getY(0); + float cy1 = curr.getY(1); + mCurrAverageY = (cy0 + cy1) / 2.0f; + } + + @Override + protected boolean isSloppyGesture(MotionEvent event) { + boolean sloppy = super.isSloppyGesture(event); + if (sloppy) + return true; + + // If it's not traditionally sloppy, we check if the angle between fingers + // is acceptable. + double angle = Math.abs(Math.atan2(mCurrFingerDiffY, mCurrFingerDiffX)); + //about 20 degrees, left or right + return !((0.0f < angle && angle < 0.35f) + || 2.79f < angle && angle < Math.PI); + } + + /** + * Return the distance in pixels from the previous shove event to the current + * event. + * + * @return The current distance in pixels. + */ + public float getShovePixelsDelta() { + return mCurrAverageY - mPrevAverageY; + } + + @Override + protected void resetState() { + super.resetState(); + mSloppyGesture = false; + mPrevAverageY = 0.0f; + mCurrAverageY = 0.0f; + } + + /** + * Listener which must be implemented which is used by ShoveGestureDetector + * to perform callbacks to any implementing class which is registered to a + * ShoveGestureDetector via the constructor. + * + * @see ShoveGestureDetector.SimpleOnShoveGestureListener + */ + public interface OnShoveGestureListener { + public boolean onShove(ShoveGestureDetector detector); + + public boolean onShoveBegin(ShoveGestureDetector detector); + + public void onShoveEnd(ShoveGestureDetector detector); + } + + /** + * Helper class which may be extended and where the methods may be + * implemented. This way it is not necessary to implement all methods + * of OnShoveGestureListener. + */ + public static class SimpleOnShoveGestureListener implements OnShoveGestureListener { + public boolean onShove(ShoveGestureDetector detector) { + return false; + } + + public boolean onShoveBegin(ShoveGestureDetector detector) { + return true; + } + + public void onShoveEnd(ShoveGestureDetector detector) { + // Do nothing, overridden implementation may be used + } + } +} diff --git a/src/org/thoughtcrime/securesms/scribbles/multitouch/TwoFingerGestureDetector.java b/src/org/thoughtcrime/securesms/scribbles/multitouch/TwoFingerGestureDetector.java new file mode 100644 index 0000000000..ef94a9e63c --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/multitouch/TwoFingerGestureDetector.java @@ -0,0 +1,186 @@ +package org.thoughtcrime.securesms.scribbles.multitouch; + +import android.content.Context; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +/** + * @author Almer Thie (code.almeros.com) + * Copyright (c) 2013, Almer Thie (code.almeros.com) + *
+ * All rights reserved. + *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the distribution. + *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+ * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+ * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
+ * OF SUCH DAMAGE.
+ */
+public abstract class TwoFingerGestureDetector extends BaseGestureDetector {
+
+ private final float mEdgeSlop;
+ protected float mPrevFingerDiffX;
+ protected float mPrevFingerDiffY;
+ protected float mCurrFingerDiffX;
+ protected float mCurrFingerDiffY;
+ private float mRightSlopEdge;
+ private float mBottomSlopEdge;
+ private float mCurrLen;
+ private float mPrevLen;
+
+ public TwoFingerGestureDetector(Context context) {
+ super(context);
+
+ ViewConfiguration config = ViewConfiguration.get(context);
+ mEdgeSlop = config.getScaledEdgeSlop();
+ }
+
+ @Override
+ protected abstract void handleStartProgressEvent(int actionCode, MotionEvent event);
+
+ @Override
+ protected abstract void handleInProgressEvent(int actionCode, MotionEvent event);
+
+ protected void updateStateByEvent(MotionEvent curr) {
+ super.updateStateByEvent(curr);
+
+ final MotionEvent prev = mPrevEvent;
+
+ mCurrLen = -1;
+ mPrevLen = -1;
+
+ // Previous
+ final float px0 = prev.getX(0);
+ final float py0 = prev.getY(0);
+ final float px1 = prev.getX(1);
+ final float py1 = prev.getY(1);
+ final float pvx = px1 - px0;
+ final float pvy = py1 - py0;
+ mPrevFingerDiffX = pvx;
+ mPrevFingerDiffY = pvy;
+
+ // Current
+ final float cx0 = curr.getX(0);
+ final float cy0 = curr.getY(0);
+ final float cx1 = curr.getX(1);
+ final float cy1 = curr.getY(1);
+ final float cvx = cx1 - cx0;
+ final float cvy = cy1 - cy0;
+ mCurrFingerDiffX = cvx;
+ mCurrFingerDiffY = cvy;
+ }
+
+ /**
+ * Return the current distance between the two pointers forming the
+ * gesture in progress.
+ *
+ * @return Distance between pointers in pixels.
+ */
+ public float getCurrentSpan() {
+ if (mCurrLen == -1) {
+ final float cvx = mCurrFingerDiffX;
+ final float cvy = mCurrFingerDiffY;
+ mCurrLen = (float) Math.sqrt(cvx * cvx + cvy * cvy);
+ }
+ return mCurrLen;
+ }
+
+ /**
+ * Return the previous distance between the two pointers forming the
+ * gesture in progress.
+ *
+ * @return Previous distance between pointers in pixels.
+ */
+ public float getPreviousSpan() {
+ if (mPrevLen == -1) {
+ final float pvx = mPrevFingerDiffX;
+ final float pvy = mPrevFingerDiffY;
+ mPrevLen = (float) Math.sqrt(pvx * pvx + pvy * pvy);
+ }
+ return mPrevLen;
+ }
+
+ /**
+ * Check if we have a sloppy gesture. Sloppy gestures can happen if the edge
+ * of the user's hand is touching the screen, for example.
+ *
+ * @param event
+ * @return
+ */
+ protected boolean isSloppyGesture(MotionEvent event) {
+ // As orientation can change, query the metrics in touch down
+ DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
+ mRightSlopEdge = metrics.widthPixels - mEdgeSlop;
+ mBottomSlopEdge = metrics.heightPixels - mEdgeSlop;
+
+ final float edgeSlop = mEdgeSlop;
+ final float rightSlop = mRightSlopEdge;
+ final float bottomSlop = mBottomSlopEdge;
+
+ final float x0 = event.getRawX();
+ final float y0 = event.getRawY();
+ final float x1 = getRawX(event, 1);
+ final float y1 = getRawY(event, 1);
+
+
+ Log.w("TwoFinger",
+ String.format("x0: %f, y0: %f, x1: %f, y1: %f, EdgeSlop: %f, RightSlop: %f, BottomSlop: %f",
+ x0, y0, x1, y1, edgeSlop, rightSlop, bottomSlop));
+
+
+ boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop
+ || x0 > rightSlop || y0 > bottomSlop;
+ boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop
+ || x1 > rightSlop || y1 > bottomSlop;
+
+ if (p0sloppy && p1sloppy) {
+ return true;
+ } else if (p0sloppy) {
+ return true;
+ } else if (p1sloppy) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * MotionEvent has no getRawX(int) method; simulate it pending future API approval.
+ *
+ * @param event
+ * @param pointerIndex
+ * @return
+ */
+ protected static float getRawX(MotionEvent event, int pointerIndex) {
+ float offset = event.getX() - event.getRawX();
+ if (pointerIndex < event.getPointerCount()) {
+ return event.getX(pointerIndex) + offset;
+ }
+ return 0f;
+ }
+
+ /**
+ * MotionEvent has no getRawY(int) method; simulate it pending future API approval.
+ *
+ * @param event
+ * @param pointerIndex
+ * @return
+ */
+ protected static float getRawY(MotionEvent event, int pointerIndex) {
+ float offset = Math.abs(event.getY() - event.getRawY());
+ if (pointerIndex < event.getPointerCount()) {
+ return event.getY(pointerIndex) + offset;
+ }
+ return 0f;
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/scribbles/viewmodel/Font.java b/src/org/thoughtcrime/securesms/scribbles/viewmodel/Font.java
new file mode 100644
index 0000000000..2de6decf48
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/scribbles/viewmodel/Font.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2016 UPTech
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package org.thoughtcrime.securesms.scribbles.viewmodel;
+
+
+public class Font {
+
+ /**
+ * color value (ex: 0xFF00FF)
+ */
+ private int color;
+ /**
+ * name of the font
+ */
+ private String typeface;
+ /**
+ * size of the font, relative to parent
+ */
+ private float size;
+
+ public Font() {
+ }
+
+ public void increaseSize(float diff) {
+ if (size + diff <= Limits.MAX_FONT_SIZE) {
+ size = size + diff;
+ }
+ }
+
+ public void decreaseSize(float diff) {
+ if (size - diff >= Limits.MIN_FONT_SIZE) {
+ size = size - diff;
+ }
+ }
+
+ public int getColor() {
+ return color;
+ }
+
+ public void setColor(int color) {
+ this.color = color;
+ }
+
+ public String getTypeface() {
+ return typeface;
+ }
+
+ public void setTypeface(String typeface) {
+ this.typeface = typeface;
+ }
+
+ public float getSize() {
+ return size;
+ }
+
+ public void setSize(float size) {
+ this.size = size;
+ }
+
+ private interface Limits {
+ float MIN_FONT_SIZE = 0.01F;
+ float MAX_FONT_SIZE = 0.46F;
+ }
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/scribbles/viewmodel/Layer.java b/src/org/thoughtcrime/securesms/scribbles/viewmodel/Layer.java
new file mode 100644
index 0000000000..3b49bd8524
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/scribbles/viewmodel/Layer.java
@@ -0,0 +1,141 @@
+/**
+ * Copyright (c) 2016 UPTech
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.thoughtcrime.securesms.scribbles.viewmodel;
+
+import android.support.annotation.FloatRange;
+import android.util.Log;
+
+public class Layer {
+
+ /**
+ * rotation relative to the layer center, in degrees
+ */
+ @FloatRange(from = 0.0F, to = 360.0F)
+ private float rotationInDegrees;
+
+ private float scale;
+ /**
+ * top left X coordinate, relative to parent canvas
+ */
+ private float x;
+ /**
+ * top left Y coordinate, relative to parent canvas
+ */
+ private float y;
+ /**
+ * is layer flipped horizontally (by X-coordinate)
+ */
+ private boolean isFlipped;
+
+ public Layer() {
+ reset();
+ }
+
+ protected void reset() {
+ this.rotationInDegrees = 0.0F;
+ this.scale = 1.0F;
+ this.isFlipped = false;
+ this.x = 0.0F;
+ this.y = 0.0F;
+ }
+
+ public void postScale(float scaleDiff) {
+ Log.w("Layer", "ScaleDiff: " + scaleDiff);
+ float newVal = scale + scaleDiff;
+ if (newVal >= getMinScale() && newVal <= getMaxScale()) {
+ scale = newVal;
+ }
+ }
+
+ protected float getMaxScale() {
+ return Limits.MAX_SCALE;
+ }
+
+ protected float getMinScale() {
+ return Limits.MIN_SCALE;
+ }
+
+ public void postRotate(float rotationInDegreesDiff) {
+ this.rotationInDegrees += rotationInDegreesDiff;
+ this.rotationInDegrees %= 360.0F;
+ }
+
+ public void postTranslate(float dx, float dy) {
+ this.x += dx;
+ this.y += dy;
+ }
+
+ public void flip() {
+ this.isFlipped = !isFlipped;
+ }
+
+ public float initialScale() {
+ return Limits.INITIAL_ENTITY_SCALE;
+ }
+
+ public float getRotationInDegrees() {
+ return rotationInDegrees;
+ }
+
+ public void setRotationInDegrees(@FloatRange(from = 0.0, to = 360.0) float rotationInDegrees) {
+ this.rotationInDegrees = rotationInDegrees;
+ }
+
+ public float getScale() {
+ return scale;
+ }
+
+ public void setScale(float scale) {
+ this.scale = scale;
+ }
+
+ public float getX() {
+ return x;
+ }
+
+ public void setX(float x) {
+ this.x = x;
+ }
+
+ public float getY() {
+ return y;
+ }
+
+ public void setY(float y) {
+ this.y = y;
+ }
+
+ public boolean isFlipped() {
+ return isFlipped;
+ }
+
+ public void setFlipped(boolean flipped) {
+ isFlipped = flipped;
+ }
+
+ interface Limits {
+ float MIN_SCALE = 0.06F;
+ float MAX_SCALE = 4.0F;
+ float INITIAL_ENTITY_SCALE = 0.4F;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/scribbles/viewmodel/TextLayer.java b/src/org/thoughtcrime/securesms/scribbles/viewmodel/TextLayer.java
new file mode 100644
index 0000000000..c367510827
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/scribbles/viewmodel/TextLayer.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2016 UPTech
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.thoughtcrime.securesms.scribbles.viewmodel;
+
+import android.util.Log;
+
+public class TextLayer extends Layer {
+
+ private String text;
+ private Font font;
+
+ public TextLayer() {
+ }
+
+ @Override
+ protected void reset() {
+ super.reset();
+ this.text = "";
+ this.font = new Font();
+ }
+
+ @Override
+ protected float getMaxScale() {
+ return Limits.MAX_SCALE;
+ }
+
+ @Override
+ protected float getMinScale() {
+ return Limits.MIN_SCALE;
+ }
+
+ @Override
+ public float initialScale() {
+ return Limits.INITIAL_SCALE;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ public Font getFont() {
+ return font;
+ }
+
+ public void setFont(Font font) {
+ this.font = font;
+ }
+
+ @Override
+ public void postScale(float scaleDiff) {
+ if (scaleDiff > 0) font.increaseSize(scaleDiff);
+ else if (scaleDiff < 0) font.decreaseSize(Math.abs(scaleDiff));
+ }
+
+ public interface Limits {
+ /**
+ * limit text size to view bounds
+ * so that users don't put small font size and scale it 100+ times
+ */
+ float MAX_SCALE = 1.0F;
+ float MIN_SCALE = 0.2F;
+
+ float MIN_BITMAP_HEIGHT = 0.13F;
+
+ float FONT_SIZE_STEP = 0.008F;
+
+ float INITIAL_FONT_SIZE = 0.075F;
+ int INITIAL_FONT_COLOR = 0xff000000;
+
+ float INITIAL_SCALE = 0.8F; // set the same to avoid text scaling
+ }
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/scribbles/widget/CanvasView.java b/src/org/thoughtcrime/securesms/scribbles/widget/CanvasView.java
new file mode 100644
index 0000000000..79a393a42f
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/scribbles/widget/CanvasView.java
@@ -0,0 +1,901 @@
+/**
+ * CanvasView.java
+ *
+ * Copyright (c) 2014 Tomohiro IKEDA (Korilakkuma)
+ * Released under the MIT license
+ */
+
+package org.thoughtcrime.securesms.scribbles.widget;
+
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class defines fields and methods for drawing.
+ */
+public class CanvasView extends View {
+
+ // Enumeration for Mode
+ public enum Mode {
+ DRAW,
+ TEXT,
+ ERASER;
+ }
+
+ // Enumeration for Drawer
+ public enum Drawer {
+ PEN,
+ LINE,
+ RECTANGLE,
+ CIRCLE,
+ ELLIPSE,
+ QUADRATIC_BEZIER,
+ QUBIC_BEZIER;
+ }
+
+ private Context context = null;
+ private Canvas canvas = null;
+ private Bitmap bitmap = null;
+
+ private List
+ * The correct order of applying transformations is : L = S * R * T
+ *
+ * See more info: Game Dev: Transform Matrix multiplication order
+ *
+ * Preconcat works like M` = M * S, so we apply preScale -> preRotate -> preTranslate
+ * the result will be the same: L = S * R * T
+ *
+ * NOTE: postconcat (postScale, etc.) works the other way : M` = S * M, in order to use it
+ * we'd need to reverse the order of applying
+ * transformations : post holy scale -> postTranslate -> postRotate -> postScale
+ */
+ protected void updateMatrix() {
+ // init matrix to E - identity matrix
+ matrix.reset();
+
+ float widthAspect = 1.0F * canvasWidth / getWidth();
+ float heightAspect = 1.0F * canvasHeight / getHeight();
+ // fit the smallest size
+ holyScale = Math.min(widthAspect, heightAspect);
+
+ float topLeftX = layer.getX() * canvasWidth;
+ float topLeftY = layer.getY() * canvasHeight;
+
+ float centerX = topLeftX + getWidth() * holyScale * 0.5F;
+ float centerY = topLeftY + getHeight() * holyScale * 0.5F;
+
+ // calculate params
+ float rotationInDegree = layer.getRotationInDegrees();
+ float scaleX = layer.getScale();
+ float scaleY = layer.getScale();
+ if (layer.isFlipped()) {
+ // flip (by X-coordinate) if needed
+ rotationInDegree *= -1.0F;
+ scaleX *= -1.0F;
+ }
+
+ // applying transformations : L = S * R * T
+
+ // scale
+ matrix.preScale(scaleX, scaleY, centerX, centerY);
+
+ // rotate
+ matrix.preRotate(rotationInDegree, centerX, centerY);
+
+ // translate
+ matrix.preTranslate(topLeftX, topLeftY);
+
+ // applying holy scale - S`, the result will be : L = S * R * T * S`
+ matrix.preScale(holyScale, holyScale);
+ }
+
+ public float absoluteCenterX() {
+ float topLeftX = layer.getX() * canvasWidth;
+ return topLeftX + getWidth() * holyScale * 0.5F;
+ }
+
+ public float absoluteCenterY() {
+ float topLeftY = layer.getY() * canvasHeight;
+
+ return topLeftY + getHeight() * holyScale * 0.5F;
+ }
+
+ public PointF absoluteCenter() {
+ float topLeftX = layer.getX() * canvasWidth;
+ float topLeftY = layer.getY() * canvasHeight;
+
+ float centerX = topLeftX + getWidth() * holyScale * 0.5F;
+ float centerY = topLeftY + getHeight() * holyScale * 0.5F;
+
+ return new PointF(centerX, centerY);
+ }
+
+ public void moveToCanvasCenter() {
+ moveCenterTo(new PointF(canvasWidth * 0.5F, canvasHeight * 0.5F));
+ }
+
+ public void moveCenterTo(PointF moveToCenter) {
+ PointF currentCenter = absoluteCenter();
+ layer.postTranslate(1.0F * (moveToCenter.x - currentCenter.x) / canvasWidth,
+ 1.0F * (moveToCenter.y - currentCenter.y) / canvasHeight);
+ }
+
+ private final PointF pA = new PointF();
+ private final PointF pB = new PointF();
+ private final PointF pC = new PointF();
+ private final PointF pD = new PointF();
+
+ /**
+ * For more info:
+ * StackOverflow: How to check point is in rectangle
+ * NOTE: it's easier to apply the same transformation matrix (calculated before) to the original source points, rather than
+ * calculate the result points ourselves
+ * @param point point
+ * @return true if point (x, y) is inside the triangle
+ */
+ public boolean pointInLayerRect(PointF point) {
+
+ updateMatrix();
+ // map rect vertices
+ matrix.mapPoints(destPoints, srcPoints);
+
+ pA.x = destPoints[0];
+ pA.y = destPoints[1];
+ pB.x = destPoints[2];
+ pB.y = destPoints[3];
+ pC.x = destPoints[4];
+ pC.y = destPoints[5];
+ pD.x = destPoints[6];
+ pD.y = destPoints[7];
+
+ return MathUtils.pointInTriangle(point, pA, pB, pC) || MathUtils.pointInTriangle(point, pA, pD, pC);
+ }
+
+ /**
+ * http://judepereira.com/blog/calculate-the-real-scale-factor-and-the-angle-of-rotation-from-an-android-matrix/
+ *
+ * @param canvas Canvas to draw
+ * @param drawingPaint Paint to use during drawing
+ */
+ public final void draw(@NonNull Canvas canvas, @Nullable Paint drawingPaint) {
+
+ this.canvasWidth = canvas.getWidth();
+ this.canvasHeight = canvas.getHeight();
+
+ updateMatrix();
+
+ canvas.save();
+
+ drawContent(canvas, drawingPaint);
+
+ if (isSelected()) {
+ // get alpha from drawingPaint
+ int storedAlpha = borderPaint.getAlpha();
+ if (drawingPaint != null) {
+ borderPaint.setAlpha(drawingPaint.getAlpha());
+ }
+ drawSelectedBg(canvas);
+ // restore border alpha
+ borderPaint.setAlpha(storedAlpha);
+ }
+
+ canvas.restore();
+ }
+
+ private void drawSelectedBg(Canvas canvas) {
+ matrix.mapPoints(destPoints, srcPoints);
+ //noinspection Range
+ canvas.drawLines(destPoints, 0, 8, borderPaint);
+ //noinspection Range
+ canvas.drawLines(destPoints, 2, 8, borderPaint);
+ }
+
+ @NonNull
+ public Layer getLayer() {
+ return layer;
+ }
+
+ public void setBorderPaint(@NonNull Paint borderPaint) {
+ this.borderPaint = borderPaint;
+ }
+
+ protected abstract void drawContent(@NonNull Canvas canvas, @Nullable Paint drawingPaint);
+
+ public abstract int getWidth();
+
+ public abstract int getHeight();
+
+ public void release() {
+ // free resources here
+ }
+
+ public void updateEntity() {}
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ release();
+ } finally {
+ //noinspection ThrowFromFinallyBlock
+ super.finalize();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/scribbles/widget/entity/TextEntity.java b/src/org/thoughtcrime/securesms/scribbles/widget/entity/TextEntity.java
new file mode 100644
index 0000000000..73bfd315ea
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/scribbles/widget/entity/TextEntity.java
@@ -0,0 +1,189 @@
+/**
+ * Copyright (c) 2016 UPTech
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package org.thoughtcrime.securesms.scribbles.widget.entity;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+
+import org.thoughtcrime.securesms.scribbles.viewmodel.TextLayer;
+
+
+public class TextEntity extends MotionEntity {
+
+ private final TextPaint textPaint;
+
+ @Nullable
+ private Bitmap bitmap;
+
+ public TextEntity(@NonNull TextLayer textLayer,
+ @IntRange(from = 1) int canvasWidth,
+ @IntRange(from = 1) int canvasHeight)
+ {
+ super(textLayer, canvasWidth, canvasHeight);
+ this.textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+
+ updateEntity(false);
+ }
+
+ private void updateEntity(boolean moveToPreviousCenter) {
+ // save previous center
+ PointF oldCenter = absoluteCenter();
+
+ Bitmap newBmp = createBitmap(getLayer(), bitmap);
+
+ // recycle previous bitmap (if not reused) as soon as possible
+ if (bitmap != null && bitmap != newBmp && !bitmap.isRecycled()) {
+ bitmap.recycle();
+ }
+
+ this.bitmap = newBmp;
+
+ float width = bitmap.getWidth();
+ float height = bitmap.getHeight();
+
+ @SuppressWarnings("UnnecessaryLocalVariable")
+ float widthAspect = 1.0F * canvasWidth / width;
+
+ // for text we always match text width with parent width
+ this.holyScale = widthAspect;
+
+ // initial position of the entity
+ srcPoints[0] = 0;
+ srcPoints[1] = 0;
+ srcPoints[2] = width;
+ srcPoints[3] = 0;
+ srcPoints[4] = width;
+ srcPoints[5] = height;
+ srcPoints[6] = 0;
+ srcPoints[7] = height;
+ srcPoints[8] = 0;
+ srcPoints[8] = 0;
+
+ if (moveToPreviousCenter) {
+ // move to previous center
+ moveCenterTo(oldCenter);
+ }
+ }
+
+ /**
+ * If reuseBmp is not null, and size of the new bitmap matches the size of the reuseBmp,
+ * new bitmap won't be created, reuseBmp it will be reused instead
+ *
+ * @param textLayer text to draw
+ * @param reuseBmp the bitmap that will be reused
+ * @return bitmap with the text
+ */
+ @NonNull
+ private Bitmap createBitmap(@NonNull TextLayer textLayer, @Nullable Bitmap reuseBmp) {
+
+ int boundsWidth = canvasWidth;
+
+ // init params - size, color, typeface
+ textPaint.setStyle(Paint.Style.FILL);
+ textPaint.setTextSize(textLayer.getFont().getSize() * canvasWidth);
+ textPaint.setColor(textLayer.getFont().getColor());
+// textPaint.setTypeface(fontProvider.getTypeface(textLayer.getFont().getTypeface()));
+
+ // drawing text guide : http://ivankocijan.xyz/android-drawing-multiline-text-on-canvas/
+ // Static layout which will be drawn on canvas
+ StaticLayout sl = new StaticLayout(
+ textLayer.getText(), // - text which will be drawn
+ textPaint,
+ boundsWidth, // - width of the layout
+ Layout.Alignment.ALIGN_CENTER, // - layout alignment
+ 1, // 1 - text spacing multiply
+ 1, // 1 - text spacing add
+ true); // true - include padding
+
+ // calculate height for the entity, min - Limits.MIN_BITMAP_HEIGHT
+ int boundsHeight = sl.getHeight();
+
+ // create bitmap not smaller than TextLayer.Limits.MIN_BITMAP_HEIGHT
+ int bmpHeight = (int) (canvasHeight * Math.max(TextLayer.Limits.MIN_BITMAP_HEIGHT,
+ 1.0F * boundsHeight / canvasHeight));
+
+ // create bitmap where text will be drawn
+ Bitmap bmp;
+ if (reuseBmp != null && reuseBmp.getWidth() == boundsWidth
+ && reuseBmp.getHeight() == bmpHeight) {
+ // if previous bitmap exists, and it's width/height is the same - reuse it
+ bmp = reuseBmp;
+ bmp.eraseColor(Color.TRANSPARENT); // erase color when reusing
+ } else {
+ bmp = Bitmap.createBitmap(boundsWidth, bmpHeight, Bitmap.Config.ARGB_8888);
+ }
+
+ Canvas canvas = new Canvas(bmp);
+ canvas.save();
+
+ // move text to center if bitmap is bigger that text
+ if (boundsHeight < bmpHeight) {
+ //calculate Y coordinate - In this case we want to draw the text in the
+ //center of the canvas so we move Y coordinate to center.
+ float textYCoordinate = (bmpHeight - boundsHeight) / 2;
+ canvas.translate(0, textYCoordinate);
+ }
+
+ //draws static layout on canvas
+ sl.draw(canvas);
+ canvas.restore();
+
+ return bmp;
+ }
+
+ @Override
+ @NonNull
+ public TextLayer getLayer() {
+ return (TextLayer) layer;
+ }
+
+ @Override
+ protected void drawContent(@NonNull Canvas canvas, @Nullable Paint drawingPaint) {
+ if (bitmap != null) {
+ canvas.drawBitmap(bitmap, matrix, drawingPaint);
+ }
+ }
+
+ @Override
+ public int getWidth() {
+ return bitmap != null ? bitmap.getWidth() : 0;
+ }
+
+ @Override
+ public int getHeight() {
+ return bitmap != null ? bitmap.getHeight() : 0;
+ }
+
+ @Override
+ public void updateEntity() {
+ updateEntity(true);
+ }
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/util/MathUtils.java b/src/org/thoughtcrime/securesms/util/MathUtils.java
new file mode 100644
index 0000000000..15a049dfeb
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/util/MathUtils.java
@@ -0,0 +1,54 @@
+package org.thoughtcrime.securesms.util;
+
+import android.graphics.PointF;
+import android.support.annotation.NonNull;
+
+public class MathUtils {
+
+ /**
+ * For more info:
+ * StackOverflow: How to check point is in rectangle
+ *
+ * @param pt point to check
+ * @param v1 vertex 1 of the triangle
+ * @param v2 vertex 2 of the triangle
+ * @param v3 vertex 3 of the triangle
+ * @return true if point (x, y) is inside the triangle
+ */
+ public static boolean pointInTriangle(@NonNull PointF pt, @NonNull PointF v1,
+ @NonNull PointF v2, @NonNull PointF v3) {
+
+ boolean b1 = crossProduct(pt, v1, v2) < 0.0f;
+ boolean b2 = crossProduct(pt, v2, v3) < 0.0f;
+ boolean b3 = crossProduct(pt, v3, v1) < 0.0f;
+
+ return (b1 == b2) && (b2 == b3);
+ }
+
+ /**
+ * calculates cross product of vectors AB and AC
+ *
+ * @param a beginning of 2 vectors
+ * @param b end of vector 1
+ * @param c enf of vector 2
+ * @return cross product AB * AC
+ */
+ private static float crossProduct(@NonNull PointF a, @NonNull PointF b, @NonNull PointF c) {
+ return crossProduct(a.x, a.y, b.x, b.y, c.x, c.y);
+ }
+
+ /**
+ * calculates cross product of vectors AB and AC
+ *
+ * @param ax X coordinate of point A
+ * @param ay Y coordinate of point A
+ * @param bx X coordinate of point B
+ * @param by Y coordinate of point B
+ * @param cx X coordinate of point C
+ * @param cy Y coordinate of point C
+ * @return cross product AB * AC
+ */
+ private static float crossProduct(float ax, float ay, float bx, float by, float cx, float cy) {
+ return (ax - cx) * (by - cy) - (bx - cx) * (ay - cy);
+ }
+}
\ No newline at end of file