ImagePaintActivity.java 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
  1. /* _____ _
  2. * |_ _| |_ _ _ ___ ___ _ __ __ _
  3. * | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. * |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. *
  6. * Threema for Android
  7. * Copyright (c) 2016-2022 Threema GmbH
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License, version 3,
  11. * as published by the Free Software Foundation.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. */
  21. package ch.threema.app.activities;
  22. import android.annotation.SuppressLint;
  23. import android.content.Intent;
  24. import android.content.res.Configuration;
  25. import android.graphics.Bitmap;
  26. import android.graphics.BitmapFactory;
  27. import android.graphics.Canvas;
  28. import android.graphics.Color;
  29. import android.graphics.Matrix;
  30. import android.graphics.PointF;
  31. import android.graphics.Typeface;
  32. import android.media.FaceDetector;
  33. import android.net.Uri;
  34. import android.os.AsyncTask;
  35. import android.os.Bundle;
  36. import android.view.Menu;
  37. import android.view.MenuItem;
  38. import android.view.View;
  39. import android.view.ViewGroup;
  40. import android.widget.FrameLayout;
  41. import android.widget.ImageView;
  42. import android.widget.ProgressBar;
  43. import android.widget.Toast;
  44. import com.android.colorpicker.ColorPickerDialog;
  45. import com.android.colorpicker.ColorPickerSwatch;
  46. import com.getkeepsafe.taptargetview.TapTarget;
  47. import com.getkeepsafe.taptargetview.TapTargetView;
  48. import org.slf4j.Logger;
  49. import java.io.BufferedInputStream;
  50. import java.io.File;
  51. import java.io.FileOutputStream;
  52. import java.io.IOException;
  53. import java.io.InputStream;
  54. import java.util.ArrayList;
  55. import java.util.List;
  56. import androidx.annotation.ColorInt;
  57. import androidx.annotation.NonNull;
  58. import androidx.annotation.UiThread;
  59. import androidx.appcompat.app.ActionBar;
  60. import ch.threema.app.R;
  61. import ch.threema.app.ThreemaApplication;
  62. import ch.threema.app.dialogs.GenericAlertDialog;
  63. import ch.threema.app.dialogs.GenericProgressDialog;
  64. import ch.threema.app.motionviews.FaceItem;
  65. import ch.threema.app.motionviews.viewmodel.Font;
  66. import ch.threema.app.motionviews.viewmodel.Layer;
  67. import ch.threema.app.motionviews.viewmodel.TextLayer;
  68. import ch.threema.app.motionviews.widget.FaceBlurEntity;
  69. import ch.threema.app.motionviews.widget.FaceEmojiEntity;
  70. import ch.threema.app.motionviews.widget.FaceEntity;
  71. import ch.threema.app.motionviews.widget.ImageEntity;
  72. import ch.threema.app.motionviews.widget.MotionEntity;
  73. import ch.threema.app.motionviews.widget.MotionView;
  74. import ch.threema.app.motionviews.widget.PathEntity;
  75. import ch.threema.app.motionviews.widget.TextEntity;
  76. import ch.threema.app.ui.MediaItem;
  77. import ch.threema.app.ui.PaintSelectionPopup;
  78. import ch.threema.app.ui.PaintView;
  79. import ch.threema.app.utils.BitmapUtil;
  80. import ch.threema.app.utils.BitmapWorkerTask;
  81. import ch.threema.app.utils.BitmapWorkerTaskParams;
  82. import ch.threema.app.utils.ConfigUtils;
  83. import ch.threema.app.utils.DialogUtil;
  84. import ch.threema.app.utils.TestUtil;
  85. import ch.threema.base.utils.LoggingUtil;
  86. import static ch.threema.app.utils.BitmapUtil.FLIP_NONE;
  87. public class ImagePaintActivity extends ThreemaToolbarActivity implements GenericAlertDialog.DialogClickListener {
  88. private static final Logger logger = LoggingUtil.getThreemaLogger("ImagePaintActivity");
  89. private static final String DIALOG_TAG_COLOR_PICKER = "colp";
  90. private static final String KEY_PEN_COLOR = "pc";
  91. private static final int REQUEST_CODE_STICKER_SELECTOR = 44;
  92. private static final int REQUEST_CODE_ENTER_TEXT = 45;
  93. private static final String DIALOG_TAG_QUIT_CONFIRM = "qq";
  94. private static final String DIALOG_TAG_SAVING_IMAGE = "se";
  95. private static final String DIALOG_TAG_BLUR_FACES = "bf";
  96. private static final String SMILEY_PATH = "emojione/3_Emoji_classic/1f600.png";
  97. private static final int STROKE_MODE_BRUSH = 0;
  98. private static final int STROKE_MODE_PENCIL = 1;
  99. private static final int MAX_FACES = 16;
  100. private ImageView imageView;
  101. private PaintView paintView;
  102. private MotionView motionView;
  103. private FrameLayout imageFrame;
  104. private int orientation, exifOrientation, flip, exifFlip, clipWidth, clipHeight;
  105. private Uri imageUri, outputUri;
  106. private ProgressBar progressBar;
  107. @ColorInt private int penColor;
  108. private MenuItem undoItem, paletteItem, paintItem, pencilItem, blurFacesItem;
  109. private PaintSelectionPopup paintSelectionPopup;
  110. private ArrayList<MotionEntity> undoHistory = new ArrayList<>();
  111. private boolean saveSemaphore = false;
  112. private int strokeMode = STROKE_MODE_BRUSH;
  113. @Override
  114. public int getLayoutResource() {
  115. return R.layout.activity_image_paint;
  116. }
  117. @Override
  118. public void onBackPressed() {
  119. if (hasChanges()) {
  120. GenericAlertDialog dialogFragment = GenericAlertDialog.newInstance(
  121. R.string.discard_changes_title,
  122. R.string.discard_changes,
  123. R.string.discard,
  124. R.string.cancel);
  125. dialogFragment.show(getSupportFragmentManager(), DIALOG_TAG_QUIT_CONFIRM);
  126. } else {
  127. finish();
  128. }
  129. }
  130. private boolean hasChanges() {
  131. return undoHistory.size() > 0;
  132. }
  133. @Override
  134. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  135. super.onActivityResult(requestCode, resultCode, data);
  136. if (resultCode == RESULT_OK && data != null) {
  137. switch (requestCode) {
  138. case REQUEST_CODE_STICKER_SELECTOR:
  139. final String stickerPath = data.getStringExtra(StickerSelectorActivity.EXTRA_STICKER_PATH);
  140. if (!TestUtil.empty(stickerPath)) {
  141. addSticker(stickerPath);
  142. }
  143. break;
  144. case REQUEST_CODE_ENTER_TEXT:
  145. final String text = data.getStringExtra(ImagePaintKeyboardActivity.INTENT_EXTRA_TEXT);
  146. if (!TestUtil.empty(text)) {
  147. addText(text);
  148. }
  149. }
  150. }
  151. }
  152. private void addSticker(final String stickerPath) {
  153. paintView.setActive(false);
  154. new AsyncTask<Void, Void, Bitmap>() {
  155. @Override
  156. protected Bitmap doInBackground(Void... params) {
  157. try {
  158. return BitmapFactory.decodeStream(getAssets().open(stickerPath));
  159. } catch (IOException e) {
  160. logger.error("Exception", e);
  161. return null;
  162. }
  163. }
  164. @Override
  165. protected void onPostExecute(final Bitmap bitmap) {
  166. if (bitmap != null) {
  167. motionView.post(new Runnable() {
  168. @Override
  169. public void run() {
  170. Layer layer = new Layer();
  171. ImageEntity entity = new ImageEntity(layer, bitmap, motionView.getWidth(), motionView.getHeight());
  172. motionView.addEntityAndPosition(entity);
  173. }
  174. });
  175. }
  176. }
  177. }.execute();
  178. }
  179. private void addText(final String text) {
  180. paintView.setActive(false);
  181. TextLayer textLayer = new TextLayer();
  182. Font font = new Font();
  183. font.setColor(penColor);
  184. font.setSize(getResources().getDimensionPixelSize(R.dimen.imagepaint_default_font_size));
  185. textLayer.setFont(font);
  186. textLayer.setText(text);
  187. TextEntity textEntity = new TextEntity(textLayer, motionView.getWidth(),
  188. motionView.getHeight());
  189. motionView.addEntityAndPosition(textEntity);
  190. }
  191. @Override
  192. protected void onCreate(Bundle savedInstanceState) {
  193. super.onCreate(savedInstanceState);
  194. getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
  195. Intent intent = getIntent();
  196. MediaItem mediaItem = intent.getParcelableExtra(Intent.EXTRA_STREAM);
  197. if (mediaItem == null) {
  198. finish();
  199. return;
  200. }
  201. this.imageUri = mediaItem.getUri();
  202. if (this.imageUri == null) {
  203. finish();
  204. return;
  205. }
  206. this.orientation = intent.getIntExtra(ThreemaApplication.EXTRA_ORIENTATION, 0);
  207. this.flip = intent.getIntExtra(ThreemaApplication.EXTRA_FLIP, BitmapUtil.FLIP_NONE);
  208. this.exifOrientation = intent.getIntExtra(ThreemaApplication.EXTRA_EXIF_ORIENTATION, 0);
  209. this.exifFlip = intent.getIntExtra(ThreemaApplication.EXTRA_EXIF_FLIP, BitmapUtil.FLIP_NONE);
  210. this.outputUri = intent.getParcelableExtra(ThreemaApplication.EXTRA_OUTPUT_FILE);
  211. setSupportActionBar(getToolbar());
  212. ActionBar actionBar = getSupportActionBar();
  213. if (actionBar == null) {
  214. finish();
  215. return;
  216. }
  217. actionBar.setDisplayHomeAsUpEnabled(true);
  218. actionBar.setTitle("");
  219. this.paintView = findViewById(R.id.paint_view);
  220. this.progressBar = findViewById(R.id.progress);
  221. this.imageView = findViewById(R.id.preview_image);
  222. this.motionView = findViewById(R.id.motion_view);
  223. this.penColor = getResources().getColor(R.color.material_red);
  224. if (savedInstanceState != null) {
  225. this.penColor = savedInstanceState.getInt(KEY_PEN_COLOR, penColor);
  226. }
  227. this.paintView.setColor(penColor);
  228. this.paintView.setStrokeWidth(getResources().getDimensionPixelSize(R.dimen.imagepaint_brush_stroke_width));
  229. this.paintView.setTouchListener(new PaintView.TouchListener() {
  230. @Override
  231. public void onTouchUp() {
  232. invalidateOptionsMenu();
  233. }
  234. @Override
  235. public void onTouchDown() {
  236. }
  237. @Override
  238. public void onAdded() {
  239. undoHistory.add(new PathEntity());
  240. }
  241. @Override
  242. public void onDeleted() {
  243. if (undoHistory.size() > 0) {
  244. undoHistory.remove(undoHistory.size() - 1);
  245. }
  246. }
  247. });
  248. this.motionView.setTouchListener(new MotionView.TouchListener() {
  249. @Override
  250. public void onSelected(boolean isSelected) {
  251. invalidateOptionsMenu();
  252. }
  253. @Override
  254. public void onLongClick(MotionEntity entity, int x, int y) {
  255. paintSelectionPopup.show((int) motionView.getX() + x, (int) motionView.getY() + y, !entity.hasFixedPositionAndSize());
  256. }
  257. @Override
  258. public void onAdded(MotionEntity entity) {
  259. undoHistory.add(entity);
  260. }
  261. @SuppressLint("UseValueOf")
  262. @Override
  263. public void onDeleted(MotionEntity entity) {
  264. undoHistory.remove(entity);
  265. }
  266. @Override
  267. public void onTouchUp() {
  268. if (!paintView.getActive()) {
  269. invalidateOptionsMenu();
  270. }
  271. }
  272. @Override
  273. public void onTouchDown() {
  274. }
  275. });
  276. this.paintSelectionPopup = new PaintSelectionPopup(this, this.motionView);
  277. this.paintSelectionPopup.setListener(new PaintSelectionPopup.PaintSelectPopupListener() {
  278. @Override
  279. public void onItemSelected(int tag) {
  280. switch (tag) {
  281. case PaintSelectionPopup.TAG_REMOVE:
  282. deleteEntity();
  283. break;
  284. case PaintSelectionPopup.TAG_FLIP:
  285. flipEntity();
  286. break;
  287. case PaintSelectionPopup.TAG_TO_FRONT:
  288. bringToFrontEntity();
  289. break;
  290. default:
  291. break;
  292. }
  293. }
  294. @Override
  295. public void onOpen() {
  296. motionView.setClickable(false);
  297. paintView.setClickable(false);
  298. }
  299. @Override
  300. public void onClose() {
  301. motionView.setClickable(true);
  302. paintView.setClickable(true);
  303. }
  304. });
  305. this.imageFrame = findViewById(R.id.content_frame);
  306. this.imageFrame.post(() -> loadImage());
  307. showTooltip();
  308. }
  309. private void loadImage() {
  310. BitmapWorkerTaskParams bitmapParams = new BitmapWorkerTaskParams();
  311. bitmapParams.imageUri = this.imageUri;
  312. bitmapParams.width = this.imageFrame.getWidth();
  313. bitmapParams.height = this.imageFrame.getHeight();
  314. bitmapParams.contentResolver = getContentResolver();
  315. bitmapParams.orientation = this.orientation;
  316. bitmapParams.flip = this.flip;
  317. bitmapParams.exifOrientation = this.exifOrientation;
  318. bitmapParams.exifFlip = this.exifFlip;
  319. logger.debug("screen height: " + bitmapParams.height);
  320. // load main image
  321. new BitmapWorkerTask(this.imageView) {
  322. @Override
  323. protected void onPreExecute() {
  324. super.onPreExecute();
  325. imageView.setVisibility(View.INVISIBLE);
  326. paintView.setVisibility(View.INVISIBLE);
  327. motionView.setVisibility(View.INVISIBLE);
  328. progressBar.setVisibility(View.VISIBLE);
  329. }
  330. @Override
  331. protected void onPostExecute(Bitmap bitmap) {
  332. super.onPostExecute(bitmap);
  333. progressBar.setVisibility(View.GONE);
  334. imageView.setVisibility(View.VISIBLE);
  335. paintView.setVisibility(View.VISIBLE);
  336. motionView.setVisibility(View.VISIBLE);
  337. // clip other views to image size
  338. if (bitmap != null) {
  339. clipWidth = bitmap.getWidth();
  340. clipHeight = bitmap.getHeight();
  341. paintView.recalculate(clipWidth, clipHeight);
  342. resizeView(paintView, clipWidth, clipHeight);
  343. resizeView(motionView, clipWidth, clipHeight);
  344. }
  345. }
  346. }.execute(bitmapParams);
  347. }
  348. private void resizeView(View view, int width, int height) {
  349. ViewGroup.LayoutParams params = view.getLayoutParams();
  350. params.width = width;
  351. params.height = height;
  352. view.requestLayout();
  353. }
  354. private void selectSticker() {
  355. startActivityForResult(new Intent(ImagePaintActivity.this, StickerSelectorActivity.class), REQUEST_CODE_STICKER_SELECTOR);
  356. overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
  357. }
  358. private void enterText() {
  359. Intent intent = new Intent(ImagePaintActivity.this, ImagePaintKeyboardActivity.class);
  360. intent.putExtra(ImagePaintKeyboardActivity.INTENT_EXTRA_COLOR, penColor);
  361. startActivityForResult(intent, REQUEST_CODE_ENTER_TEXT);
  362. overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
  363. }
  364. @SuppressLint("StaticFieldLeak")
  365. private void blurFaces(final boolean useEmoji) {
  366. this.paintView.setActive(false);
  367. new AsyncTask<Void, Void, List<FaceItem>>() {
  368. int numFaces = -1;
  369. int originalImageWidth, originalImageHeight;
  370. @Override
  371. protected void onPreExecute() {
  372. GenericProgressDialog.newInstance(-1, R.string.please_wait).show(getSupportFragmentManager(), DIALOG_TAG_BLUR_FACES);
  373. }
  374. @Override
  375. protected List<FaceItem> doInBackground(Void... voids) {
  376. BitmapFactory.Options options;
  377. Bitmap bitmap, orgBitmap;
  378. List<FaceItem> faceItemList = new ArrayList<>();
  379. try (InputStream measure = getContentResolver().openInputStream(imageUri)) {
  380. options = BitmapUtil.getImageDimensions(measure);
  381. } catch (IOException | SecurityException | IllegalStateException | OutOfMemoryError e) {
  382. logger.error("Exception", e);
  383. return null;
  384. }
  385. if (options.outWidth < 16 || options.outHeight < 16) {
  386. return null;
  387. }
  388. options.inPreferredConfig = Bitmap.Config.ARGB_8888;
  389. options.inJustDecodeBounds = false;
  390. try (InputStream data = getContentResolver().openInputStream(imageUri)) {
  391. if (data != null) {
  392. orgBitmap = BitmapFactory.decodeStream(new BufferedInputStream(data), null, options);
  393. if (orgBitmap != null) {
  394. if (exifOrientation != 0 || exifFlip != FLIP_NONE) {
  395. orgBitmap = BitmapUtil.rotateBitmap(orgBitmap, exifOrientation, exifFlip);
  396. }
  397. if (orientation != 0 || flip != FLIP_NONE) {
  398. orgBitmap = BitmapUtil.rotateBitmap(orgBitmap, orientation, flip);
  399. }
  400. bitmap = Bitmap.createBitmap(orgBitmap.getWidth() & ~0x1, orgBitmap.getHeight(), Bitmap.Config.RGB_565);
  401. new Canvas(bitmap).drawBitmap(orgBitmap, 0, 0, null);
  402. originalImageWidth = orgBitmap.getWidth();
  403. originalImageHeight = orgBitmap.getHeight();
  404. } else {
  405. logger.info("could not open image");
  406. return null;
  407. }
  408. } else {
  409. logger.info("could not open input stream");
  410. return null;
  411. }
  412. } catch (Exception e) {
  413. logger.error("Exception", e);
  414. return null;
  415. }
  416. try {
  417. FaceDetector faceDetector = new FaceDetector(bitmap.getWidth(), bitmap.getHeight(), MAX_FACES);
  418. FaceDetector.Face[] faces = new FaceDetector.Face[MAX_FACES];
  419. numFaces = faceDetector.findFaces(bitmap, faces);
  420. if (numFaces < 1) {
  421. return null;
  422. }
  423. logger.debug("{} faces found.", numFaces);
  424. Bitmap emoji = null;
  425. if (useEmoji) {
  426. emoji = BitmapFactory.decodeStream(getAssets().open(SMILEY_PATH));
  427. }
  428. for (FaceDetector.Face face: faces) {
  429. if (face != null) {
  430. if (useEmoji) {
  431. faceItemList.add(new FaceItem(face, emoji, 1));
  432. } else {
  433. float offsetY = face.eyesDistance() * FaceEntity.BLUR_RADIUS;
  434. PointF midPoint = new PointF();
  435. face.getMidPoint(midPoint);
  436. int croppedBitmapSize = (int) (offsetY * 2);
  437. float scale = 1f;
  438. // pixelize large bitmaps
  439. if (croppedBitmapSize > 64) {
  440. scale = (float) croppedBitmapSize / 64f;
  441. }
  442. float scaleFactor = 1f / scale;
  443. Matrix matrix = new Matrix();
  444. matrix.setScale(scaleFactor, scaleFactor);
  445. Bitmap croppedBitmap = Bitmap.createBitmap(
  446. orgBitmap,
  447. offsetY > midPoint.x ? 0 : (int) (midPoint.x - offsetY),
  448. offsetY > midPoint.y ? 0 : (int) (midPoint.y - offsetY),
  449. croppedBitmapSize,
  450. croppedBitmapSize,
  451. matrix,
  452. false);
  453. faceItemList.add(new FaceItem(face, croppedBitmap, scale));
  454. }
  455. }
  456. }
  457. return faceItemList;
  458. } catch (Exception e) {
  459. logger.error("Face detection failed", e);
  460. return null;
  461. } finally {
  462. bitmap.recycle();
  463. }
  464. }
  465. @Override
  466. protected void onPostExecute(List<FaceItem> faceItemList) {
  467. if (faceItemList != null && faceItemList.size() > 0) {
  468. motionView.post(() -> {
  469. for (FaceItem faceItem : faceItemList) {
  470. Layer layer = new Layer();
  471. if (useEmoji) {
  472. FaceEmojiEntity entity = new FaceEmojiEntity(layer, faceItem, originalImageWidth, originalImageHeight, motionView.getWidth(), motionView.getHeight());
  473. motionView.addEntity(entity);
  474. } else {
  475. FaceBlurEntity entity = new FaceBlurEntity(layer, faceItem, originalImageWidth, originalImageHeight, motionView.getWidth(), motionView.getHeight());
  476. motionView.addEntity(entity);
  477. }
  478. }
  479. });
  480. } else {
  481. Toast.makeText(ImagePaintActivity.this, R.string.no_faces_detected, Toast.LENGTH_LONG).show();
  482. }
  483. DialogUtil.dismissDialog(getSupportFragmentManager(), DIALOG_TAG_BLUR_FACES, true);
  484. }
  485. }.execute();
  486. }
  487. @Override
  488. public boolean onPrepareOptionsMenu(Menu menu) {
  489. super.onPrepareOptionsMenu(menu);
  490. ConfigUtils.themeMenuItem(paletteItem, Color.WHITE);
  491. ConfigUtils.themeMenuItem(paintItem, Color.WHITE);
  492. ConfigUtils.themeMenuItem(pencilItem, Color.WHITE);
  493. if (motionView.getSelectedEntity() == null) {
  494. // no selected entities => draw mode or neutral mode
  495. if (paintView.getActive()) {
  496. if (this.strokeMode == STROKE_MODE_PENCIL) {
  497. ConfigUtils.themeMenuItem(pencilItem, this.penColor);
  498. } else {
  499. ConfigUtils.themeMenuItem(paintItem, this.penColor);
  500. }
  501. }
  502. }
  503. undoItem.setVisible(undoHistory.size() > 0);
  504. blurFacesItem.setVisible(motionView.getEntitiesCount() == 0);
  505. return true;
  506. }
  507. @Override
  508. public boolean onCreateOptionsMenu(Menu menu) {
  509. super.onCreateOptionsMenu(menu);
  510. getMenuInflater().inflate(R.menu.activity_image_paint, menu);
  511. undoItem = menu.findItem(R.id.item_undo);
  512. paletteItem = menu.findItem(R.id.item_palette);
  513. paintItem = menu.findItem(R.id.item_draw);
  514. pencilItem = menu.findItem(R.id.item_pencil);
  515. blurFacesItem = menu.findItem(R.id.item_face);
  516. return true;
  517. }
  518. @Override
  519. public boolean onOptionsItemSelected(MenuItem item) {
  520. super.onOptionsItemSelected(item);
  521. switch (item.getItemId()) {
  522. case android.R.id.home:
  523. if (undoHistory.size() > 0) {
  524. item.setEnabled(false);
  525. renderImage();
  526. } else {
  527. finish();
  528. }
  529. return true;
  530. case R.id.item_undo:
  531. undo();
  532. break;
  533. case R.id.item_stickers:
  534. selectSticker();
  535. break;
  536. case R.id.item_palette:
  537. chooseColor();
  538. break;
  539. case R.id.item_text:
  540. enterText();
  541. break;
  542. case R.id.item_draw:
  543. if (strokeMode == STROKE_MODE_BRUSH && this.paintView.getActive()) {
  544. // switch to selection mode
  545. setDrawMode(false);
  546. } else {
  547. setStrokeMode(STROKE_MODE_BRUSH);
  548. setDrawMode(true);
  549. }
  550. break;
  551. case R.id.item_pencil:
  552. if (strokeMode == STROKE_MODE_PENCIL && this.paintView.getActive()) {
  553. // switch to selection mode
  554. setDrawMode(false);
  555. } else {
  556. setStrokeMode(STROKE_MODE_PENCIL);
  557. setDrawMode(true);
  558. }
  559. break;
  560. case R.id.item_face_blur:
  561. blurFaces(false);
  562. break;
  563. case R.id.item_face_emoji:
  564. blurFaces(true);
  565. break;
  566. default:
  567. break;
  568. }
  569. return false;
  570. }
  571. @UiThread
  572. public void showTooltip() {
  573. if (!preferenceService.getIsFaceBlurTooltipShown()) {
  574. if (getToolbar() != null) {
  575. getToolbar().postDelayed(() -> {
  576. final View v = findViewById(R.id.item_face);
  577. try {
  578. TapTargetView.showFor(this,
  579. TapTarget.forView(v, getString(R.string.face_blur_tooltip_title), getString(R.string.face_blur_tooltip_text))
  580. .outerCircleColor(R.color.dark_accent) // Specify a color for the outer circle
  581. .outerCircleAlpha(0.96f) // Specify the alpha amount for the outer circle
  582. .targetCircleColor(android.R.color.white) // Specify a color for the target circle
  583. .titleTextSize(24) // Specify the size (in sp) of the title text
  584. .titleTextColor(android.R.color.white) // Specify the color of the title text
  585. .descriptionTextSize(18) // Specify the size (in sp) of the description text
  586. .descriptionTextColor(android.R.color.white) // Specify the color of the description text
  587. .textColor(android.R.color.white) // Specify a color for both the title and description text
  588. .textTypeface(Typeface.SANS_SERIF) // Specify a typeface for the text
  589. .dimColor(android.R.color.black) // If set, will dim behind the view with 30% opacity of the given color
  590. .drawShadow(true) // Whether to draw a drop shadow or not
  591. .cancelable(true) // Whether tapping outside the outer circle dismisses the view
  592. .tintTarget(true) // Whether to tint the target view's color
  593. .transparentTarget(false) // Specify whether the target is transparent (displays the content underneath)
  594. .targetRadius(50) // Specify the target radius (in dp)
  595. );
  596. preferenceService.setFaceBlurTooltipShown(true);
  597. } catch (Exception ignore) {
  598. // catch null typeface exception on CROSSCALL Action-X3
  599. }
  600. }, 2000);
  601. }
  602. }
  603. }
  604. private void setStrokeMode(int strokeMode) {
  605. this.strokeMode = strokeMode;
  606. this.paintView.setStrokeWidth(
  607. getResources().getDimensionPixelSize(strokeMode == STROKE_MODE_PENCIL ?
  608. R.dimen.imagepaint_pencil_stroke_width :
  609. R.dimen.imagepaint_brush_stroke_width));
  610. }
  611. private void deleteEntity() {
  612. motionView.deletedSelectedEntity();
  613. invalidateOptionsMenu();
  614. }
  615. private void flipEntity() {
  616. motionView.flipSelectedEntity();
  617. invalidateOptionsMenu();
  618. }
  619. private void bringToFrontEntity() {
  620. motionView.moveSelectedEntityToFront();
  621. invalidateOptionsMenu();
  622. }
  623. private void undo() {
  624. if (undoHistory.size() > 0) {
  625. MotionEntity entity = undoHistory.get(undoHistory.size() - 1);
  626. motionView.unselectEntity();
  627. if (entity instanceof PathEntity) {
  628. paintView.undo();
  629. } else {
  630. motionView.deleteEntity(entity);
  631. }
  632. invalidateOptionsMenu();
  633. }
  634. }
  635. private void setDrawMode(boolean enable) {
  636. if (enable) {
  637. motionView.unselectEntity();
  638. paintView.setActive(true);
  639. } else {
  640. paintView.setActive(false);
  641. }
  642. invalidateOptionsMenu();
  643. }
  644. @Override
  645. public void onConfigurationChanged(@NonNull Configuration newConfig) {
  646. super.onConfigurationChanged(newConfig);
  647. // hack to adjust toolbar height after rotate
  648. ConfigUtils.adjustToolbar(this, getToolbar());
  649. this.imageFrame = findViewById(R.id.content_frame);
  650. if (this.imageFrame != null) {
  651. this.imageFrame.post(new Runnable() {
  652. @Override
  653. public void run() {
  654. loadImage();
  655. }
  656. });
  657. }
  658. }
  659. private void chooseColor() {
  660. int[] colors = {
  661. getResources().getColor(R.color.material_cyan),
  662. getResources().getColor(R.color.material_blue),
  663. getResources().getColor(R.color.material_indigo),
  664. getResources().getColor(R.color.material_deep_purple),
  665. getResources().getColor(R.color.material_purple),
  666. getResources().getColor(R.color.material_pink),
  667. getResources().getColor(R.color.material_red),
  668. getResources().getColor(R.color.material_orange),
  669. getResources().getColor(R.color.material_amber),
  670. getResources().getColor(R.color.material_yellow),
  671. getResources().getColor(R.color.material_lime),
  672. getResources().getColor(R.color.material_green),
  673. getResources().getColor(R.color.material_green_700),
  674. getResources().getColor(R.color.material_teal),
  675. getResources().getColor(R.color.material_brown),
  676. getResources().getColor(R.color.material_grey_600),
  677. getResources().getColor(R.color.material_grey_500),
  678. getResources().getColor(R.color.material_grey_300),
  679. Color.WHITE,
  680. Color.BLACK,
  681. };
  682. ColorPickerDialog colorPickerDialog = new ColorPickerDialog();
  683. colorPickerDialog.initialize(R.string.color_picker_default_title, colors, 0, 4, colors.length);
  684. colorPickerDialog.setSelectedColor(penColor);
  685. colorPickerDialog.setOnColorSelectedListener(new ColorPickerSwatch.OnColorSelectedListener() {
  686. @Override
  687. public void onColorSelected(int color) {
  688. paintView.setColor(color);
  689. penColor = color;
  690. ConfigUtils.themeMenuItem(paletteItem, penColor);
  691. if (motionView.getSelectedEntity() != null) {
  692. if (motionView.getSelectedEntity() instanceof TextEntity) {
  693. TextEntity textEntity = (TextEntity) motionView.getSelectedEntity();
  694. textEntity.getLayer().getFont().setColor(penColor);
  695. textEntity.updateEntity();
  696. motionView.invalidate();
  697. } else {
  698. // ignore color selection for stickers
  699. }
  700. } else {
  701. setDrawMode(true);
  702. }
  703. }
  704. });
  705. colorPickerDialog.show(getSupportFragmentManager(), DIALOG_TAG_COLOR_PICKER);
  706. }
  707. private void renderImage() {
  708. logger.debug("renderImage");
  709. if (saveSemaphore) {
  710. return;
  711. }
  712. saveSemaphore = true;
  713. BitmapWorkerTaskParams bitmapParams = new BitmapWorkerTaskParams();
  714. bitmapParams.imageUri = this.imageUri;
  715. bitmapParams.contentResolver = getContentResolver();
  716. bitmapParams.orientation = this.orientation;
  717. bitmapParams.flip = this.flip;
  718. bitmapParams.exifOrientation = this.exifOrientation;
  719. bitmapParams.exifFlip = this.exifFlip;
  720. bitmapParams.mutable = true;
  721. new BitmapWorkerTask(null) {
  722. @Override
  723. protected void onPreExecute() {
  724. super.onPreExecute();
  725. GenericProgressDialog.newInstance(R.string.draw, R.string.saving_media).show(getSupportFragmentManager(), DIALOG_TAG_SAVING_IMAGE);
  726. }
  727. @Override
  728. protected void onPostExecute(Bitmap bitmap) {
  729. Canvas canvas = new Canvas(bitmap);
  730. motionView.renderOverlay(canvas);
  731. paintView.renderOverlay(canvas, clipWidth, clipHeight);
  732. new AsyncTask<Bitmap, Void, Boolean>() {
  733. @Override
  734. protected Boolean doInBackground(Bitmap... params) {
  735. try {
  736. File output = new File(outputUri.getPath());
  737. FileOutputStream outputStream = new FileOutputStream(output);
  738. params[0].compress(Bitmap.CompressFormat.PNG, 100, outputStream);
  739. outputStream.flush();
  740. outputStream.close();
  741. } catch (Exception e) {
  742. return false;
  743. }
  744. return true;
  745. }
  746. @Override
  747. protected void onPostExecute(Boolean success) {
  748. DialogUtil.dismissDialog(getSupportFragmentManager(), DIALOG_TAG_SAVING_IMAGE, true);
  749. if (success) {
  750. setResult(RESULT_OK);
  751. finish();
  752. } else {
  753. Toast.makeText(ImagePaintActivity.this, R.string.error_saving_file, Toast.LENGTH_SHORT).show();
  754. }
  755. }
  756. }.execute(bitmap);
  757. }
  758. }.execute(bitmapParams);
  759. }
  760. @Override
  761. public void onSaveInstanceState(Bundle outState) {
  762. super.onSaveInstanceState(outState);
  763. outState.putInt(KEY_PEN_COLOR, penColor);
  764. }
  765. @Override
  766. public void onYes(String tag, Object data) {
  767. finish();
  768. }
  769. @Override
  770. public void onNo(String tag, Object data) {}
  771. }