diff --git a/COPYING b/LICENSE similarity index 100% rename from COPYING rename to LICENSE diff --git a/app/build.gradle b/app/build.gradle index 1ee7312..b1a29cb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,39 +1,43 @@ apply plugin: 'com.android.application' android { compileSdkVersion 28 buildToolsVersion "29.0.3" defaultConfig { applicationId "anupam.acrylic" minSdkVersion 14 // Figure out stupid garbage Scoped Storage for 29+ // https://developer.android.com/preview/privacy/storage#scoped-storage //noinspection OldTargetApi targetSdkVersion 28 versionCode 18 versionName "2.4.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } compileOptions { encoding "UTF-8" sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } lintOptions { disable 'GoogleAppIndexingWarning' abortOnError false } + + buildFeatures { + viewBinding true + } } dependencies { implementation 'com.nabinbhandari.android:permissions:3.8' } diff --git a/app/src/main/java/anupam/acrylic/AboutActivity.java b/app/src/main/java/anupam/acrylic/AboutActivity.java index 25529bd..464a884 100644 --- a/app/src/main/java/anupam/acrylic/AboutActivity.java +++ b/app/src/main/java/anupam/acrylic/AboutActivity.java @@ -1,21 +1,23 @@ package anupam.acrylic; import android.app.Activity; import android.os.Bundle; import android.text.Html; import android.text.Spanned; import android.text.method.LinkMovementMethod; -import android.widget.TextView; + +import anupam.acrylic.databinding.ActivityAboutBinding; public class AboutActivity extends Activity { + ActivityAboutBinding aboutBinding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_about); + aboutBinding = ActivityAboutBinding.inflate(getLayoutInflater()); + setContentView(aboutBinding.getRoot()); Spanned htmlText = Html.fromHtml(getResources().getString(R.string.about_description)); - TextView aboutTextView = (TextView) findViewById(R.id.aboutTextView); - aboutTextView.setText(htmlText); - aboutTextView.setMovementMethod(LinkMovementMethod.getInstance()); + aboutBinding.aboutTextView.setText(htmlText); + aboutBinding.aboutTextView.setMovementMethod(LinkMovementMethod.getInstance()); } } diff --git a/app/src/main/java/anupam/acrylic/ColorPickerDialog.java b/app/src/main/java/anupam/acrylic/ColorPickerDialog.java index e15b797..90ab408 100644 --- a/app/src/main/java/anupam/acrylic/ColorPickerDialog.java +++ b/app/src/main/java/anupam/acrylic/ColorPickerDialog.java @@ -1,239 +1,239 @@ /* * Copyright (C) 2014 Valerio Bozzolan * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package anupam.acrylic; import android.annotation.SuppressLint; import android.app.Dialog; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.SweepGradient; import android.os.Bundle; import android.view.MotionEvent; import android.view.View; @SuppressLint("ClickableViewAccessibility") public class ColorPickerDialog extends Dialog { - private OnColorChangedListener mListener; - private int mInitialColor; + private final OnColorChangedListener mListener; + private final int mInitialColor; public ColorPickerDialog(Context context, OnColorChangedListener listener, int initialColor) { super(context); mListener = listener; mInitialColor = initialColor; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); OnColorChangedListener l = color -> { mListener.colorChanged(color); dismiss(); }; setContentView(new ColorPickerView(getContext(), l, mInitialColor)); setTitle(R.string.pick_color); } public interface OnColorChangedListener { void colorChanged(int color); } private static class ColorPickerView extends View { private static final int CENTER_X = 230; private static final int CENTER_Y = 230; private static final int CENTER_RADIUS = 100; private static final float PI = 3.1415926f; private final int[] mColors; - private Paint mPaint; - private Paint mCenterPaint; - private OnColorChangedListener mListener; + private final Paint mPaint; + private final Paint mCenterPaint; + private final OnColorChangedListener mListener; private boolean mTrackingCenter; private boolean mHighlightCenter; ColorPickerView(Context c, OnColorChangedListener l, int color) { super(c); mListener = l; mColors = new int[]{ 0xFFFF0000, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFF00FF00, 0xFFFFFF00, 0xFFFF0000 }; Shader s = new SweepGradient(0, 0, mColors, null); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setShader(s); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(32); mCenterPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCenterPaint.setColor(color); mCenterPaint.setStrokeWidth(EasyPaint.DEFAULT_BRUSH_SIZE); } @Override protected void onDraw(Canvas canvas) { float r = CENTER_X - mPaint.getStrokeWidth() * 0.5f - 30; canvas.translate(CENTER_X, CENTER_X); canvas.drawOval(new RectF(-r, -r, r, r), mPaint); canvas.drawCircle(0, 0, CENTER_RADIUS, mCenterPaint); if (mTrackingCenter) { int c = mCenterPaint.getColor(); mCenterPaint.setStyle(Paint.Style.STROKE); if (mHighlightCenter) { mCenterPaint.setAlpha(0xFF); } else { mCenterPaint.setAlpha(0x80); } canvas.drawCircle(0, 0, CENTER_RADIUS + mCenterPaint.getStrokeWidth(), mCenterPaint); mCenterPaint.setStyle(Paint.Style.FILL); mCenterPaint.setColor(c); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(CENTER_X * 2, CENTER_Y * 2); } private int floatToByte(float x) { return Math.round(x); } private int pinToByte(int n) { if (n < 0) { n = 0; } else if (n > 255) { n = 255; } return n; } private int ave(int s, int d, float p) { return s + Math.round(p * (d - s)); } private int interpColor(int[] colors, float unit) { if (unit <= 0) { return colors[0]; } if (unit >= 1) { return colors[colors.length - 1]; } float p = unit * (colors.length - 1); int i = (int) p; p -= i; // now p is just the fractional part [0...1) and i is the index int c0 = colors[i]; int c1 = colors[i + 1]; int a = ave(Color.alpha(c0), Color.alpha(c1), p); int r = ave(Color.red(c0), Color.red(c1), p); int g = ave(Color.green(c0), Color.green(c1), p); int b = ave(Color.blue(c0), Color.blue(c1), p); return Color.argb(a, r, g, b); } @SuppressWarnings("unused") private int rotateColor(int color, float rad) { - float deg = rad * 180 / 3.1415927f; + float deg = rad * 180 / PI; int r = Color.red(color); int g = Color.green(color); int b = Color.blue(color); ColorMatrix cm = new ColorMatrix(); ColorMatrix tmp = new ColorMatrix(); cm.setRGB2YUV(); tmp.setRotate(0, deg); cm.postConcat(tmp); tmp.setYUV2RGB(); cm.postConcat(tmp); final float[] a = cm.getArray(); int ir = floatToByte(a[0] * r + a[1] * g + a[2] * b); int ig = floatToByte(a[5] * r + a[6] * g + a[7] * b); int ib = floatToByte(a[10] * r + a[11] * g + a[12] * b); return Color.argb(Color.alpha(color), pinToByte(ir), pinToByte(ig), pinToByte(ib)); } @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX() - CENTER_X; float y = event.getY() - CENTER_Y; boolean inCenter = Math.sqrt(x * x + y * y) <= CENTER_RADIUS; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mTrackingCenter = inCenter; if (inCenter) { mHighlightCenter = true; invalidate(); break; } case MotionEvent.ACTION_MOVE: if (mTrackingCenter) { if (mHighlightCenter != inCenter) { mHighlightCenter = inCenter; invalidate(); } } else { float angle = (float) Math.atan2(y, x); // need to turn angle [-PI ... PI] into unit [0....1] float unit = angle / (2 * PI); if (unit < 0) { unit += 1; } mCenterPaint.setColor(interpColor(mColors, unit)); invalidate(); } break; case MotionEvent.ACTION_UP: if (mTrackingCenter) { if (inCenter) { mListener.colorChanged(mCenterPaint.getColor()); } mTrackingCenter = false; // so we draw w/o halo invalidate(); } break; } return true; } } } diff --git a/app/src/main/java/anupam/acrylic/EasyPaint.java b/app/src/main/java/anupam/acrylic/EasyPaint.java index d83d29d..7bdee72 100644 --- a/app/src/main/java/anupam/acrylic/EasyPaint.java +++ b/app/src/main/java/anupam/acrylic/EasyPaint.java @@ -1,714 +1,673 @@ /* * Copyright (C) 2014, 2016 Valerio Bozzolan & James Dearing (TheOpenSourceNinja) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package anupam.acrylic; import android.Manifest; import android.annotation.SuppressLint; import android.app.AlertDialog; -import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapShader; import android.graphics.BlurMaskFilter; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.EmbossMaskFilter; import android.graphics.MaskFilter; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffXfermode; import android.graphics.Shader; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.provider.MediaStore; import android.renderscript.Allocation; import android.renderscript.Element; import android.renderscript.RSIllegalArgumentException; import android.renderscript.RenderScript; import android.renderscript.ScriptIntrinsicBlur; import android.util.Log; import android.view.Display; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; import android.widget.SeekBar; -import android.widget.TextView; import android.widget.Toast; import com.nabinbhandari.android.permissions.PermissionHandler; import com.nabinbhandari.android.permissions.Permissions; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.Calendar; +import anupam.acrylic.databinding.BrushBinding; + @SuppressLint("ClickableViewAccessibility") public class EasyPaint extends GraphicsActivity implements ColorPickerDialog.OnColorChangedListener { private static final float TOUCH_TOLERANCE = 4; private static final int CHOOSE_IMAGE = 0; public static int DEFAULT_BRUSH_SIZE = 10; - private static int MAX_POINTERS = 10; + private static final int MAX_POINTERS = 10; private Paint mPaint; private MaskFilter mEmboss; private MaskFilter mBlur; private boolean doubleBackToExitPressedOnce = false; private MyView contentView; + BrushBinding brushBinding; private boolean waitingForBackgroundColor = false; //If true and colorChanged() is called, fill the background, else mPaint.setColor() private boolean extractingColor = false; //If this is true, the next touch event should extract a color rather than drawing a line. @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // it removes the title from the actionbar(more space for icons?) // this.getActionBar().setDisplayShowTitleEnabled(false); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); contentView = new MyView(this); setContentView(contentView); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setDither(true); mPaint.setColor(Color.GREEN); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeJoin(Paint.Join.ROUND); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setStrokeWidth(DEFAULT_BRUSH_SIZE); // Where did these magic numbers come from? What do they mean? Can I change them? ~TheOpenSourceNinja // Absolutely random numbers in order to see the emboss. asd! ~Valerio mEmboss = new EmbossMaskFilter(new float[]{1, 1, 1}, 0.4f, 6, 3.5f); mBlur = new BlurMaskFilter(5, BlurMaskFilter.Blur.NORMAL); if (isFirstTime()) { AlertDialog.Builder alert = new AlertDialog.Builder(this); alert.setTitle(R.string.app_name); alert.setMessage(R.string.app_description); alert.setNegativeButton(R.string.continue_fuck, (dialog, whichButton) -> Toast.makeText(getApplicationContext(), R.string.here_is_your_canvas, Toast.LENGTH_SHORT).show()); alert.show(); } else { Toast.makeText(getApplicationContext(), R.string.here_is_your_canvas, Toast.LENGTH_SHORT).show(); } loadFromIntents(); } @Override public void onBackPressed() { if (doubleBackToExitPressedOnce) { super.onBackPressed(); return; } this.doubleBackToExitPressedOnce = true; Toast.makeText(this, R.string.press_back_again, Toast.LENGTH_SHORT) .show(); new Handler().postDelayed(() -> doubleBackToExitPressedOnce = false, 3000); } public void colorChanged(int color) { if (waitingForBackgroundColor) { waitingForBackgroundColor = false; contentView.mBitmapBackground.eraseColor(color); //int[] colors = new int[1]; //colors[0] = color; //contentView.mBitmapBackground = Bitmap.createBitmap(colors, contentView.mBitmapBackground.getWidth(), contentView.mBitmapBackground.getHeight(), contentView.mBitmapBackground.getConfig()); } else { // Changes the color of the action bar when the pencil color is changed // TODO: Figure out how to deal with choosing white color /*ActionBar actionBar = getActionBar(); ColorDrawable colorDrawable = new ColorDrawable(color); actionBar.setBackgroundDrawable(colorDrawable);*/ mPaint.setColor(color); } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { mPaint.setXfermode(null); mPaint.setAlpha(0xFF); switch (item.getItemId()) { - case R.id.extract_color_menu: { + case R.id.extract_color_menu: Toast.makeText(getApplicationContext(), R.string.tap_to_extract_color, Toast.LENGTH_LONG).show(); extractingColor = true; return true; - } case R.id.brush_menu: mPaint.setShader(null); mPaint.setMaskFilter(null); return true; case R.id.palette_menu: new ColorPickerDialog(this, this, mPaint.getColor()).show(); return true; case R.id.emboss_submenu: mPaint.setShader(null); mPaint.setMaskFilter(mEmboss); return true; - case R.id.smudge_submenu: { + case R.id.smudge_submenu: /* I considered making this what happens when the blur_menu item is selected, but * that could surprise users who are used to blur_menu's previous functionality, so * I made this new smudge_menu item instead. I don't like calling it "Smudge" because * this isn't exactly the same as what Photoshop and GIMP refer to as "Smudge", but I * couldn't think of a better name that isn't "Blur". * ~TheOpenSourceNinja */ - if (Build.VERSION.SDK_INT >= 17) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { /* Basically what we're doing here is copying the entire foreground bitmap, * blurring it, then telling mPaint to use that instead of a solid color. */ RenderScript rs = RenderScript.create(getApplicationContext()); ScriptIntrinsicBlur script; try { script = ScriptIntrinsicBlur.create(rs, Element.RGBA_8888(rs)); } catch (RSIllegalArgumentException e) { script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); } script.setRadius(20f); //The radius must be between 0 and 25. Smaller radius means less blur. I just picked 20 randomly. ~TheOpenSourceNinja //copy the foreground: (n API level 18+, this will be really fast because it uses a shared memory model, thus not really copying everything) Allocation input = Allocation.createFromBitmap(rs, contentView.mBitmap); script.setInput(input); //allocate memory for the output: Allocation output = Allocation.createTyped(rs, input.getType()); //Blur the image: script.forEach(output); //Store the blurred image as a Bitmap object: Bitmap blurred = Bitmap.createBitmap(contentView.mBitmap); output.copyTo(blurred); //Tell mPaint to use the blurred image: Shader shader = new BitmapShader(blurred, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); mPaint.setShader(shader); } else { Toast.makeText(this.getApplicationContext(), R.string.ability_disabled_need_newer_api_level, Toast.LENGTH_LONG).show(); } return true; - } case R.id.blur_submenu: mPaint.setShader(null); mPaint.setMaskFilter(mBlur); return true; - case R.id.size_menu: { - LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View layout = inflater.inflate(R.layout.brush, - (ViewGroup) findViewById(R.id.root)); - AlertDialog.Builder builder = new AlertDialog.Builder(this) - .setView(layout); - builder.setTitle(R.string.choose_width); - final AlertDialog alertDialog = builder.create(); - alertDialog.show(); - SeekBar sb = (SeekBar) layout.findViewById(R.id.brushSizeSeekBar); - sb.setProgress(getStrokeSize()); - final TextView txt = (TextView) layout - .findViewById(R.id.sizeValueTextView); - txt.setText(String.format( - getResources().getString(R.string.your_selected_size_is), - getStrokeSize() + 1)); - sb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - public void onProgressChanged(SeekBar seekBar, - final int progress, boolean fromUser) { - // Do something here with new value - mPaint.setStrokeWidth(progress); - txt.setText(String.format( - getResources().getString( - R.string.your_selected_size_is), progress + 1)); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - // TODO Auto-generated method stub - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - // TODO Auto-generated method stub - } - }); + case R.id.size_menu: + setBrushSize(); return true; - } - case R.id.erase_menu: { - LayoutInflater inflater_e = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View layout_e = inflater_e.inflate(R.layout.brush, - (ViewGroup) findViewById(R.id.root)); - AlertDialog.Builder builder_e = new AlertDialog.Builder(this) - .setView(layout_e); - builder_e.setTitle(R.string.choose_width); - final AlertDialog alertDialog_e = builder_e.create(); - alertDialog_e.show(); - SeekBar sb_e = (SeekBar) layout_e.findViewById(R.id.brushSizeSeekBar); - sb_e.setProgress(getStrokeSize()); - final TextView txt_e = (TextView) layout_e - .findViewById(R.id.sizeValueTextView); - txt_e.setText(String.format( - getResources().getString(R.string.your_selected_size_is), - getStrokeSize() + 1)); - sb_e.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - public void onProgressChanged(SeekBar seekBar, - final int progress, boolean fromUser) { - // Do something here with new value - mPaint.setStrokeWidth(progress); - txt_e.setText(String.format( - getResources().getString( - R.string.your_selected_size_is), progress + 1)); - } - - public void onStartTrackingTouch(SeekBar seekBar) { - // TODO Auto-generated method stub - } - - public void onStopTrackingTouch(SeekBar seekBar) { - // TODO Auto-generated method stub - } - }); + case R.id.erase_menu: + setBrushSize(); mPaint.setShader(null); mPaint.setXfermode(new PorterDuffXfermode(Mode.CLEAR)); return true; - } - case R.id.clear_foreground_submenu: { + case R.id.clear_foreground_submenu: contentView.mBitmap.eraseColor(Color.TRANSPARENT); return true; - } - case R.id.clear_background_submenu: { + case R.id.clear_background_submenu: contentView.mBitmapBackground.eraseColor(Color.TRANSPARENT); return true; - } - case R.id.clear_everything_submenu: { + case R.id.clear_everything_submenu: contentView.mBitmap.eraseColor(Color.TRANSPARENT); contentView.mBitmapBackground.eraseColor(Color.TRANSPARENT); return true; - } case R.id.save_menu: takeScreenshot(true); break; - case R.id.share_menu: { - File screenshotPath = takeScreenshot(false); + case R.id.share_menu: + new AlertDialog.Builder(EasyPaint.this) + .setMessage("The share feature is temporarily disabled. To achieve the same effect, save the image and share it using a file explorer.") + .show(); + /*File screenshotPath = takeScreenshot(false); Intent i = new Intent(); i.setAction(Intent.ACTION_SEND); i.setType("image/png"); i.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_title_template)); i.putExtra(Intent.EXTRA_TEXT, getString(R.string.share_text_template)); i.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(screenshotPath)); try { startActivity(Intent.createChooser(i, getString(R.string.toolbox_share_title))); } catch (ActivityNotFoundException ex) { Toast.makeText(this.getApplicationContext(), R.string.no_way_to_share, Toast.LENGTH_LONG).show(); - } + }*/ break; - } - case R.id.open_image_menu: { + case R.id.open_image_menu: Intent intent = new Intent(); intent.setType("image/*"); //The argument is an all-lower-case MIME type - in this case, any image format. intent.setAction(Intent.ACTION_GET_CONTENT); intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); //This is false by default, but I felt that for code clarity it was better to be explicit: we only want one image startActivityForResult(Intent.createChooser(intent, getResources().getString(R.string.select_image_to_open)), CHOOSE_IMAGE); break; - } - case R.id.fill_background_with_color: { + case R.id.fill_background_with_color: waitingForBackgroundColor = true; new ColorPickerDialog(this, this, contentView.mBitmapBackground.getPixel(0, 0)).show(); return true; - } case R.id.about_menu: startActivity(new Intent(this, AboutActivity.class)); break; } return super.onOptionsItemSelected(item); } + private void setBrushSize() { + brushBinding = BrushBinding.inflate(LayoutInflater.from(this)); + AlertDialog.Builder builder = new AlertDialog.Builder(this) + .setView(brushBinding.root); + builder.setTitle(R.string.choose_width); + final AlertDialog alertDialog = builder.create(); + alertDialog.show(); + + brushBinding.brushSizeSeekBar.setProgress(getStrokeSize()); + brushBinding.sizeValueTextView.setText(String.format( + getResources().getString(R.string.your_selected_size_is), + getStrokeSize() + 1)); + brushBinding.brushSizeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + public void onProgressChanged(SeekBar seekBar, + final int progress, boolean fromUser) { + // Do something here with new value + mPaint.setStrokeWidth(progress); + brushBinding.sizeValueTextView.setText(String.format( + getResources().getString( + R.string.your_selected_size_is), progress + 1)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // TODO Auto-generated method stub + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // TODO Auto-generated method stub + } + }); + } + private boolean isWritePermissionGranted() { - return Build.VERSION.SDK_INT <= Build.VERSION_CODES.M || + return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; } /** * This takes the screenshot of the whole screen. Is this a good thing? */ private File takeScreenshot(boolean showToast) { // First we must check that permission has been granted, and if not, ask if (isWritePermissionGranted()) { // Do nothing. } else { Permissions.check(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, null, new PermissionHandler() { @Override public void onGranted() { new AlertDialog.Builder(EasyPaint.this) .setMessage("Thanks for granting permission! Please press save again.") .show(); } }); } View v = findViewById(R.id.CanvasId); v.setDrawingCacheEnabled(true); Bitmap cachedBitmap = v.getDrawingCache(); Bitmap copyBitmap = cachedBitmap.copy(Bitmap.Config.ARGB_8888, true); v.destroyDrawingCache(); FileOutputStream output = null; File file; try { File path = Places.getScreenshotFolder(); Calendar cal = Calendar.getInstance(); file = new File(path, cal.get(Calendar.YEAR) + "_" + (1 + cal.get(Calendar.MONTH)) + "_" + cal.get(Calendar.DAY_OF_MONTH) + "_" + cal.get(Calendar.HOUR_OF_DAY) + "_" + cal.get(Calendar.MINUTE) + "_" + cal.get(Calendar.SECOND) + ".png"); output = new FileOutputStream(file); copyBitmap.compress(CompressFormat.PNG, 100, output); } catch (FileNotFoundException e) { file = null; e.printStackTrace(); } finally { if (output != null) { try { output.close(); } catch (IOException e) { e.printStackTrace(); } } } if (file != null) { if (showToast) Toast.makeText( getApplicationContext(), String.format( getResources().getString( R.string.saved_your_location_to), file.getAbsolutePath()), Toast.LENGTH_LONG) .show(); // sending a broadcast to the media scanner so it will scan the new // screenshot. Intent requestScan = new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); requestScan.setData(Uri.fromFile(file)); sendBroadcast(requestScan); return file; - } else { - return null; } + return null; } private boolean isFirstTime() { SharedPreferences preferences = getPreferences(MODE_PRIVATE); boolean ranBefore = preferences.getBoolean("RanBefore", false); if (!ranBefore) { // first time SharedPreferences.Editor editor = preferences.edit(); editor.putBoolean("RanBefore", true); editor.commit(); } return !ranBefore; } private int getStrokeSize() { return (int) mPaint.getStrokeWidth(); } public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode != RESULT_CANCELED) { //"The resultCode will be RESULT_CANCELED if the activity explicitly returned that, didn't return any result, or crashed during its operation." (quote from https://developer.android.com/reference/android/app/Activity.html#onActivityResult(int,%20int,%20android.content.Intent) ) if (requestCode == CHOOSE_IMAGE) { setBackgroundUri(data.getData()); } } } public void setBackgroundUri(Uri uri) { if (uri == null) { return; } try { //I don't like loading both full-sized and reduced-size copies of the image (the larger copy can use a lot of memory), but I couldn't find any other way to do this. Bitmap fullsize = MediaStore.Images.Media.getBitmap(this.getContentResolver(), uri); contentView.mBitmapBackground = Bitmap.createScaledBitmap(fullsize, contentView.mBitmap.getWidth(), contentView.mBitmap.getHeight(), true); //contentView.mCanvas = new Canvas(contentView.mBitmapBackground); } catch (IOException exception) { //TODO: How should we handle this exception? } } public void loadFromIntents() { Intent intent = getIntent(); String action = intent.getAction(); String type = intent.getType(); System.out.println("Intentoso " + action + " type " + type); if (Intent.ACTION_SEND.equals(action) && type != null) { if (type.startsWith("image/")) { - setBackgroundUri((Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM)); + setBackgroundUri(intent.getParcelableExtra(Intent.EXTRA_STREAM)); } } } public class MyView extends View { public Bitmap mBitmap; private Bitmap mBitmapBackground; - private Canvas mCanvas; - private Paint mBitmapPaint; - private MultiLinePathManager multiLinePathManager; + private final Canvas mCanvas; + private final Paint mBitmapPaint; + private final MultiLinePathManager multiLinePathManager; public MyView(Context c) { super(c); setId(R.id.CanvasId); Display display = getWindowManager().getDefaultDisplay(); Point size = new Point(display.getWidth(), display.getHeight()); mBitmapBackground = Bitmap.createBitmap(size.x, size.y, Bitmap.Config.ARGB_8888); mBitmap = Bitmap.createBitmap(size.x, size.y, Bitmap.Config.ARGB_8888); mCanvas = new Canvas(mBitmap); mBitmapPaint = new Paint(Paint.DITHER_FLAG); multiLinePathManager = new MultiLinePathManager(MAX_POINTERS); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); } @Override protected void onDraw(Canvas canvas) { canvas.drawColor(0xFFFFFFFF); canvas.drawBitmap(mBitmapBackground, 0, 0, new Paint()); canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint); for (int i = 0; i < multiLinePathManager.superMultiPaths.length; i++) { canvas.drawPath(multiLinePathManager.superMultiPaths[i], mPaint); } } @Override public boolean onTouchEvent(MotionEvent event) { LinePath linePath; int index; int id; int eventMasked = event.getActionMasked(); switch (eventMasked) { case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: { + case MotionEvent.ACTION_POINTER_DOWN: index = event.getActionIndex(); id = event.getPointerId(index); if (extractingColor) { //If the user chose the 'extract color' menu option, the touch event indicates where they want to extract the color from. extractingColor = false; View v = findViewById(R.id.CanvasId); v.setDrawingCacheEnabled(true); Bitmap cachedBitmap = v.getDrawingCache(); int newColor = cachedBitmap.getPixel(Math.round(event.getX(index)), Math.round(event.getY(index))); v.destroyDrawingCache(); colorChanged(newColor); Toast.makeText(getApplicationContext(), R.string.color_extracted, Toast.LENGTH_SHORT).show(); } else { linePath = multiLinePathManager.addLinePathWithPointer(id); if (linePath != null) { linePath.touchStart(event.getX(index), event.getY(index)); } else { Log.e("anupam", "Too many fingers!"); } } break; - } case MotionEvent.ACTION_MOVE: for (int i = 0; i < event.getPointerCount(); i++) { id = event.getPointerId(i); index = event.findPointerIndex(id); linePath = multiLinePathManager.findLinePathFromPointer(id); if (linePath != null) { linePath.touchMove(event.getX(index), event.getY(index)); } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_CANCEL: index = event.getActionIndex(); id = event.getPointerId(index); linePath = multiLinePathManager.findLinePathFromPointer(id); if (linePath != null) { linePath.lineTo(linePath.getLastX(), linePath.getLastY()); // Commit the path to our offscreen mCanvas.drawPath(linePath, mPaint); // Kill this so we don't double draw linePath.reset(); // Allow this LinePath to be associated to another idPointer linePath.disassociateFromPointer(); } break; } invalidate(); return true; } private class LinePath extends Path { private Integer idPointer; private float lastX; private float lastY; LinePath() { this.idPointer = null; } public float getLastX() { return lastX; } public float getLastY() { return lastY; } public void touchStart(float x, float y) { this.reset(); this.moveTo(x, y); this.lastX = x; this.lastY = y; } public void touchMove(float x, float y) { float dx = Math.abs(x - lastX); float dy = Math.abs(y - lastY); if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) { this.quadTo(lastX, lastY, (x + lastX) / 2, (y + lastY) / 2); lastX = x; lastY = y; } } public boolean isDisassociatedFromPointer() { return idPointer == null; } public boolean isAssociatedToPointer(int idPointer) { return this.idPointer != null - && (int) this.idPointer == idPointer; + && this.idPointer == idPointer; } public void disassociateFromPointer() { idPointer = null; } public void associateToPointer(int idPointer) { this.idPointer = idPointer; } } private class MultiLinePathManager { public LinePath[] superMultiPaths; MultiLinePathManager(int maxPointers) { superMultiPaths = new LinePath[maxPointers]; for (int i = 0; i < maxPointers; i++) { superMultiPaths[i] = new LinePath(); } } public LinePath findLinePathFromPointer(int idPointer) { for (LinePath superMultiPath : superMultiPaths) { if (superMultiPath.isAssociatedToPointer(idPointer)) { return superMultiPath; } } return null; } public LinePath addLinePathWithPointer(int idPointer) { for (LinePath superMultiPath : superMultiPaths) { if (superMultiPath.isDisassociatedFromPointer()) { superMultiPath.associateToPointer(idPointer); return superMultiPath; } } return null; } } } } diff --git a/app/src/main/java/anupam/acrylic/PictureLayout.java b/app/src/main/java/anupam/acrylic/PictureLayout.java index c7f78f9..45e8985 100644 --- a/app/src/main/java/anupam/acrylic/PictureLayout.java +++ b/app/src/main/java/anupam/acrylic/PictureLayout.java @@ -1,164 +1,164 @@ /* * Copyright (C) 2014 Valerio Bozzolan * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package anupam.acrylic; import android.content.Context; import android.graphics.Canvas; import android.graphics.Picture; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; public class PictureLayout extends ViewGroup { - private static String error = "PictureLayout can host only one direct child"; + private static final String error = "PictureLayout can host only one direct child"; private final Picture mPicture = new Picture(); public PictureLayout(Context context) { super(context); } public PictureLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override public void addView(View child) { if (getChildCount() > 1) { throw new IllegalStateException(error); } super.addView(child); } @Override public void addView(View child, int index) { if (getChildCount() > 1) { throw new IllegalStateException(error); } super.addView(child, index); } @Override public void addView(View child, LayoutParams params) { if (getChildCount() > 1) { throw new IllegalStateException(error); } super.addView(child, params); } @Override public void addView(View child, int index, LayoutParams params) { if (getChildCount() > 1) { throw new IllegalStateException(error); } super.addView(child, index, params); } @Override protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int count = getChildCount(); int maxHeight = 0; int maxWidth = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } maxWidth += getPaddingLeft() + getPaddingRight(); maxHeight += getPaddingTop() + getPaddingBottom(); Drawable drawable = getBackground(); if (drawable != null) { maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); } setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), resolveSize(maxHeight, heightMeasureSpec)); } private void drawPict(Canvas canvas, int x, int y, int w, int h, float sx, float sy) { canvas.save(); canvas.translate(x, y); canvas.clipRect(0, 0, w, h); canvas.scale(0.5f, 0.5f); canvas.scale(sx, sy, w, h); canvas.drawPicture(mPicture); canvas.restore(); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(mPicture.beginRecording(getWidth(), getHeight())); mPicture.endRecording(); int x = getWidth() / 2; int y = getHeight() / 2; if (false) { canvas.drawPicture(mPicture); } else { drawPict(canvas, 0, 0, x, y, 1, 1); drawPict(canvas, x, 0, x, y, -1, 1); drawPict(canvas, 0, y, x, y, 1, -1); drawPict(canvas, x, y, x, y, -1, -1); } } @Override public ViewParent invalidateChildInParent(int[] location, Rect dirty) { location[0] = getLeft(); location[1] = getTop(); dirty.set(0, 0, getWidth(), getHeight()); return getParent(); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = super.getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final int childLeft = getPaddingLeft(); final int childTop = getPaddingTop(); child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight()); } } } } diff --git a/app/src/main/java/anupam/acrylic/Splash.java b/app/src/main/java/anupam/acrylic/Splash.java index 84ab508..bb0e8cb 100644 --- a/app/src/main/java/anupam/acrylic/Splash.java +++ b/app/src/main/java/anupam/acrylic/Splash.java @@ -1,50 +1,54 @@ /* * Copyright (C) 2014 Valerio Bozzolan * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package anupam.acrylic; import android.app.Activity; import android.content.Intent; import android.content.pm.ActivityInfo; import android.os.Bundle; +import anupam.acrylic.databinding.MainBinding; + public class Splash extends Activity { + MainBinding mainBinding; @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - setContentView(R.layout.main); + mainBinding = MainBinding.inflate(getLayoutInflater()); + setContentView(mainBinding.getRoot()); Thread t = new Thread() { public void run() { try { Thread.sleep(1000); startActivity(new Intent().setClassName("anupam.acrylic", "anupam.acrylic.EasyPaint")); finish(); } catch (Exception e) { e.printStackTrace(); } } }; t.start(); } } diff --git a/build.gradle b/build.gradle index 6f90ea9..5b8fde3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,26 +1,26 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:4.1.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir } diff --git a/fastlane/metadata/android/en-US/changelogs/18.txt b/fastlane/metadata/android/en-US/changelogs/18.txt new file mode 100644 index 0000000..93a30b2 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/18.txt @@ -0,0 +1,22 @@ +2.4.0 is here! + +Support-related: +- Dropped support for Android versions under 14 (by AdamHooyer) +- Improved Italian translation (by ilpianista) +- Added Turkish translation (by berkaygunduzz) +- Now compiles on and targets API 28 (by TacoTheDank) + +Design-related: (by TacoTheDank) +- Code optimizations +- Adaptive app icon +- Muted actionbar color to a paler green so it's not so bright +- Actionbar will no longer change color for now because white will make icons invisible +- Improved actionbar icons; they now look much nicer +- Renamed and reorganized drop-down options to make more sense +- Created submenus for related options +- Improved screenshot quality + +Feature-related: (by TacoTheDank) +- Added ability to clear background only +- Added ability to clear both the foreground and the background at the same time +- Disabled share feature, as proper implementation for compileSdk above 23 requires support libraries that will severely bloat the APK