Source added
1
image-editor/app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
20
image-editor/app/build.gradle
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
plugins {
|
||||
id("signal-sample-app")
|
||||
id("com.google.devtools.ksp")
|
||||
alias(libs.plugins.compose.compiler)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.signal.imageeditor.app"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "org.signal.imageeditor.app"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":image-editor"))
|
||||
|
||||
implementation(libs.glide.glide)
|
||||
ksp(libs.glide.ksp)
|
||||
}
|
||||
21
image-editor/app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# 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 *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
27
image-editor/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Signal">
|
||||
|
||||
<activity
|
||||
android:name="org.signal.imageeditor.app.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,272 @@
|
|||
package org.signal.imageeditor.app;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.imageeditor.app.renderers.UriRenderer;
|
||||
import org.signal.imageeditor.app.renderers.UrlRenderer;
|
||||
import org.signal.imageeditor.core.ImageEditorView;
|
||||
import org.signal.imageeditor.core.RendererContext;
|
||||
import org.signal.imageeditor.core.model.EditorElement;
|
||||
import org.signal.imageeditor.core.model.EditorModel;
|
||||
import org.signal.imageeditor.core.renderers.MultiLineTextRenderer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class MainActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
private ImageEditorView imageEditorView;
|
||||
private Menu menu;
|
||||
|
||||
private final RendererContext.TypefaceProvider typefaceProvider = (context, renderer, invalidate) -> {
|
||||
if (Build.VERSION.SDK_INT < 26) {
|
||||
return Typeface.create(Typeface.DEFAULT, Typeface.BOLD);
|
||||
} else {
|
||||
return new Typeface.Builder("")
|
||||
.setFallback("sans-serif")
|
||||
.setWeight(900)
|
||||
.build();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
toolbar.setTitle(R.string.app_name_short);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
imageEditorView = findViewById(R.id.image_editor);
|
||||
|
||||
imageEditorView.setTypefaceProvider(typefaceProvider);
|
||||
|
||||
imageEditorView.setUndoRedoStackListener((undoAvailable, redoAvailable) -> {
|
||||
Log.d("ALAN", String.format("Undo/Redo available: %s, %s", undoAvailable ? "Y" : "N", redoAvailable ? "Y" : "N"));
|
||||
if (menu == null) return;
|
||||
MenuItem undo = menu.findItem(R.id.action_undo);
|
||||
MenuItem redo = menu.findItem(R.id.action_redo);
|
||||
if (undo != null) undo.setVisible(undoAvailable);
|
||||
if (redo != null) redo.setVisible(redoAvailable);
|
||||
});
|
||||
|
||||
EditorModel model = null;
|
||||
if (savedInstanceState != null) {
|
||||
model = savedInstanceState.getParcelable("MODEL");
|
||||
Log.d("ALAN", "Restoring instance " + (model != null ? model.hashCode() : 0));
|
||||
}
|
||||
|
||||
if (model == null) {
|
||||
model = initialModel();
|
||||
Log.d("ALAN", "New instance created " + model.hashCode());
|
||||
}
|
||||
|
||||
imageEditorView.setModel(model);
|
||||
|
||||
imageEditorView.setTapListener(new ImageEditorView.TapListener() {
|
||||
@Override
|
||||
public void onEntityDown(@Nullable EditorElement editorElement) {
|
||||
Log.d("ALAN", "Entity down " + editorElement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntitySingleTap(@Nullable EditorElement editorElement) {
|
||||
Log.d("ALAN", "Entity single tapped " + editorElement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityDoubleTap(@NonNull EditorElement editorElement) {
|
||||
Log.d("ALAN", "Entity double tapped " + editorElement);
|
||||
if (editorElement.getRenderer() instanceof MultiLineTextRenderer) {
|
||||
imageEditorView.startTextEditing(editorElement);
|
||||
} else {
|
||||
imageEditorView.deleteElement(editorElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static EditorModel initialModel() {
|
||||
|
||||
EditorModel model = EditorModel.create(0xFF000000);
|
||||
|
||||
EditorElement image = new EditorElement(new UrlRenderer("https://cdn.aarp.net/content/dam/aarp/home-and-family/your-home/2018/06/1140-house-inheriting.imgcache.rev68c065601779c5d76b913cf9ec3a977e.jpg"));
|
||||
image.getFlags().setSelectable(false).persist();
|
||||
model.addElement(image);
|
||||
|
||||
EditorElement elementC = new EditorElement(new UrlRenderer("https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/SNice.svg/220px-SNice.svg.png"));
|
||||
elementC.getLocalMatrix().postScale(0.2f, 0.2f);
|
||||
//elementC.getLocalMatrix().postRotate(30);
|
||||
model.addElement(elementC);
|
||||
|
||||
EditorElement elementE = new EditorElement(new UrlRenderer("https://www.vitalessentialsraw.com/assets/images/background-images/laying-grey-cat.png"));
|
||||
elementE.getLocalMatrix().postScale(0.2f, 0.2f);
|
||||
//elementE.getLocalMatrix().postRotate(60);
|
||||
model.addElement(elementE);
|
||||
|
||||
EditorElement elementD = new EditorElement(new UrlRenderer("https://petspluslubbocktx.com/files/2016/11/DC-Cat-Weight-Management.png"));
|
||||
elementD.getLocalMatrix().postScale(0.2f, 0.2f);
|
||||
//elementD.getLocalMatrix().postRotate(60);
|
||||
model.addElement(elementD);
|
||||
|
||||
EditorElement elementF = new EditorElement(new UrlRenderer("https://purepng.com/public/uploads/large/purepng.com-black-top-hathatsstandard-sizeblacktop-14215263591972x0zh.png"));
|
||||
elementF.getLocalMatrix().postScale(0.2f, 0.2f);
|
||||
//elementF.getLocalMatrix().postRotatF(60);
|
||||
model.addElement(elementF);
|
||||
|
||||
EditorElement elementG = new EditorElement(new UriRenderer(Uri.parse("file:///android_asset/food/apple.png")));
|
||||
elementG.getLocalMatrix().postScale(0.2f, 0.2f);
|
||||
//elementG.getLocalMatrix().postRotatG(60);
|
||||
model.addElement(elementG);
|
||||
|
||||
EditorElement elementH = new EditorElement(new MultiLineTextRenderer("Hello, World!", 0xff0000ff, MultiLineTextRenderer.Mode.REGULAR));
|
||||
//elementH.getLocalMatrix().postScale(0.2f, 0.2f);
|
||||
model.addElement(elementH);
|
||||
|
||||
EditorElement elementH2 = new EditorElement(new MultiLineTextRenderer("Hello, World 2!", 0xff0000ff, MultiLineTextRenderer.Mode.REGULAR));
|
||||
//elementH.getLocalMatrix().postScale(0.2f, 0.2f);
|
||||
model.addElement(elementH2);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putParcelable("MODEL", imageEditorView.getModel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.action_menu, menu);
|
||||
this.menu = menu;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == R.id.action_undo) {
|
||||
imageEditorView.getModel().undo();
|
||||
Log.d(TAG, String.format("Model is %s", imageEditorView.getModel().isChanged() ? "changed" : "unchanged"));
|
||||
return true;
|
||||
} else if (itemId == R.id.action_redo) {
|
||||
imageEditorView.getModel().redo();
|
||||
return true;
|
||||
} else if (itemId == R.id.action_crop) {
|
||||
imageEditorView.setMode(ImageEditorView.Mode.MoveAndResize);
|
||||
imageEditorView.getModel().startCrop();
|
||||
return true;
|
||||
} else if (itemId == R.id.action_done) {
|
||||
imageEditorView.setMode(ImageEditorView.Mode.MoveAndResize);
|
||||
imageEditorView.getModel().doneCrop();
|
||||
return true;
|
||||
} else if (itemId == R.id.action_draw) {
|
||||
imageEditorView.setDrawingBrushColor(0xffffff00);
|
||||
imageEditorView.startDrawing(0.02f, Paint.Cap.ROUND, false);
|
||||
return true;
|
||||
} else if (itemId == R.id.action_rotate_left_90) {
|
||||
imageEditorView.getModel().rotate90anticlockwise();
|
||||
return true;
|
||||
} else if (itemId == R.id.action_flip_horizontal) {
|
||||
imageEditorView.getModel().flipHorizontal();
|
||||
return true;
|
||||
} else if (itemId == R.id.action_edit_text) {
|
||||
editText();
|
||||
return true;
|
||||
} else if (itemId == R.id.action_lock_crop_aspect) {
|
||||
imageEditorView.getModel().setCropAspectLock(!imageEditorView.getModel().isCropAspectLocked());
|
||||
return true;
|
||||
} else if (itemId == R.id.action_save) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED)
|
||||
{
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE },
|
||||
0);
|
||||
} else {
|
||||
Bitmap bitmap = imageEditorView.getModel().render(this, typefaceProvider);
|
||||
try {
|
||||
Uri uri = saveBmp(bitmap);
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(uri, "image/*");
|
||||
startActivity(intent);
|
||||
|
||||
} finally {
|
||||
bitmap.recycle();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void editText() {
|
||||
imageEditorView.getModel().getRoot().findElement(new Matrix(), new Matrix(), (element, inverseMatrix) -> {
|
||||
if (element.getRenderer() instanceof MultiLineTextRenderer) {
|
||||
imageEditorView.startTextEditing(element);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private Uri saveBmp(Bitmap bitmap) {
|
||||
String path = Environment.getExternalStorageDirectory().toString();
|
||||
|
||||
File filePath = new File(path);
|
||||
File imageEditor = new File(filePath, "ImageEditor");
|
||||
if (!imageEditor.exists()) {
|
||||
imageEditor.mkdir();
|
||||
}
|
||||
|
||||
int counter = 0;
|
||||
File file;
|
||||
do {
|
||||
counter++;
|
||||
file = new File(imageEditor, String.format(Locale.US, "ImageEditor_%03d.jpg", counter));
|
||||
} while (file.exists());
|
||||
|
||||
try {
|
||||
try (OutputStream stream = new FileOutputStream(file)) {
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
||||
}
|
||||
return Uri.parse(MediaStore.Images.Media.insertImage(getContentResolver(), file.getAbsolutePath(), file.getName(), file.getName()));
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package org.signal.imageeditor.app;
|
||||
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
|
||||
@GlideModule
|
||||
public class TheAppGlideModule extends AppGlideModule {
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package org.signal.imageeditor.app.renderers;
|
||||
|
||||
import org.signal.imageeditor.core.Bounds;
|
||||
import org.signal.imageeditor.core.Renderer;
|
||||
|
||||
public abstract class StandardHitTestRenderer implements Renderer {
|
||||
|
||||
@Override
|
||||
public boolean hitTest(float x, float y) {
|
||||
return Bounds.contains(x, y);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package org.signal.imageeditor.app.renderers;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.RectF;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.signal.imageeditor.core.Bounds;
|
||||
import org.signal.imageeditor.core.Renderer;
|
||||
import org.signal.imageeditor.core.RendererContext;
|
||||
|
||||
public final class UriRenderer implements Renderer, Parcelable {
|
||||
|
||||
private final Uri imageUri;
|
||||
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
private final Matrix temp1 = new Matrix();
|
||||
private final Matrix temp2 = new Matrix();
|
||||
|
||||
@Nullable
|
||||
private Bitmap bitmap;
|
||||
|
||||
public UriRenderer(Uri imageUri) {
|
||||
this.imageUri = imageUri;
|
||||
paint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
private UriRenderer(Parcel in) {
|
||||
this(Uri.parse(in.readString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(@NonNull RendererContext rendererContext) {
|
||||
if (bitmap != null && bitmap.isRecycled()) bitmap = null;
|
||||
|
||||
if (bitmap == null) {
|
||||
Glide.with(rendererContext.context)
|
||||
.asBitmap()
|
||||
.load(imageUri)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(new SimpleTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
setBitmap(resource);
|
||||
rendererContext.rendererReady.onReady(UriRenderer.this, cropMatrix(resource), new Point(resource.getWidth(), resource.getHeight()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
rendererContext.save();
|
||||
rendererContext.canvasMatrix.concat(temp1);
|
||||
|
||||
// FYI units are pixels at this point.
|
||||
paint.setAlpha(rendererContext.getAlpha(255));
|
||||
rendererContext.canvas.drawBitmap(bitmap, 0, 0, paint);
|
||||
rendererContext.restore();
|
||||
} else {
|
||||
rendererContext.canvas.drawRect(-0.5f, -0.5f, 0.5f, 0.5f, paint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hitTest(float x, float y) {
|
||||
return pixelNotAlpha(x, y);
|
||||
}
|
||||
|
||||
private boolean pixelNotAlpha(float x, float y) {
|
||||
if (bitmap == null) return false;
|
||||
|
||||
temp1.invert(temp2);
|
||||
|
||||
float[] onBmp = new float[2];
|
||||
temp2.mapPoints(onBmp, new float[]{ x, y });
|
||||
|
||||
int xInt = (int) onBmp[0];
|
||||
int yInt = (int) onBmp[1];
|
||||
|
||||
if (xInt >= 0 && xInt < bitmap.getWidth() && yInt >= 0 && yInt < bitmap.getHeight()) {
|
||||
return (bitmap.getPixel(xInt, yInt) & 0xff000000) != 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setBitmap(Bitmap bitmap) {
|
||||
if (bitmap != null) {
|
||||
this.bitmap = bitmap;
|
||||
RectF from = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
temp1.setRectToRect(from, Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
private static Matrix cropMatrix(Bitmap bitmap) {
|
||||
Matrix matrix = new Matrix();
|
||||
if (bitmap.getWidth() > bitmap.getHeight()) {
|
||||
matrix.preScale(1, ((float) bitmap.getHeight()) / bitmap.getWidth());
|
||||
} else {
|
||||
matrix.preScale(((float) bitmap.getWidth()) / bitmap.getHeight(), 1);
|
||||
}
|
||||
return matrix;
|
||||
}
|
||||
|
||||
public static final Creator<UriRenderer> CREATOR = new Creator<UriRenderer>() {
|
||||
@Override
|
||||
public UriRenderer createFromParcel(Parcel in) {
|
||||
return new UriRenderer(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriRenderer[] newArray(int size) {
|
||||
return new UriRenderer[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(imageUri.toString());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
package org.signal.imageeditor.app.renderers;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Parcel;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.signal.imageeditor.core.Bounds;
|
||||
import org.signal.imageeditor.core.RendererContext;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public final class UrlRenderer extends StandardHitTestRenderer {
|
||||
|
||||
private static final String TAG = "UrlRenderer";
|
||||
private final String url;
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
private final Matrix temp1 = new Matrix();
|
||||
private final Matrix temp2 = new Matrix();
|
||||
|
||||
private Bitmap bitmap;
|
||||
|
||||
public UrlRenderer(@Nullable String url) {
|
||||
this.url = url;
|
||||
paint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
private UrlRenderer(Parcel in) {
|
||||
this(in.readString());
|
||||
}
|
||||
|
||||
public static final Creator<UrlRenderer> CREATOR = new Creator<UrlRenderer>() {
|
||||
@Override
|
||||
public UrlRenderer createFromParcel(Parcel in) {
|
||||
return new UrlRenderer(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UrlRenderer[] newArray(int size) {
|
||||
return new UrlRenderer[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void render(@NonNull RendererContext rendererContext) {
|
||||
if (bitmap != null && bitmap.isRecycled()) bitmap = null;
|
||||
|
||||
if (bitmap == null) {
|
||||
if (rendererContext.isBlockingLoad()) {
|
||||
try {
|
||||
setBitmap(rendererContext, Glide.with(rendererContext.context)
|
||||
.asBitmap()
|
||||
.load(url)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.submit()
|
||||
.get());
|
||||
} catch (ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
Glide.with(rendererContext.context)
|
||||
.asBitmap()
|
||||
.load(url)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(new SimpleTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
setBitmap(rendererContext, resource);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
rendererContext.save();
|
||||
rendererContext.getCurrent(temp2);
|
||||
temp2.preConcat(temp1);
|
||||
rendererContext.canvas.concat(temp1);
|
||||
|
||||
// FYI units are pixels at this point.
|
||||
paint.setAlpha(rendererContext.getAlpha(255));
|
||||
rendererContext.canvas.drawBitmap(bitmap, 0, 0, paint);
|
||||
rendererContext.restore();
|
||||
} else {
|
||||
if (rendererContext.isBlockingLoad()) {
|
||||
Log.e(TAG, "blocking but drawing null :(");
|
||||
}
|
||||
rendererContext.canvas.drawRect(Bounds.FULL_BOUNDS, paint);
|
||||
}
|
||||
|
||||
drawDebugInfo(rendererContext);
|
||||
}
|
||||
|
||||
private void drawDebugInfo(RendererContext rendererContext) {
|
||||
// float width = bitmap.getWidth();
|
||||
// float height = bitmap.getWidth();
|
||||
|
||||
//RectF bounds = new RectF(Bounds.LEFT, Bounds.TOP/2f, Bounds.RIGHT,Bounds.BOTTOM/2f );//Bounds.FULL_BOUNDS;
|
||||
RectF bounds = Bounds.FULL_BOUNDS;
|
||||
|
||||
Paint paint = new Paint();
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setColor(0xffffff00);
|
||||
rendererContext.canvas.drawRect(bounds, paint);
|
||||
|
||||
RectF fullBounds = new RectF();
|
||||
rendererContext.mapRect(fullBounds, bounds);
|
||||
|
||||
rendererContext.save();
|
||||
|
||||
RectF dst = new RectF();
|
||||
rendererContext.mapRect(dst, bounds);
|
||||
paint.setColor(0xffff00ff);
|
||||
rendererContext.canvasMatrix.setToIdentity();
|
||||
rendererContext.canvas.drawRect(dst, paint);
|
||||
|
||||
rendererContext.restore();
|
||||
|
||||
rendererContext.save();
|
||||
|
||||
Matrix unrotated = new Matrix();
|
||||
rendererContext.getCurrent(unrotated);
|
||||
findUnrotateMatrix(unrotated);
|
||||
|
||||
Matrix rotated = new Matrix();
|
||||
rendererContext.getCurrent(rotated);
|
||||
findRotateMatrix(rotated);
|
||||
|
||||
RectF dst2 = new RectF();
|
||||
unrotated.mapRect(dst2, Bounds.FULL_BOUNDS); // works because square, do we need rotated here?
|
||||
|
||||
float scaleX = Bounds.FULL_BOUNDS.width() / dst2.width();
|
||||
float scaleY = Bounds.FULL_BOUNDS.height() / dst2.height();
|
||||
|
||||
rendererContext.canvasMatrix.concat(unrotated);
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.setScale(scaleX, scaleY);
|
||||
rendererContext.canvasMatrix.concat(matrix);
|
||||
|
||||
paint.setColor(0xff0000ff);
|
||||
rendererContext.canvas.drawRect(bounds, paint);
|
||||
|
||||
rendererContext.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a scaled/rotated and transformed matrix, extract just the rotate and reverse it.
|
||||
*/
|
||||
private void findUnrotateMatrix(@NonNull Matrix matrix) {
|
||||
float[] values = new float[9];
|
||||
|
||||
matrix.getValues(values);
|
||||
|
||||
float xScale = (float) Math.sqrt(values[0] * values[0] + values[3] * values[3]);
|
||||
float yScale = (float) Math.sqrt(values[1] * values[1] + values[4] * values[4]);
|
||||
|
||||
values[0] /= xScale;
|
||||
values[1] /= -yScale;
|
||||
values[2] = 0;
|
||||
|
||||
values[3] /= -xScale;
|
||||
values[4] /= yScale;
|
||||
values[5] = 0;
|
||||
|
||||
matrix.setValues(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a scaled/rotated and transformed matrix, extract just the rotate and reverse it.
|
||||
*/
|
||||
private void findRotateMatrix(@NonNull Matrix matrix) {
|
||||
float[] values = new float[9];
|
||||
|
||||
matrix.getValues(values);
|
||||
|
||||
float xScale = (float) Math.sqrt(values[0] * values[0] + values[3] * values[3]);
|
||||
float yScale = (float) Math.sqrt(values[1] * values[1] + values[4] * values[4]);
|
||||
|
||||
values[0] /= xScale;
|
||||
values[1] /= yScale;
|
||||
values[2] = 0;
|
||||
|
||||
values[3] /= xScale;
|
||||
values[4] /= yScale;
|
||||
values[5] = 0;
|
||||
|
||||
matrix.setValues(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hitTest(float x, float y) {
|
||||
return super.hitTest(x, y) && pixelNotAlpha(x, y);
|
||||
}
|
||||
|
||||
private boolean pixelNotAlpha(float x, float y) {
|
||||
if (bitmap == null) return false;
|
||||
|
||||
temp1.invert(temp2);
|
||||
|
||||
float[] onBmp = new float[2];
|
||||
temp2.mapPoints(onBmp, new float[]{ x, y });
|
||||
|
||||
int xInt = (int) onBmp[0];
|
||||
int yInt = (int) onBmp[1];
|
||||
|
||||
if (xInt >= 0 && xInt < bitmap.getWidth() && yInt >= 0 && yInt < bitmap.getHeight()) {
|
||||
return (bitmap.getPixel(xInt, yInt) & 0xff000000) != 0;
|
||||
} else {
|
||||
return xInt >= 0 && xInt <= bitmap.getWidth() && yInt >= 0 && yInt <= bitmap.getHeight();
|
||||
}
|
||||
}
|
||||
|
||||
private void setBitmap(@NonNull RendererContext rendererContext, @Nullable Bitmap bitmap) {
|
||||
this.bitmap = bitmap;
|
||||
if (bitmap != null) {
|
||||
RectF from = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
temp1.setRectToRect(from, Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER);
|
||||
rendererContext.rendererReady.onReady(this, cropMatrix(bitmap), new Point(bitmap.getWidth(), bitmap.getHeight()));
|
||||
}
|
||||
}
|
||||
|
||||
private void setBitmap(Bitmap bitmap) {
|
||||
if (bitmap != null) {
|
||||
this.bitmap = bitmap;
|
||||
RectF from = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
temp1.setRectToRect(from, Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
private static Matrix cropMatrix(Bitmap bitmap) {
|
||||
Matrix matrix = new Matrix();
|
||||
if (bitmap.getWidth() > bitmap.getHeight()) {
|
||||
matrix.preScale(1, ((float) bitmap.getHeight()) / bitmap.getWidth());
|
||||
} else {
|
||||
matrix.preScale(((float) bitmap.getWidth()) / bitmap.getHeight(), 1);
|
||||
}
|
||||
return matrix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M17,15h2V7c0,-1.1 -0.9,-2 -2,-2H9v2h8v8zM7,17V1H5v4H1v2h4v10c0,1.1 0.9,2 2,2h10v4h2v-4h4v-2H7z"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15,21h2v-2h-2v2zM19,9h2L21,7h-2v2zM3,5v14c0,1.1 0.9,2 2,2h4v-2L5,19L5,5h4L9,3L5,3c-1.1,0 -2,0.9 -2,2zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM11,23h2L13,1h-2v22zM19,17h2v-2h-2v2zM15,5h2L17,3h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2z"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M18.4,10.6C16.55,8.99 14.15,8 11.5,8c-4.65,0 -8.58,3.03 -9.96,7.22L3.9,16c1.05,-3.19 4.05,-5.5 7.6,-5.5 1.95,0 3.73,0.72 5.12,1.88L13,16h9V7l-3.6,3.6z"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M7.11,8.53L5.7,7.11C4.8,8.27 4.24,9.61 4.07,11h2.02c0.14,-0.87 0.49,-1.72 1.02,-2.47zM6.09,13L4.07,13c0.17,1.39 0.72,2.73 1.62,3.89l1.41,-1.42c-0.52,-0.75 -0.87,-1.59 -1.01,-2.47zM7.1,18.32c1.16,0.9 2.51,1.44 3.9,1.61L11,17.9c-0.87,-0.15 -1.71,-0.49 -2.46,-1.03L7.1,18.32zM13,4.07L13,1L8.45,5.55 13,10L13,6.09c2.84,0.48 5,2.94 5,5.91s-2.16,5.43 -5,5.91v2.02c3.95,-0.49 7,-3.85 7,-7.93s-3.05,-7.44 -7,-7.93z"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15.55,5.55L11,1v3.07C7.06,4.56 4,7.92 4,12s3.05,7.44 7,7.93v-2.02c-2.84,-0.48 -5,-2.94 -5,-5.91s2.16,-5.43 5,-5.91L11,10l4.55,-4.45zM19.93,11c-0.17,-1.39 -0.72,-2.73 -1.62,-3.89l-1.42,1.42c0.54,0.75 0.88,1.6 1.02,2.47h2.02zM13,17.9v2.02c1.39,-0.17 2.74,-0.71 3.9,-1.61l-1.44,-1.44c-0.75,0.54 -1.59,0.89 -2.46,1.03zM16.89,15.48l1.42,1.41c0.9,-1.16 1.45,-2.5 1.62,-3.89h-2.02c-0.14,0.87 -0.48,1.72 -1.02,2.48z"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12.5,8c-2.65,0 -5.05,0.99 -6.9,2.6L2,7v9h9l-3.62,-3.62c1.39,-1.16 3.16,-1.88 5.12,-1.88 3.54,0 6.55,2.31 7.6,5.5l2.37,-0.78C21.08,11.03 17.15,8 12.5,8z"/>
|
||||
</vector>
|
||||
26
image-editor/app/src/main/res/layout/main_activity.xml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:elevation="4dp"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
|
||||
|
||||
<org.signal.imageeditor.core.ImageEditorView
|
||||
android:id="@+id/image_editor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
62
image-editor/app/src/main/res/menu/action_menu.xml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_crop"
|
||||
android:icon="@drawable/ic_crop_black_24dp"
|
||||
android:title="@string/crop"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_done"
|
||||
android:icon="@drawable/ic_check_black_24dp"
|
||||
android:title="@string/done"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_undo"
|
||||
android:icon="@drawable/ic_undo_black_24dp"
|
||||
android:title="@string/undo"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_redo"
|
||||
android:icon="@drawable/ic_redo_black_24dp"
|
||||
android:title="@string/redo"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_save"
|
||||
android:icon="@drawable/ic_save_black_24dp"
|
||||
android:title="@string/save"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_draw"
|
||||
android:title="@string/draw"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_rotate_left_90"
|
||||
android:icon="@drawable/ic_rotate_left_black_24dp"
|
||||
android:title="@string/rotate_90_left"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_flip_horizontal"
|
||||
android:icon="@drawable/ic_flip_black_24dp"
|
||||
android:title="@string/flip_horizontal"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_edit_text"
|
||||
android:title="@string/edit_text"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_lock_crop_aspect"
|
||||
android:title="@string/lock_crop_aspect"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
</menu>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
image-editor/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
image-editor/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
image-editor/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
image-editor/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
image-editor/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
BIN
image-editor/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
BIN
image-editor/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
6
image-editor/app/src/main/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#008577</color>
|
||||
<color name="colorPrimaryDark">#00574B</color>
|
||||
<color name="colorAccent">#D81B60</color>
|
||||
</resources>
|
||||
17
image-editor/app/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<resources>
|
||||
<string name="app_name">Image Editor Sample App</string>
|
||||
<string name="app_name_short">Image Editor</string>
|
||||
|
||||
<string name="undo">Undo</string>
|
||||
<string name="redo">Redo</string>
|
||||
<string name="crop">Crop</string>
|
||||
<string name="done">Done</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="draw">Draw</string>
|
||||
<string name="rotate_90_right">Rotate 90 right</string>
|
||||
<string name="rotate_90_left">Rotate 90 left</string>
|
||||
<string name="flip_horizontal">Flip horizontal</string>
|
||||
<string name="flip_vertical">Flip vertical</string>
|
||||
<string name="edit_text">Edit Text</string>
|
||||
<string name="lock_crop_aspect">Lock crop aspect</string>
|
||||
</resources>
|
||||
9
image-editor/app/src/main/res/values/themes.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Signal" parent="Theme.MaterialComponents.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.example.imageeditor.app
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||