diff --git a/README.md b/README.md index c4cc357..111d4db 100644 --- a/README.md +++ b/README.md @@ -16,27 +16,15 @@ Updraft is built by App Agencies [Apps with love](https://appswithlove.com/) and ## Requirements -- minSdkVersion >=19 +- minSdkVersion >=21 ## Installation -Add the repository into your top-level build.gradle file: - -``` -allproject { - repositories { - mavenCentral() - // needed for an old dependency, we will need to fix in later release - maven { url 'https://maven.scijava.org/content/repositories/public/' } - } -} -``` - Add this line to your dependencies in app-level build.gradle file: ``` dependencies { - implementation 'com.appswithlove.updraft:updraft-sdk:1.0.14' + implementation 'com.appswithlove.updraft:updraft-sdk:1.0.15' } ``` diff --git a/app/build.gradle b/app/build.gradle index 8ceada2..9b69693 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ android { compileSdk 34 defaultConfig { applicationId "com.appswithlove.updraftsdk" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 34 versionCode 5 versionName "1.4" diff --git a/freedrawview/.gitignore b/freedrawview/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/freedrawview/.gitignore @@ -0,0 +1 @@ +/build diff --git a/freedrawview/README.md b/freedrawview/README.md new file mode 100644 index 0000000..af09da0 --- /dev/null +++ b/freedrawview/README.md @@ -0,0 +1,199 @@ +FreeDrawView +====== + +
+A View that let you draw freely on it. You can customize paint width, alpha and color. Can be useful for notes app, signatures or hands-free writing

+
+This View works flawlessly inside Scrolling parents like NestedScrollView. Be careful with lists, you need to restore manually the draw state!

+
+Also supports state-restore on rotation, with custom behaviours like "clear, crop or fitXY" and you can take a screenshot (given to you as a Bitmap Object) of the View drawn content
+ +[Changelog](CHANGELOG.md)
+ +You can try the demo app on google play store.
+coming soon

+Or see the full video demo on YouTube.
+https://youtu.be/ejEdq4lnPjc

+ +Download +------ +#### Gradle: +```groovy +compile 'com.rm:freedrawview:1.1.2' +``` + +
+Min SDK version: 9 (Android 2.3) +
+ +## Usage + +To use this library, just add this inside your layout file + +```xml + +``` + +... if you need to use this View's custom xml attributes (shown in a table below or in the example above) do not forget to add this to your root layout +``` +xmlns:app="http://schemas.android.com/apk/res-auto" +``` + +And this in your Activity +```java +public class MainActivity extends AppCompatActivity { + FreeDrawView mSignatureView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mSignatureView = (FreeDrawView) findViewById(R.id.your_id); + + // Setup the View + mSignatureView.setPaintColor(Color.BLACK); + mSignatureView.setPaintWidthPx(getResources.getDimensionPixelSize(R.dimen.paint_width)); + //mSignatureView.setPaintWidthPx(12); + mSignatureView.setPaintWidthDp(getResources.getDimension(R.dimen.paint_width)); + //mSignatureView.setPaintWidthDp(6); + mSignatureView.setPaintAlpha(255);// from 0 to 255 + mSignatureView.setResizeBehaviour(ResizeBehaviour.CROP);// Must be one of ResizeBehaviour + // values; + + // This listener will be notified every time the path done and undone count changes + mSignatureView.setPathRedoUndoCountChangeListener(new PathRedoUndoCountChangeListener() { + @Override + public void onUndoCountChanged(int undoCount) { + // The undoCount is the number of the paths that can be undone + } + + @Override + public void onRedoCountChanged(int redoCount) { + // The redoCount is the number of path removed that can be redrawn + } + }); + // This listener will be notified every time a new path has been drawn + mSignatureView.setOnPathDrawnListener(new PathDrawnListener() { + @Override + public void onNewPathDrawn() { + // The user has finished drawing a path + } + + @Override + public void onPathStart() { + // The user has started drawing a path + } + }); + + // This will take a screenshot of the current drawn content of the view + mSignatureView.getDrawScreenshot(new FreeDrawView.DrawCreatorListener() { + @Override + public void onDrawCreated(Bitmap draw) { + // The draw Bitmap is the drawn content of the View + } + + @Override + public void onDrawCreationError() { + // Something went wrong creating the bitmap, should never + // happen unless the async task has been canceled + } + }); + } +} +``` + +#### Save and restore manually the Draw content +From v1.1.0 you can get the current state of the Draw (as a Serializable object) and than restore it: +```java + FreeDrawSerializableState state = mSignatureView.getCurrentViewStateAsSerializable();// This returns a FreeDrawSerializableState (which implements Serializable) + + // Save this "state" object into a file or keep it where you want + + mSignatureView.restoreStateFromSerializable(state);// Restore the state of the view + + // Now all the previous paths and points have been restored (including the history) +``` + +To save this Serializable Object inside a file you can take a look at the class [FileHelper](app/src/main/java/com/rm/freedrawsample/FileHelper.java) + +
+ +#### Supported Attributes +FreeDrawView +------ +| XML Attribute | Java method | Description | Default value | +|------------------------- |----------------------------------------------------------------- |----------------------------------------------------------------------------------------------------------------- |--------------------------------------------------------------------------------------------- | +| paintColor | setPaintColor(@ColorInt int checked) | Set the color of the paint | Color.BLACK | +| paintWidth | setPaintWidthPx(@FloatRange(from = 0) float widthPx) | Set the width of the paint in pixels | 4dp | +| | setPaintWidthDp(float dp) | Set the width of the paint in dp | 4dp | +| paintAlpha | setPaintAlpha(@IntRange(from = 0, to = 255) int alpha) | Set the alpha of the paint | 255 | +| resizeBehaviour | setResizeBehaviour(ResizeBehaviour newBehaviour) | The behaviour of the view every time it is resized (on rotation for example) one of [clear, fitXY, crop] | ResizeBehaviour.CROP | + +
+ +#### Limitations and TODO +* Multitouch drawing is currently not supported
+* Eraser is not yet implemented
+* ~~Manually restore state is not supported~~
+* Get the draw screenshot from a FreeDrawSerializableState without adding the view + +
+ +Also, the FreeDrawView class gives some utility methods to handle path history:
+* ```public void undoLast()```
+ This method undo the last drawn segment

+* ```public void redoLast()```
+ This method redraw the last undone segment

+* ```public void undoAll()```
+ This method undo all the drawn segments, they can be redone one by one or all in one

+* ```public void redoAll()```
+ This method redraw all the undone segments

+* ```public void clearHistory()```
+ This method removes all the history segments (The one that could be redone)

+* ```public void clearDraw()```
+ This method removes all the current drawn segments, without adding them to the history

+* ```public void clearDrawAndHistory()```
+ This method removes all the current drawn segments and clears the history

+* ```public int getPathCount(boolean includeCurrentlyDrawingPath)```
+ This method returns the current number of segments drawn

+ + +
+ +You can use:
+* ```setOnPathDrawnListener(PathDrawnListener listener)```
+to be notified every time the user starts or finishes drawing a line.

+* ```setPathRedoUndoCountChangeListener(PathRedoUndoCountChangeListener listener)```
+to be notified when the undo or redo count changes.

+ +And remove them with:
+* ```removePathDrawnListener()```
+* ```removePathRedoUndoCountChangeListener()```
+
+ +License +-------- + + Copyright 2017 Riccardo Moro. + + 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. diff --git a/freedrawview/build.gradle b/freedrawview/build.gradle new file mode 100644 index 0000000..d43e337 --- /dev/null +++ b/freedrawview/build.gradle @@ -0,0 +1,45 @@ +apply plugin: 'com.android.library' + +ext { + bintrayRepo = 'maven' + bintrayName = 'freedrawview' + + publishedGroupId = 'com.rm' + libraryName = 'freedrawview' + artifact = 'freedrawview' + + libraryDescription = 'A View on which you can freely draw, customizing paint width, alpha and color, and take a screenshot of the content.' + + siteUrl = 'https://github.com/RiccardoMoro/FreeDrawView' + gitUrl = 'https://github.com/RiccardoMoro/FreeDrawView.git' + + libraryVersion = '1.1.2' + + developerId = 'RiccardoMoro' + developerName = 'Riccardo Moro' + developerEmail = 'riccardomoro.rm@gmail.com' + + licenseName = 'The Apache Software License, Version 2.0' + licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + allLicenses = ["Apache-2.0"] +} + +android { + namespace "com.rm.freedrawview" + compileSdk 34 + + defaultConfig { + minSdkVersion 9 + targetSdkVersion 34 + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation libs.androidx.annotation +} diff --git a/freedrawview/proguard-rules.pro b/freedrawview/proguard-rules.pro new file mode 100644 index 0000000..49b4af1 --- /dev/null +++ b/freedrawview/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\Users\riky9\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/freedrawview/sample.gif b/freedrawview/sample.gif new file mode 100644 index 0000000..11ce3a6 Binary files /dev/null and b/freedrawview/sample.gif differ diff --git a/freedrawview/sample_rotation.gif b/freedrawview/sample_rotation.gif new file mode 100644 index 0000000..7ab0bc5 Binary files /dev/null and b/freedrawview/sample_rotation.gif differ diff --git a/freedrawview/sample_scrollable.gif b/freedrawview/sample_scrollable.gif new file mode 100644 index 0000000..57f8cc6 Binary files /dev/null and b/freedrawview/sample_scrollable.gif differ diff --git a/freedrawview/screen.png b/freedrawview/screen.png new file mode 100644 index 0000000..b740dc1 Binary files /dev/null and b/freedrawview/screen.png differ diff --git a/freedrawview/src/main/AndroidManifest.xml b/freedrawview/src/main/AndroidManifest.xml new file mode 100644 index 0000000..737b447 --- /dev/null +++ b/freedrawview/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/freedrawview/src/main/java/com/rm/freedrawview/FreeDrawHelper.java b/freedrawview/src/main/java/com/rm/freedrawview/FreeDrawHelper.java new file mode 100644 index 0000000..ec98f4d --- /dev/null +++ b/freedrawview/src/main/java/com/rm/freedrawview/FreeDrawHelper.java @@ -0,0 +1,114 @@ +package com.rm.freedrawview; + +import android.content.res.Resources; +import android.graphics.ComposePathEffect; +import android.graphics.CornerPathEffect; +import android.graphics.Paint; + +import androidx.annotation.NonNull; + +import java.util.List; + +/** + * Created by Riccardo Moro on 10/23/2016. + */ + +public class FreeDrawHelper { + + /** + * Function used to check whenever a list of points is a line or a path to draw + */ + static boolean isAPoint(@NonNull List points) { + if (points.size() == 0) + return false; + + if (points.size() == 1) + return true; + + for (int i = 1; i < points.size(); i++) { + if (points.get(i - 1).x != points.get(i).x || points.get(i - 1).y != points.get(i).y) + return false; + } + + return true; + } + + /** + * Create, initialize and setup a paint + */ + static Paint createPaintAndInitialize(int paintColor, int paintAlpha, + float paintWidth, boolean fill) { + + Paint paint = createPaint(); + + initializePaint(paint, paintColor, paintAlpha, paintWidth, fill); + + return paint; + } + + static Paint createPaint() { + return new Paint(Paint.ANTI_ALIAS_FLAG); + } + + static void initializePaint(Paint paint, int paintColor, int paintAlpha, float paintWidth, + boolean fill) { + + if (fill) { + setupFillPaint(paint); + } else { + setupStrokePaint(paint); + } + + paint.setStrokeWidth(paintWidth); + paint.setColor(paintColor); + paint.setAlpha(paintAlpha); + } + + static void setupFillPaint(Paint paint) { + paint.setStyle(Paint.Style.FILL); + } + + static void setupStrokePaint(Paint paint) { + paint.setStrokeJoin(Paint.Join.ROUND); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setPathEffect(new ComposePathEffect( + new CornerPathEffect(100f), + new CornerPathEffect(100f))); + paint.setStyle(Paint.Style.STROKE); + } + + static void copyFromPaint(Paint from, Paint to, boolean copyWidth) { + + to.setColor(from.getColor()); + to.setAlpha(from.getAlpha()); + + if (copyWidth) { + to.setStrokeWidth(from.getStrokeWidth()); + } + } + + static void copyFromValues(Paint to, int color, int alpha, float strokeWidth, + boolean copyWidth) { + + to.setColor(color); + to.setAlpha(alpha); + + if (copyWidth) { + to.setStrokeWidth(strokeWidth); + } + } + + /** + * Converts a given dp number to it's pixel corresponding number + */ + public static float convertDpToPixels(float dp) { + return (dp * Resources.getSystem().getDisplayMetrics().density); + } + + /** + * Converts a given pixel number to it's dp corresponding number + */ + public static float convertPixelsToDp(float px) { + return px / Resources.getSystem().getDisplayMetrics().density; + } +} diff --git a/freedrawview/src/main/java/com/rm/freedrawview/FreeDrawSavedState.java b/freedrawview/src/main/java/com/rm/freedrawview/FreeDrawSavedState.java new file mode 100644 index 0000000..632fbf8 --- /dev/null +++ b/freedrawview/src/main/java/com/rm/freedrawview/FreeDrawSavedState.java @@ -0,0 +1,137 @@ +package com.rm.freedrawview; + +import android.graphics.Paint; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.View; + +import androidx.annotation.ColorInt; +import androidx.annotation.IntRange; + +import java.util.ArrayList; + +/** + * Created by Riccardo Moro on 11/4/2016. + */ + +class FreeDrawSavedState extends View.BaseSavedState { + + private ArrayList mPaths = new ArrayList<>(); + private ArrayList mCanceledPaths = new ArrayList<>(); + + private int mPaintColor; + private int mPaintAlpha; + private float mPaintWidth; + + private ResizeBehaviour mResizeBehaviour; + + private int mLastDimensionW; + private int mLastDimensionH; + + FreeDrawSavedState(Parcelable superState, ArrayList paths, + ArrayList canceledPaths, float paintWidth, + int paintColor, int paintAlpha, ResizeBehaviour resizeBehaviour, + int lastDimensionW, int lastDimensionH) { + super(superState); + + mPaths = paths; + mCanceledPaths = canceledPaths; + mPaintWidth = paintWidth; + + mPaintColor = paintColor; + mPaintAlpha = paintAlpha; + + mResizeBehaviour = resizeBehaviour; + + mLastDimensionW = lastDimensionW; + mLastDimensionH = lastDimensionH; + } + + ArrayList getPaths() { + return mPaths; + } + + ArrayList getCanceledPaths() { + return mCanceledPaths; + } + + @ColorInt + int getPaintColor() { + return mPaintColor; + } + + @IntRange(from = 0, to = 255) + int getPaintAlpha() { + return mPaintAlpha; + } + + float getCurrentPaintWidth() { + return mPaintWidth; + } + + Paint getCurrentPaint() { + + Paint paint = FreeDrawHelper.createPaint(); + FreeDrawHelper.setupStrokePaint(paint); + FreeDrawHelper.copyFromValues(paint, mPaintColor, mPaintAlpha, mPaintWidth, true); + return paint; + } + + ResizeBehaviour getResizeBehaviour() { + return mResizeBehaviour; + } + + int getLastDimensionW() { + return mLastDimensionW; + } + + int getLastDimensionH() { + return mLastDimensionH; + } + + // Parcelable stuff + private FreeDrawSavedState(Parcel in) { + super(in); + + in.readTypedList(mPaths, HistoryPath.CREATOR); + in.readTypedList(mCanceledPaths, HistoryPath.CREATOR); + + mPaintColor = in.readInt(); + mPaintAlpha = in.readInt(); + mPaintWidth = in.readFloat(); + + mResizeBehaviour = (ResizeBehaviour) in.readSerializable(); + + mLastDimensionW = in.readInt(); + mLastDimensionH = in.readInt(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + + out.writeTypedList(mPaths); + out.writeTypedList(mCanceledPaths); + + out.writeInt(mPaintColor); + out.writeInt(mPaintAlpha); + out.writeFloat(mPaintWidth); + + out.writeSerializable(mResizeBehaviour); + + out.writeInt(mLastDimensionW); + out.writeInt(mLastDimensionH); + } + + // Parcelable CREATOR class, needed for parcelable to work + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public FreeDrawSavedState createFromParcel(Parcel in) { + return new FreeDrawSavedState(in); + } + + public FreeDrawSavedState[] newArray(int size) { + return new FreeDrawSavedState[size]; + } + }; +} diff --git a/freedrawview/src/main/java/com/rm/freedrawview/FreeDrawSerializableState.java b/freedrawview/src/main/java/com/rm/freedrawview/FreeDrawSerializableState.java new file mode 100644 index 0000000..c6fe72c --- /dev/null +++ b/freedrawview/src/main/java/com/rm/freedrawview/FreeDrawSerializableState.java @@ -0,0 +1,104 @@ +package com.rm.freedrawview; + +import java.io.Serializable; +import java.util.ArrayList; + +/** + * Created by Riccardo on 23/05/2017. + */ + +public class FreeDrawSerializableState implements Serializable { + + static final long serialVersionUID = 40L; + + private ArrayList mCanceledPaths; + private ArrayList mPaths; + + private int mPaintColor; + private int mPaintAlpha; + private float mPaintWidth; + + private ResizeBehaviour mResizeBehaviour; + + private int mLastDimensionW; + private int mLastDimensionH; + + public FreeDrawSerializableState(ArrayList canceledPaths, + ArrayList paths, int paintColor, int paintAlpha, + float paintWidth, ResizeBehaviour resizeBehaviour, + int lastW, int lastH) { + + setCanceledPaths(canceledPaths != null ? canceledPaths : new ArrayList()); + setPaths(paths != null ? paths : new ArrayList()); + setPaintWidth(paintWidth >= 0 ? paintWidth : 0); + setPaintColor(paintColor); + setPaintAlpha(paintAlpha); + setResizeBehaviour(resizeBehaviour); + setLastDimensionW(lastW >= 0 ? lastW : 0); + setLastDimensionH(lastH >= 0 ? lastH : 0); + } + + public ArrayList getCanceledPaths() { + return mCanceledPaths; + } + + public void setCanceledPaths(ArrayList canceledPaths) { + this.mCanceledPaths = canceledPaths; + } + + public ArrayList getPaths() { + return mPaths; + } + + public void setPaths(ArrayList paths) { + this.mPaths = paths; + } + + public float getPaintWidth() { + return mPaintWidth; + } + + public void setPaintWidth(float paintWidth) { + this.mPaintWidth = paintWidth; + } + + public int getPaintColor() { + return mPaintColor; + } + + public void setPaintColor(int paintColor) { + this.mPaintColor = paintColor; + } + + public int getPaintAlpha() { + return mPaintAlpha; + } + + public void setPaintAlpha(int paintAlpha) { + this.mPaintAlpha = paintAlpha; + } + + public ResizeBehaviour getResizeBehaviour() { + return mResizeBehaviour; + } + + public void setResizeBehaviour(ResizeBehaviour resizeBehaviour) { + this.mResizeBehaviour = resizeBehaviour; + } + + public int getLastDimensionW() { + return mLastDimensionW; + } + + public void setLastDimensionW(int lastDimensionW) { + this.mLastDimensionW = lastDimensionW; + } + + public int getLastDimensionH() { + return mLastDimensionH; + } + + public void setLastDimensionH(int lastDimensionH) { + this.mLastDimensionH = lastDimensionH; + } +} diff --git a/freedrawview/src/main/java/com/rm/freedrawview/FreeDrawView.java b/freedrawview/src/main/java/com/rm/freedrawview/FreeDrawView.java new file mode 100644 index 0000000..2ffa849 --- /dev/null +++ b/freedrawview/src/main/java/com/rm/freedrawview/FreeDrawView.java @@ -0,0 +1,775 @@ +package com.rm.freedrawview; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.os.AsyncTask; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.ColorInt; +import androidx.annotation.FloatRange; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; + +/** + * Created by Riccardo Moro on 9/10/2016. + */ +public class FreeDrawView extends View implements View.OnTouchListener { + private static final String TAG = FreeDrawView.class.getSimpleName(); + + private static final float DEFAULT_STROKE_WIDTH = 4; + private static final int DEFAULT_COLOR = Color.BLACK; + private static final int DEFAULT_ALPHA = 255; + + private Paint mCurrentPaint; + private Path mCurrentPath; + + private ResizeBehaviour mResizeBehaviour; + + private ArrayList mPoints = new ArrayList<>(); + private ArrayList mPaths = new ArrayList<>(); + private ArrayList mCanceledPaths = new ArrayList<>(); + + @ColorInt + private int mPaintColor = DEFAULT_COLOR; + @IntRange(from = 0, to = 255) + private int mPaintAlpha = DEFAULT_ALPHA; + + private int mLastDimensionW = -1; + private int mLastDimensionH = -1; + + private boolean mFinishPath = false; + + private PathDrawnListener mPathDrawnListener; + private PathRedoUndoCountChangeListener mPathRedoUndoCountChangeListener; + + public FreeDrawView(Context context) { + this(context, null); + } + + public FreeDrawView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FreeDrawView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + setOnTouchListener(this); + + TypedArray a = null; + try { + + a = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.FreeDrawView, + defStyleAttr, 0); + + initPaints(a); + } finally { + if (a != null) { + a.recycle(); + } + } + } + + @Override + protected Parcelable onSaveInstanceState() { + + // Get the superclass parcelable state + Parcelable superState = super.onSaveInstanceState(); + + if (mPoints.size() > 0) {// Currently doing a line, save it's current path + createHistoryPathFromPoints(); + } + + return new FreeDrawSavedState(superState, mPaths, mCanceledPaths, + getPaintWidth(), getPaintColor(), getPaintAlpha(), + getResizeBehaviour(), mLastDimensionW, mLastDimensionH); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + + // If not instance of my state, let the superclass handle it + if (!(state instanceof FreeDrawSavedState)) { + super.onRestoreInstanceState(state); + return; + } + + FreeDrawSavedState savedState = (FreeDrawSavedState) state; + // Superclass restore state + super.onRestoreInstanceState(savedState.getSuperState()); + + // My state restore + mPaths = savedState.getPaths(); + mCanceledPaths = savedState.getCanceledPaths(); + mCurrentPaint = savedState.getCurrentPaint(); + + setPaintWidthPx(savedState.getCurrentPaintWidth()); + setPaintColor(savedState.getPaintColor()); + setPaintAlpha(savedState.getPaintAlpha()); + + setResizeBehaviour(savedState.getResizeBehaviour()); + + // Restore the last dimensions, so that in onSizeChanged i can calculate the + // height and width change factor and multiply every point x or y to it, so that if the + // View is resized, it adapt automatically it's points to the new width/height + mLastDimensionW = savedState.getLastDimensionW(); + mLastDimensionH = savedState.getLastDimensionH(); + + notifyRedoUndoCountChanged(); + } + + /** + * Set the paint color + * + * @param color The now color to be applied to the + */ + public void setPaintColor(@ColorInt int color) { + + invalidate(); + + mPaintColor = color; + + mCurrentPaint.setColor(mPaintColor); + mCurrentPaint.setAlpha(mPaintAlpha);// Restore the previous alpha + } + + /** + * Get the current paint color without it's alpha + */ + @ColorInt + public int getPaintColor() { + return mPaintColor; + } + + /** + * Get the current color with the current alpha + */ + @ColorInt + public int getPaintColorWithAlpha() { + return mCurrentPaint.getColor(); + } + + + /** + * Set the paint width in px + * + * @param widthPx The new weight in px, must be > 0 + */ + public void setPaintWidthPx(@FloatRange(from = 0) float widthPx) { + if (widthPx > 0) { + + invalidate(); + + mCurrentPaint.setStrokeWidth(widthPx); + } + } + + /** + * Set the paint width in dp + * + * @param dp The new weight in dp, must be > 0 + */ + public void setPaintWidthDp(float dp) { + setPaintWidthPx(FreeDrawHelper.convertDpToPixels(dp)); + } + + /** + * {@link #getPaintWidth(boolean)} + */ + @FloatRange(from = 0) + public float getPaintWidth() { + return getPaintWidth(false); + } + + /** + * Get the current paint with in dp or pixel + */ + @FloatRange(from = 0) + public float getPaintWidth(boolean inDp) { + if (inDp) { + return FreeDrawHelper.convertPixelsToDp(mCurrentPaint.getStrokeWidth()); + } else { + return mCurrentPaint.getStrokeWidth(); + } + } + + + /** + * Set the paint opacity, must be between 0 and 1 + * + * @param alpha The alpha to apply to the paint + */ + public void setPaintAlpha(@IntRange(from = 0, to = 255) int alpha) { + + // Finish current path and redraw, so that the new setting is applied only to the next path + invalidate(); + + mPaintAlpha = alpha; + mCurrentPaint.setAlpha(mPaintAlpha); + } + + /** + * Get the current paint alpha + */ + @IntRange(from = 0, to = 255) + public int getPaintAlpha() { + return mPaintAlpha; + } + + + /** + * Set what to do when the view is resized (on rotation if its dimensions are not fixed) + * {@link ResizeBehaviour} + */ + public void setResizeBehaviour(ResizeBehaviour newBehaviour) { + mResizeBehaviour = newBehaviour; + } + + /** + * Get the current behaviour on view resize + */ + public ResizeBehaviour getResizeBehaviour() { + return mResizeBehaviour; + } + + + /** + * Cancel the last drawn segment + */ + public void undoLast() { + + if (mPaths.size() > 0) { + // End current path + mFinishPath = true; + invalidate(); + + // Cancel the last one and redraw + mCanceledPaths.add(mPaths.get(mPaths.size() - 1)); + mPaths.remove(mPaths.size() - 1); + invalidate(); + + notifyRedoUndoCountChanged(); + } + } + + /** + * Re-add the first removed path and redraw + */ + public void redoLast() { + + if (mCanceledPaths.size() > 0) { + mPaths.add(mCanceledPaths.get(mCanceledPaths.size() - 1)); + mCanceledPaths.remove(mCanceledPaths.size() - 1); + invalidate(); + + notifyRedoUndoCountChanged(); + } + } + + /** + * Remove all the paths and redraw (can be undone with {@link #redoLast()}) + */ + public void undoAll() { + Collections.reverse(mPaths); + mCanceledPaths.addAll(mPaths); + mPaths = new ArrayList<>(); + invalidate(); + + notifyRedoUndoCountChanged(); + } + + /** + * Re-add all the removed paths and redraw + */ + public void redoAll() { + + if (mCanceledPaths.size() > 0) { + mPaths.addAll(mCanceledPaths); + mCanceledPaths = new ArrayList<>(); + invalidate(); + + notifyRedoUndoCountChanged(); + } + } + + /** + * Get how many undo operations are available + */ + public int getUndoCount() { + return mPaths.size(); + } + + /** + * Get how many redo operations are available + */ + public int getRedoCount() { + return mCanceledPaths.size(); + } + + /** + * Get how many paths are drawn on this FreeDrawView + * + * @param includeCurrentlyDrawingPath Include the path that is currently been drawn + * @return The number of paths drawn + */ + public int getPathCount(boolean includeCurrentlyDrawingPath) { + int size = mPaths.size(); + + if (includeCurrentlyDrawingPath && mPoints.size() > 0) { + size++; + } + return size; + } + + /** + * Set a path drawn listener, will be called every time a new path is drawn + */ + public void setOnPathDrawnListener(PathDrawnListener listener) { + mPathDrawnListener = listener; + } + + /** + * Remove the path drawn listener + */ + public void removePathDrawnListener() { + mPathDrawnListener = null; + } + + /** + * Clear the current draw and the history + */ + public void clearDrawAndHistory() { + + clearDraw(false); + clearHistory(true); + } + + /** + * Clear the current draw + */ + public void clearDraw() { + + clearDraw(true); + } + + private void clearDraw(boolean invalidate) { + mPoints = new ArrayList<>(); + mPaths = new ArrayList<>(); + + notifyRedoUndoCountChanged(); + + if (invalidate) { + invalidate(); + } + } + + /** + * Clear the history (paths that can be redone) + */ + public void clearHistory() { + clearHistory(true); + } + + private void clearHistory(boolean invalidate) { + mCanceledPaths = new ArrayList<>(); + + notifyRedoUndoCountChanged(); + + if (invalidate) { + invalidate(); + } + } + + /** + * Set a redo-undo count change listener, this will be called every time undo or redo count + * changes + */ + public void setPathRedoUndoCountChangeListener(PathRedoUndoCountChangeListener listener) { + mPathRedoUndoCountChangeListener = listener; + } + + /** + * Remove the redo-undo count listener + */ + public void removePathRedoUndoCountChangeListener() { + mPathRedoUndoCountChangeListener = null; + } + + /** + * Get a serializable object with all the needed info about the current draw and state + * + * @return A {@link FreeDrawSerializableState} containing all the needed data + */ + public FreeDrawSerializableState getCurrentViewStateAsSerializable() { + + return new FreeDrawSerializableState(mCanceledPaths, mPaths, getPaintColor(), + getPaintAlpha(), getPaintWidth(), getResizeBehaviour(), + mLastDimensionW, mLastDimensionH); + } + + /** + * Restore the state of the draw from the given serializable state + * + * @param state A {@link FreeDrawSerializableState} containing all the draw and paint info, + * if null, nothing will be restored. Null sub fields will be ignored + */ + public void restoreStateFromSerializable(FreeDrawSerializableState state) { + + if (state != null) { + + if (state.getCanceledPaths() != null) { + mCanceledPaths = state.getCanceledPaths(); + } + + if (state.getPaths() != null) { + mPaths = state.getPaths(); + } + + mPaintColor = state.getPaintColor(); + mPaintAlpha = state.getPaintAlpha(); + + mCurrentPaint.setColor(state.getPaintColor()); + mCurrentPaint.setAlpha(state.getPaintAlpha()); + setPaintWidthPx(state.getPaintWidth()); + + mResizeBehaviour = state.getResizeBehaviour(); + + if (state.getLastDimensionW() >= 0) { + mLastDimensionW = state.getLastDimensionW(); + } + + if (state.getLastDimensionH() >= 0) { + mLastDimensionH = state.getLastDimensionH(); + } + + notifyRedoUndoCountChanged(); + invalidate(); + } + } + + /** + * Create a Bitmap with the content drawn inside the view + */ + public void getDrawScreenshot(@NonNull final DrawCreatorListener listener) { + new TakeScreenShotAsyncTask(listener).execute(); + } + + + // Internal methods + private void notifyPathStart() { + if (mPathDrawnListener != null) { + mPathDrawnListener.onPathStart(); + } + } + + private void notifyPathDrawn() { + if (mPathDrawnListener != null) { + mPathDrawnListener.onNewPathDrawn(); + } + } + + private void notifyRedoUndoCountChanged() { + if (mPathRedoUndoCountChangeListener != null) { + mPathRedoUndoCountChangeListener.onRedoCountChanged(getRedoCount()); + mPathRedoUndoCountChangeListener.onUndoCountChanged(getUndoCount()); + } + } + + private void initPaints(TypedArray a) { + mCurrentPaint = FreeDrawHelper.createPaint(); + + mCurrentPaint.setColor(a != null ? a.getColor(R.styleable.FreeDrawView_paintColor, + mPaintColor) : mPaintColor); + mCurrentPaint.setAlpha(a != null ? + a.getInt(R.styleable.FreeDrawView_paintAlpha, mPaintAlpha) + : mPaintAlpha); + mCurrentPaint.setStrokeWidth(a != null ? + a.getDimensionPixelSize(R.styleable.FreeDrawView_paintWidth, + (int) FreeDrawHelper.convertDpToPixels(DEFAULT_STROKE_WIDTH)) + : FreeDrawHelper.convertDpToPixels(DEFAULT_STROKE_WIDTH)); + + FreeDrawHelper.setupStrokePaint(mCurrentPaint); + + if (a != null) { + int resizeBehaviour = a.getInt(R.styleable.FreeDrawView_resizeBehaviour, -1); + mResizeBehaviour = + resizeBehaviour == 0 ? ResizeBehaviour.CLEAR : + resizeBehaviour == 1 ? ResizeBehaviour.FIT_XY : + resizeBehaviour == 2 ? ResizeBehaviour.CROP : + ResizeBehaviour.CROP; + } + } + + private Paint createAndCopyColorAndAlphaForFillPaint(Paint from, boolean copyWidth) { + Paint paint = FreeDrawHelper.createPaint(); + FreeDrawHelper.setupFillPaint(paint); + paint.setColor(from.getColor()); + paint.setAlpha(from.getAlpha()); + if (copyWidth) { + paint.setStrokeWidth(from.getStrokeWidth()); + } + return paint; + } + + @Override + protected synchronized void onDraw(Canvas canvas) { + if (mPaths.size() == 0 && mPoints.size() == 0) { + return; + } + + // Avoid concurrency errors by first setting the finished path variable to false + final boolean finishedPath = mFinishPath; + mFinishPath = false; + + for (HistoryPath currentPath : mPaths) { + + // If the path is just a single point, draw as a point + if (currentPath.isPoint()) { + + canvas.drawCircle(currentPath.getOriginX(), currentPath.getOriginY(), + currentPath.getPaint().getStrokeWidth() / 2, currentPath.getPaint()); + } else {// Else draw the complete path + + canvas.drawPath(currentPath.getPath(), currentPath.getPaint()); + } + } + + // Initialize the current path + if (mCurrentPath == null) + mCurrentPath = new Path(); + else + mCurrentPath.rewind(); + + // If a single point, add a circle to the path + if (mPoints.size() == 1 || FreeDrawHelper.isAPoint(mPoints)) { + + canvas.drawCircle(mPoints.get(0).x, mPoints.get(0).y, + mCurrentPaint.getStrokeWidth() / 2, + createAndCopyColorAndAlphaForFillPaint(mCurrentPaint, false)); + } else if (mPoints.size() != 0) {// Else draw the complete series of points + + boolean first = true; + + for (Point point : mPoints) { + + if (first) { + mCurrentPath.moveTo(point.x, point.y); + first = false; + } else { + mCurrentPath.lineTo(point.x, point.y); + } + } + + canvas.drawPath(mCurrentPath, mCurrentPaint); + } + + // If the path is finished, add it to the history + if (finishedPath && mPoints.size() > 0) { + createHistoryPathFromPoints(); + } + } + + // Create a path from the current points + private void createHistoryPathFromPoints() { + mPaths.add(new HistoryPath(mPoints, new Paint(mCurrentPaint))); + + mPoints = new ArrayList<>(); + + notifyPathDrawn(); + notifyRedoUndoCountChanged(); + } + + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + + if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { + notifyPathStart(); + } + if (getParent() != null) { + getParent().requestDisallowInterceptTouchEvent(true); + } + + // Clear all the history when restarting to draw + mCanceledPaths = new ArrayList<>(); + + if ((motionEvent.getAction() != MotionEvent.ACTION_UP) && + (motionEvent.getAction() != MotionEvent.ACTION_CANCEL)) { + Point point; + for (int i = 0; i < motionEvent.getHistorySize(); i++) { + point = new Point(); + point.x = motionEvent.getHistoricalX(i); + point.y = motionEvent.getHistoricalY(i); + mPoints.add(point); + } + point = new Point(); + point.x = motionEvent.getX(); + point.y = motionEvent.getY(); + mPoints.add(point); + mFinishPath = false; + } else + mFinishPath = true; + + invalidate(); + return true; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + float xMultiplyFactor = 1; + float yMultiplyFactor = 1; + + + if (mLastDimensionW == -1) { + mLastDimensionW = w; + } + + if (mLastDimensionH == -1) { + mLastDimensionH = h; + } + + if (w >= 0 && w != oldw && w != mLastDimensionW) { + xMultiplyFactor = (float) w / mLastDimensionW; + mLastDimensionW = w; + } + + if (h >= 0 && h != oldh && h != mLastDimensionH) { + yMultiplyFactor = (float) h / mLastDimensionH; + mLastDimensionH = h; + } + + multiplyPathsAndPoints(xMultiplyFactor, yMultiplyFactor); + } + + // Translate all the paths, used every time that this view size is changed + @SuppressWarnings("SuspiciousNameCombination") + private void multiplyPathsAndPoints(float xMultiplyFactor, float yMultiplyFactor) { + + // If both factors == 1 or <= 0 or no paths/points to apply things, just return + if ((xMultiplyFactor == 1 && yMultiplyFactor == 1) + || (xMultiplyFactor <= 0 || yMultiplyFactor <= 0) || + (mPaths.size() == 0 && mCanceledPaths.size() == 0 && mPoints.size() == 0)) { + return; + } + + if (mResizeBehaviour == ResizeBehaviour.CLEAR) {// If clear, clear all and return + mPaths = new ArrayList<>(); + mCanceledPaths = new ArrayList<>(); + mPoints = new ArrayList<>(); + return; + } else if (mResizeBehaviour == ResizeBehaviour.CROP) { + xMultiplyFactor = yMultiplyFactor = 1; + } + + // Adapt drawn paths + for (HistoryPath historyPath : mPaths) { + + if (historyPath.isPoint()) { + historyPath.setOriginX(historyPath.getOriginX() * xMultiplyFactor); + historyPath.setOriginY(historyPath.getOriginY() * yMultiplyFactor); + } else { + for (Point point : historyPath.getPoints()) { + point.x *= xMultiplyFactor; + point.y *= yMultiplyFactor; + } + } + + historyPath.generatePath(); + } + + // Adapt canceled paths + for (HistoryPath historyPath : mCanceledPaths) { + + if (historyPath.isPoint()) { + historyPath.setOriginX(historyPath.getOriginX() * xMultiplyFactor); + historyPath.setOriginY(historyPath.getOriginY() * yMultiplyFactor); + } else { + for (Point point : historyPath.getPoints()) { + point.x *= xMultiplyFactor; + point.y *= yMultiplyFactor; + } + } + + historyPath.generatePath(); + } + + // Adapt drawn points + for (Point point : mPoints) { + point.x *= xMultiplyFactor; + point.y *= yMultiplyFactor; + } + } + + public interface DrawCreatorListener { + void onDrawCreated(Bitmap draw); + + void onDrawCreationError(); + } + + + private class TakeScreenShotAsyncTask extends AsyncTask { + private int mWidth, mHeight; + private Canvas mCanvas; + private Bitmap mBitmap; + private DrawCreatorListener mListener; + + public TakeScreenShotAsyncTask(@NonNull DrawCreatorListener listener) { + mListener = listener; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + mWidth = getWidth(); + mHeight = getHeight(); + } + + @Override + protected Void doInBackground(Void... params) { + + try { + mBitmap = Bitmap.createBitmap( + mWidth, mHeight, Bitmap.Config.ARGB_8888); + mCanvas = new Canvas(mBitmap); + } catch (Exception e) { + e.printStackTrace(); + cancel(true); + } + + return null; + } + + @Override + protected void onCancelled() { + super.onCancelled(); + + if (mListener != null) { + mListener.onDrawCreationError(); + } + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + + draw(mCanvas); + + if (mListener != null) { + mListener.onDrawCreated(mBitmap); + } + } + } +} diff --git a/freedrawview/src/main/java/com/rm/freedrawview/HistoryPath.java b/freedrawview/src/main/java/com/rm/freedrawview/HistoryPath.java new file mode 100644 index 0000000..066c8df --- /dev/null +++ b/freedrawview/src/main/java/com/rm/freedrawview/HistoryPath.java @@ -0,0 +1,205 @@ +package com.rm.freedrawview; + +import android.graphics.Paint; +import android.graphics.Path; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import java.io.Serializable; +import java.util.ArrayList; + +/** + * Created by Riccardo Moro on 9/27/2016. + */ + +class HistoryPath implements Parcelable, Serializable { + + static final long serialVersionUID = 41L; + + private static final String TAG = HistoryPath.class.getSimpleName(); + + private ArrayList points = new ArrayList<>(); + private int paintColor; + private int paintAlpha; + private float paintWidth; + private float originX, originY; + private boolean isPoint; + + private transient Path path = null; + private transient Paint paint = null; + + HistoryPath(@NonNull ArrayList points, @NonNull Paint paint) { + this.points = new ArrayList<>(points); + this.paintColor = paint.getColor(); + this.paintAlpha = paint.getAlpha(); + this.paintWidth = paint.getStrokeWidth(); + this.originX = points.get(0).x; + this.originY = points.get(0).y; + this.isPoint = FreeDrawHelper.isAPoint(points); + + generatePath(); + generatePaint(); + } + + public void generatePath() { + + path = new Path(); + + if (points != null) { + boolean first = true; + + for (int i = 0; i < points.size(); i++) { + + Point point = points.get(i); + + if (first) { + path.moveTo(point.x, point.y); + first = false; + } else { + path.lineTo(point.x, point.y); + } + } + } + } + + private void generatePaint() { + + paint = FreeDrawHelper.createPaintAndInitialize(paintColor, paintAlpha, paintWidth, + isPoint); + } + + public Path getPath() { + + if (path == null) { + + generatePath(); + } + + return path; + } + + public boolean isPoint() { + return isPoint; + } + + public void setPoint(boolean point) { + isPoint = point; + } + + public float getOriginX() { + return originX; + } + + public void setOriginX(float originX) { + this.originX = originX; + } + + public float getOriginY() { + return originY; + } + + public void setOriginY(float originY) { + this.originY = originY; + } + + public int getPaintColor() { + return paintColor; + } + + public void setPaintColor(int paintColor) { + this.paintColor = paintColor; + } + + public int getPaintAlpha() { + return paintAlpha; + } + + public void setPaintAlpha(int paintAlpha) { + this.paintAlpha = paintAlpha; + } + + public float getPaintWidth() { + return paintWidth; + } + + public void setPaintWidth(float paintWidth) { + this.paintWidth = paintWidth; + } + + public Paint getPaint() { + + if (paint == null) { + generatePaint(); + } + + return paint; + } + + public ArrayList getPoints() { + return points; + } + + public void setPoints(ArrayList points) { + this.points = points; + } + + // Parcelable stuff + private HistoryPath(Parcel in) { + in.readTypedList(points, Point.CREATOR); + + paintColor = in.readInt(); + paintAlpha = in.readInt(); + paintWidth = in.readFloat(); + + originX = in.readFloat(); + originY = in.readFloat(); + + isPoint = in.readByte() != 0; + + generatePath(); + generatePaint(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedList(points); + + dest.writeInt(paintColor); + dest.writeInt(paintAlpha); + dest.writeFloat(paintWidth); + + dest.writeFloat(originX); + dest.writeFloat(originY); + + dest.writeByte((byte) (isPoint ? 1 : 0)); + } + + // Parcelable CREATOR class + public static final Creator CREATOR = new Creator() { + @Override + public HistoryPath createFromParcel(Parcel in) { + return new HistoryPath(in); + } + + @Override + public HistoryPath[] newArray(int size) { + return new HistoryPath[size]; + } + }; + + @Override + public String toString() { + return "Point: " + isPoint + "\n" + + "Points: " + points + "\n" + + "Color: " + paintColor + "\n" + + "Alpha: " + paintAlpha + "\n" + + "Width: " + paintWidth; + } +} diff --git a/freedrawview/src/main/java/com/rm/freedrawview/PathDrawnListener.java b/freedrawview/src/main/java/com/rm/freedrawview/PathDrawnListener.java new file mode 100644 index 0000000..909737d --- /dev/null +++ b/freedrawview/src/main/java/com/rm/freedrawview/PathDrawnListener.java @@ -0,0 +1,12 @@ +package com.rm.freedrawview; + +/** + * Created by Riccardo on 22/11/16. + */ + +public interface PathDrawnListener { + + void onPathStart(); + + void onNewPathDrawn(); +} diff --git a/freedrawview/src/main/java/com/rm/freedrawview/PathRedoUndoCountChangeListener.java b/freedrawview/src/main/java/com/rm/freedrawview/PathRedoUndoCountChangeListener.java new file mode 100644 index 0000000..14659e6 --- /dev/null +++ b/freedrawview/src/main/java/com/rm/freedrawview/PathRedoUndoCountChangeListener.java @@ -0,0 +1,11 @@ +package com.rm.freedrawview; + +/** + * Created by Riccardo on 22/11/16. + */ + +public interface PathRedoUndoCountChangeListener { + void onUndoCountChanged(int undoCount); + + void onRedoCountChanged(int redoCount); +} diff --git a/freedrawview/src/main/java/com/rm/freedrawview/Point.java b/freedrawview/src/main/java/com/rm/freedrawview/Point.java new file mode 100644 index 0000000..34a91e0 --- /dev/null +++ b/freedrawview/src/main/java/com/rm/freedrawview/Point.java @@ -0,0 +1,57 @@ +package com.rm.freedrawview; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.Serializable; + +/** + * Created by Riccardo Moro on 9/25/2016. + */ + +class Point implements Parcelable, Serializable { + + static final long serialVersionUID = 42L; + + float x, y; + + Point() { + x = y = -1; + } + + @Override + public String toString() { + return "" + x + " : " + y + " - "; + } + + + // Parcelable stuff + private Point(Parcel in) { + x = in.readFloat(); + y = in.readFloat(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeFloat(x); + dest.writeFloat(y); + } + + // Parcelable CREATOR class + public static final Creator CREATOR = new Creator() { + @Override + public Point createFromParcel(Parcel in) { + return new Point(in); + } + + @Override + public Point[] newArray(int size) { + return new Point[size]; + } + }; +} diff --git a/freedrawview/src/main/java/com/rm/freedrawview/ResizeBehaviour.java b/freedrawview/src/main/java/com/rm/freedrawview/ResizeBehaviour.java new file mode 100644 index 0000000..2afcc88 --- /dev/null +++ b/freedrawview/src/main/java/com/rm/freedrawview/ResizeBehaviour.java @@ -0,0 +1,18 @@ +package com.rm.freedrawview; + +/** + * Created by Riccardo Moro on 11/6/2016. + */ + +/** + * When RMFreeDrawView dimensions are changed, you can apply one of the following behaviours + * {@link #CLEAR} - It just clear the View from every previous paint + * {@link #FIT_XY} - It stretch the content to fit the new dimensions + * {@link #CROP} - Keep the exact position of the previous point, if the dimensions changes, there + * may be points outside the view and not visible + */ +public enum ResizeBehaviour { + CLEAR, + FIT_XY, + CROP +} diff --git a/freedrawview/src/main/res/values/attrs.xml b/freedrawview/src/main/res/values/attrs.xml new file mode 100644 index 0000000..7ef2e3d --- /dev/null +++ b/freedrawview/src/main/res/values/attrs.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cfedeff..41c78d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,21 +1,20 @@ [versions] androidMavenGradlePlugin = "2.1" -appcompat = "1.6.1" +appcompat = "1.7.0" constraintlayout = "2.1.4" -coreKtx = "1.7.0" +coreKtx = "1.13.1" dokkaGradlePlugin = "1.7.20" -freedrawview = "1.1.2" -gradle = "8.2.2" +gradle = "8.3.2" ink = "1.0.0" -lifecycleExtensions = "2.0.0" -lifecycleViewmodelKtx = "2.6.1" +lifecycleExtensions = "2.2.0" +lifecycleViewmodelKtx = "2.8.2" loco = "0.3.1" -loggingInterceptor = "3.10.0" -material = "1.9.0" +loggingInterceptor = "4.12.0" +material = "1.12.0" multidex = "1.0.3" -retrofit = "2.4.0" -rxandroid = "2.0.2" -rxjava = "2.1.12" +retrofit = "2.9.0" +rxandroid = "2.1.1" +rxjava = "2.2.21" updraft = "2.2.4" kotlin = "1.9.23" vanniktechMavenPublish = "0.24.0" @@ -29,7 +28,6 @@ constraintlayout = { module = "androidx.constraintlayout:constraintlayout", vers converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } dokka-gradle-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokkaGradlePlugin" } -freedrawview = { module = "com.rm:freedrawview", version.ref = "freedrawview" } ink = { module = "com.simplify:ink", version.ref = "ink" } kotlin-stdlib-jdk7 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin" } lifecycle-compiler = { module = "androidx.lifecycle:lifecycle-compiler", version.ref = "lifecycleExtensions" } @@ -46,6 +44,8 @@ tools-build-gradle = { module = "com.android.tools.build:gradle", version.ref = kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } loco = { module = "com.appswithlove.loco:loco", version = "0.3.1" } updraft = { module = "com.appswithlove.updraft:updraft", version.ref = "updraft" } +androidx-startup = { module = "androidx.startup:startup-runtime", version = "1.1.1" } +androidx-annotation = { module = "androidx.annotation:annotation", version = "1.8.0" } [plugins] androidApplication = { id = "com.android.application", version.ref = "gradle" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0f56903..b8e2ce0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Nov 23 11:31:29 CET 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle index cf5b7e1..1e677b8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,6 @@ pluginManagement { google() gradlePluginPortal() mavenCentral() - maven { url 'https://maven.scijava.org/content/repositories/public/' } } } @@ -11,10 +10,9 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - maven { url 'https://maven.scijava.org/content/repositories/public/' } } } rootProject.name = "updraft-sdk-android" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") -include ':app', ':updraft-sdk' +include ':app', ':updraft-sdk', ':freedrawview' diff --git a/updraft-sdk/build.gradle b/updraft-sdk/build.gradle index b2b631d..3a018bc 100644 --- a/updraft-sdk/build.gradle +++ b/updraft-sdk/build.gradle @@ -45,6 +45,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(':freedrawview') implementation libs.appcompat implementation libs.material @@ -61,8 +62,8 @@ dependencies { implementation libs.lifecycle.extensions implementation libs.lifecycle.runtime annotationProcessor libs.lifecycle.compiler + implementation libs.androidx.startup - implementation libs.freedrawview implementation libs.core.ktx implementation libs.lifecycle.viewmodel.ktx implementation libs.kotlin.stdlib.jdk7 diff --git a/updraft-sdk/src/main/AndroidManifest.xml b/updraft-sdk/src/main/AndroidManifest.xml index fef41cb..d401c87 100644 --- a/updraft-sdk/src/main/AndroidManifest.xml +++ b/updraft-sdk/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> @@ -8,5 +9,15 @@ android:name="com.appswithlove.updraft.feedback.FeedbackActivity" android:theme="@style/Theme.Transparent" android:exported="true" /> + + + + + diff --git a/updraft-sdk/src/main/java/com/appswithlove/updraft/Updraft.kt b/updraft-sdk/src/main/java/com/appswithlove/updraft/Updraft.kt index faac116..7d3d275 100644 --- a/updraft-sdk/src/main/java/com/appswithlove/updraft/Updraft.kt +++ b/updraft-sdk/src/main/java/com/appswithlove/updraft/Updraft.kt @@ -6,6 +6,7 @@ import com.appswithlove.updraft.interactor.CheckFeedbackEnabledInteractor import com.appswithlove.updraft.interactor.CheckUpdateInteractor import com.appswithlove.updraft.manager.AppUpdateManager import com.appswithlove.updraft.manager.CheckFeedbackEnabledManager +import com.appswithlove.updraft.manager.CurrentActivityManger import com.appswithlove.updraft.manager.ShakeDetectorManager import com.appswithlove.updraft.presentation.UpdraftSdkUi @@ -28,7 +29,7 @@ class Updraft private constructor(application: Application, private val settings init { val checkUpdateInteractor = CheckUpdateInteractor(apiWrapper) - val updraftSdkUi = UpdraftSdkUi(application, settings) + val updraftSdkUi = UpdraftSdkUi(CurrentActivityManger.INSTANCE, settings) mAppUpdateManager = AppUpdateManager(checkUpdateInteractor, updraftSdkUi) val checkFeedbackEnabledInteractor = CheckFeedbackEnabledInteractor( apiWrapper, application diff --git a/updraft-sdk/src/main/java/com/appswithlove/updraft/manager/CurrentActivityManger.kt b/updraft-sdk/src/main/java/com/appswithlove/updraft/manager/CurrentActivityManger.kt new file mode 100644 index 0000000..0218b67 --- /dev/null +++ b/updraft-sdk/src/main/java/com/appswithlove/updraft/manager/CurrentActivityManger.kt @@ -0,0 +1,75 @@ +package com.appswithlove.updraft.manager + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.Application +import android.os.Bundle + +@SuppressLint("StaticFieldLeak") +class CurrentActivityManger private constructor() : Application.ActivityLifecycleCallbacks { + + companion object { + val INSTANCE: CurrentActivityManger by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + CurrentActivityManger() + } + } + + private var currentActivity: Activity? = null + private var currentLifecycle: LifecycleState? = null + + private val listeners: MutableSet = mutableSetOf() + + fun addListener(listener: CurrentActivityListener) { + listeners.add(listener) + dispatchCurrentActivityState() + } + + fun removeListener(listener: CurrentActivityListener) { + listeners.remove(listener) + } + + override fun onActivityCreated(activity: Activity, bundle: Bundle?) { + currentActivity = activity + } + + override fun onActivityStarted(activity: Activity) {} + override fun onActivityResumed(activity: Activity) { + currentLifecycle = LifecycleState.Resumed + dispatchCurrentActivityState() + } + + override fun onActivityPaused(activity: Activity) { + currentLifecycle = LifecycleState.Paused + dispatchCurrentActivityState() + } + + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {} + override fun onActivityDestroyed(activity: Activity) { + currentActivity = null + currentLifecycle = null + } + + private fun dispatchCurrentActivityState() { + val activity = currentActivity + if (activity != null) { + listeners.forEach { listener -> + if (currentLifecycle == LifecycleState.Resumed) { + listener.onActivityResumed(activity) + } else if (currentLifecycle == LifecycleState.Paused) { + listener.onActivityPaused(activity) + } + } + } + } + + interface CurrentActivityListener { + fun onActivityResumed(activity: Activity) + fun onActivityPaused(activity: Activity) + } + + private enum class LifecycleState { + Resumed, + Paused + } +} \ No newline at end of file diff --git a/updraft-sdk/src/main/java/com/appswithlove/updraft/manager/SdkInitializer.kt b/updraft-sdk/src/main/java/com/appswithlove/updraft/manager/SdkInitializer.kt new file mode 100644 index 0000000..0f27ee0 --- /dev/null +++ b/updraft-sdk/src/main/java/com/appswithlove/updraft/manager/SdkInitializer.kt @@ -0,0 +1,17 @@ +package com.appswithlove.updraft.manager + +import android.annotation.SuppressLint +import android.app.Application +import android.content.Context +import androidx.startup.Initializer + +@SuppressLint("Unused") +class SdkInitializer : Initializer { + override fun create(context: Context): CurrentActivityManger { + val activityManger = CurrentActivityManger.INSTANCE + (context.applicationContext as? Application)?.registerActivityLifecycleCallbacks(activityManger) + return activityManger + } + + override fun dependencies(): List>> = emptyList() +} diff --git a/updraft-sdk/src/main/java/com/appswithlove/updraft/presentation/UpdraftSdkUi.kt b/updraft-sdk/src/main/java/com/appswithlove/updraft/presentation/UpdraftSdkUi.kt index c46eb69..f72403c 100644 --- a/updraft-sdk/src/main/java/com/appswithlove/updraft/presentation/UpdraftSdkUi.kt +++ b/updraft-sdk/src/main/java/com/appswithlove/updraft/presentation/UpdraftSdkUi.kt @@ -2,27 +2,27 @@ package com.appswithlove.updraft.presentation import android.app.Activity import android.app.AlertDialog -import android.app.Application -import android.app.Application.ActivityLifecycleCallbacks import android.content.Context import android.content.DialogInterface import android.content.Intent import android.graphics.Bitmap import android.net.Uri -import android.os.Bundle import android.widget.Toast import com.appswithlove.updraft.BuildConfig import com.appswithlove.updraft.R import com.appswithlove.updraft.Settings import com.appswithlove.updraft.Updraft.Companion.getInstance import com.appswithlove.updraft.feedback.FeedbackActivity +import com.appswithlove.updraft.manager.CurrentActivityManger import java.io.IOException /** * Created by satori on 3/27/18. */ -class UpdraftSdkUi(application: Application, private val mSettings: Settings) : - ActivityLifecycleCallbacks { +class UpdraftSdkUi( + currentActivityManger: CurrentActivityManger, + private val mSettings: Settings, +) : CurrentActivityManger.CurrentActivityListener { private var mCurrentActivity: Activity? = null private var mShowDialogPending = false private var mPendingUrl: String? = null @@ -36,6 +36,9 @@ class UpdraftSdkUi(application: Application, private val mSettings: Settings) : var isCurrentlyShowingFeedback = false private set + init { + currentActivityManger.addListener(this) + } fun setListener(listener: Listener?) { mListener = listener @@ -167,8 +170,15 @@ class UpdraftSdkUi(application: Application, private val mSettings: Settings) : bitmap.recycle() } - override fun onActivityCreated(activity: Activity, bundle: Bundle?) {} - override fun onActivityStarted(activity: Activity) {} + + fun closeFeedback() { + mCurrentActivity.apply { + if (this is FeedbackActivity && !this.isFinishing()) { + this.finish() + } + } + } + override fun onActivityResumed(activity: Activity) { mCurrentActivity = activity if (mShowStartAlertDialogPending) { @@ -195,27 +205,11 @@ class UpdraftSdkUi(application: Application, private val mSettings: Settings) : mCurrentActivity = null } - fun closeFeedback() { - mCurrentActivity.apply { - if (this is FeedbackActivity && !this.isFinishing()) { - this.finish() - } - } - } - - override fun onActivityStopped(activity: Activity) {} - override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {} - override fun onActivityDestroyed(activity: Activity) {} - companion object { private const val TAG = "UpdraftSdkUi" private const val FILENAME = "UPDRAFT_SCREENSHOT.png" } - init { - application.registerActivityLifecycleCallbacks(this) - } - interface Listener { fun onOkClicked(url: String) }