diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 2159be1a83..630f822222 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -366,6 +366,15 @@ android:theme="@style/TextSecure.LightTheme" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> + + + + diff --git a/assets/stickers/animals/anteater.png b/assets/stickers/animals/anteater.png new file mode 100644 index 0000000000..ca798a852f Binary files /dev/null and b/assets/stickers/animals/anteater.png differ diff --git a/assets/stickers/animals/bat.png b/assets/stickers/animals/bat.png new file mode 100644 index 0000000000..27a586a847 Binary files /dev/null and b/assets/stickers/animals/bat.png differ diff --git a/assets/stickers/animals/beetle.png b/assets/stickers/animals/beetle.png new file mode 100644 index 0000000000..ee58fb14f9 Binary files /dev/null and b/assets/stickers/animals/beetle.png differ diff --git a/assets/stickers/animals/bulldog.png b/assets/stickers/animals/bulldog.png new file mode 100644 index 0000000000..b9fc34307d Binary files /dev/null and b/assets/stickers/animals/bulldog.png differ diff --git a/assets/stickers/animals/butterfly.png b/assets/stickers/animals/butterfly.png new file mode 100644 index 0000000000..467927a233 Binary files /dev/null and b/assets/stickers/animals/butterfly.png differ diff --git a/assets/stickers/animals/camel.png b/assets/stickers/animals/camel.png new file mode 100644 index 0000000000..1026c7044b Binary files /dev/null and b/assets/stickers/animals/camel.png differ diff --git a/assets/stickers/animals/cat.png b/assets/stickers/animals/cat.png new file mode 100644 index 0000000000..a277712082 Binary files /dev/null and b/assets/stickers/animals/cat.png differ diff --git a/assets/stickers/animals/chameleon.png b/assets/stickers/animals/chameleon.png new file mode 100644 index 0000000000..45a16a389e Binary files /dev/null and b/assets/stickers/animals/chameleon.png differ diff --git a/assets/stickers/animals/clown-fish.png b/assets/stickers/animals/clown-fish.png new file mode 100644 index 0000000000..836a7a7260 Binary files /dev/null and b/assets/stickers/animals/clown-fish.png differ diff --git a/assets/stickers/animals/cobra.png b/assets/stickers/animals/cobra.png new file mode 100644 index 0000000000..56f54d0e4f Binary files /dev/null and b/assets/stickers/animals/cobra.png differ diff --git a/assets/stickers/animals/cow.png b/assets/stickers/animals/cow.png new file mode 100644 index 0000000000..dbb128df8e Binary files /dev/null and b/assets/stickers/animals/cow.png differ diff --git a/assets/stickers/animals/crab.png b/assets/stickers/animals/crab.png new file mode 100644 index 0000000000..d9b69dc6de Binary files /dev/null and b/assets/stickers/animals/crab.png differ diff --git a/assets/stickers/animals/crocodile.png b/assets/stickers/animals/crocodile.png new file mode 100644 index 0000000000..ad9f2f8b0f Binary files /dev/null and b/assets/stickers/animals/crocodile.png differ diff --git a/assets/stickers/animals/duck.png b/assets/stickers/animals/duck.png new file mode 100644 index 0000000000..ad987a53c1 Binary files /dev/null and b/assets/stickers/animals/duck.png differ diff --git a/assets/stickers/animals/elephant.png b/assets/stickers/animals/elephant.png new file mode 100644 index 0000000000..38e9fd2368 Binary files /dev/null and b/assets/stickers/animals/elephant.png differ diff --git a/assets/stickers/animals/frog.png b/assets/stickers/animals/frog.png new file mode 100644 index 0000000000..f9631266e8 Binary files /dev/null and b/assets/stickers/animals/frog.png differ diff --git a/assets/stickers/animals/giraffe.png b/assets/stickers/animals/giraffe.png new file mode 100644 index 0000000000..9b18301545 Binary files /dev/null and b/assets/stickers/animals/giraffe.png differ diff --git a/assets/stickers/animals/hen.png b/assets/stickers/animals/hen.png new file mode 100644 index 0000000000..f875bb1bf3 Binary files /dev/null and b/assets/stickers/animals/hen.png differ diff --git a/assets/stickers/animals/hippopotamus.png b/assets/stickers/animals/hippopotamus.png new file mode 100644 index 0000000000..04782f9362 Binary files /dev/null and b/assets/stickers/animals/hippopotamus.png differ diff --git a/assets/stickers/animals/kangaroo.png b/assets/stickers/animals/kangaroo.png new file mode 100644 index 0000000000..b9c60b1ce6 Binary files /dev/null and b/assets/stickers/animals/kangaroo.png differ diff --git a/assets/stickers/animals/lion.png b/assets/stickers/animals/lion.png new file mode 100644 index 0000000000..0c73c676a9 Binary files /dev/null and b/assets/stickers/animals/lion.png differ diff --git a/assets/stickers/animals/llama.png b/assets/stickers/animals/llama.png new file mode 100644 index 0000000000..43945dc3d1 Binary files /dev/null and b/assets/stickers/animals/llama.png differ diff --git a/assets/stickers/animals/macaw.png b/assets/stickers/animals/macaw.png new file mode 100644 index 0000000000..4c7c10668d Binary files /dev/null and b/assets/stickers/animals/macaw.png differ diff --git a/assets/stickers/animals/monkey.png b/assets/stickers/animals/monkey.png new file mode 100644 index 0000000000..50248cd7f9 Binary files /dev/null and b/assets/stickers/animals/monkey.png differ diff --git a/assets/stickers/animals/moose.png b/assets/stickers/animals/moose.png new file mode 100644 index 0000000000..29f8fe5cd5 Binary files /dev/null and b/assets/stickers/animals/moose.png differ diff --git a/assets/stickers/animals/mouse.png b/assets/stickers/animals/mouse.png new file mode 100644 index 0000000000..c15080a443 Binary files /dev/null and b/assets/stickers/animals/mouse.png differ diff --git a/assets/stickers/animals/octopus.png b/assets/stickers/animals/octopus.png new file mode 100644 index 0000000000..0aa5c457c3 Binary files /dev/null and b/assets/stickers/animals/octopus.png differ diff --git a/assets/stickers/animals/ostrich.png b/assets/stickers/animals/ostrich.png new file mode 100644 index 0000000000..f9fd66c2c3 Binary files /dev/null and b/assets/stickers/animals/ostrich.png differ diff --git a/assets/stickers/animals/owl.png b/assets/stickers/animals/owl.png new file mode 100644 index 0000000000..bcaf9a6a96 Binary files /dev/null and b/assets/stickers/animals/owl.png differ diff --git a/assets/stickers/animals/panda.png b/assets/stickers/animals/panda.png new file mode 100644 index 0000000000..80ad77836b Binary files /dev/null and b/assets/stickers/animals/panda.png differ diff --git a/assets/stickers/animals/pelican.png b/assets/stickers/animals/pelican.png new file mode 100644 index 0000000000..b04d90cda3 Binary files /dev/null and b/assets/stickers/animals/pelican.png differ diff --git a/assets/stickers/animals/penguin.png b/assets/stickers/animals/penguin.png new file mode 100644 index 0000000000..7a730d1119 Binary files /dev/null and b/assets/stickers/animals/penguin.png differ diff --git a/assets/stickers/animals/pig.png b/assets/stickers/animals/pig.png new file mode 100644 index 0000000000..181da2e2d6 Binary files /dev/null and b/assets/stickers/animals/pig.png differ diff --git a/assets/stickers/animals/rabbit.png b/assets/stickers/animals/rabbit.png new file mode 100644 index 0000000000..7c45ebdd03 Binary files /dev/null and b/assets/stickers/animals/rabbit.png differ diff --git a/assets/stickers/animals/racoon.png b/assets/stickers/animals/racoon.png new file mode 100644 index 0000000000..80183ce268 Binary files /dev/null and b/assets/stickers/animals/racoon.png differ diff --git a/assets/stickers/animals/ray.png b/assets/stickers/animals/ray.png new file mode 100644 index 0000000000..3f280ab231 Binary files /dev/null and b/assets/stickers/animals/ray.png differ diff --git a/assets/stickers/animals/rhinoceros.png b/assets/stickers/animals/rhinoceros.png new file mode 100644 index 0000000000..bee18d6a23 Binary files /dev/null and b/assets/stickers/animals/rhinoceros.png differ diff --git a/assets/stickers/animals/sea-cow.png b/assets/stickers/animals/sea-cow.png new file mode 100644 index 0000000000..ad0c4ff109 Binary files /dev/null and b/assets/stickers/animals/sea-cow.png differ diff --git a/assets/stickers/animals/shark.png b/assets/stickers/animals/shark.png new file mode 100644 index 0000000000..db300fbaae Binary files /dev/null and b/assets/stickers/animals/shark.png differ diff --git a/assets/stickers/animals/sheep.png b/assets/stickers/animals/sheep.png new file mode 100644 index 0000000000..2799792d89 Binary files /dev/null and b/assets/stickers/animals/sheep.png differ diff --git a/assets/stickers/animals/siberian-husky.png b/assets/stickers/animals/siberian-husky.png new file mode 100644 index 0000000000..21e619bca4 Binary files /dev/null and b/assets/stickers/animals/siberian-husky.png differ diff --git a/assets/stickers/animals/sloth.png b/assets/stickers/animals/sloth.png new file mode 100644 index 0000000000..b22f3c7383 Binary files /dev/null and b/assets/stickers/animals/sloth.png differ diff --git a/assets/stickers/animals/snake.png b/assets/stickers/animals/snake.png new file mode 100644 index 0000000000..ab2bffafa2 Binary files /dev/null and b/assets/stickers/animals/snake.png differ diff --git a/assets/stickers/animals/spider.png b/assets/stickers/animals/spider.png new file mode 100644 index 0000000000..36843a6666 Binary files /dev/null and b/assets/stickers/animals/spider.png differ diff --git a/assets/stickers/animals/squirrel.png b/assets/stickers/animals/squirrel.png new file mode 100644 index 0000000000..bd75170e22 Binary files /dev/null and b/assets/stickers/animals/squirrel.png differ diff --git a/assets/stickers/animals/swan.png b/assets/stickers/animals/swan.png new file mode 100644 index 0000000000..832449151f Binary files /dev/null and b/assets/stickers/animals/swan.png differ diff --git a/assets/stickers/animals/tiger.png b/assets/stickers/animals/tiger.png new file mode 100644 index 0000000000..0ff4f9c894 Binary files /dev/null and b/assets/stickers/animals/tiger.png differ diff --git a/assets/stickers/animals/toucan.png b/assets/stickers/animals/toucan.png new file mode 100644 index 0000000000..0c4986722f Binary files /dev/null and b/assets/stickers/animals/toucan.png differ diff --git a/assets/stickers/animals/turtle.png b/assets/stickers/animals/turtle.png new file mode 100644 index 0000000000..cca4b7d138 Binary files /dev/null and b/assets/stickers/animals/turtle.png differ diff --git a/assets/stickers/animals/whale.png b/assets/stickers/animals/whale.png new file mode 100644 index 0000000000..ff0c3227cb Binary files /dev/null and b/assets/stickers/animals/whale.png differ diff --git a/assets/stickers/clothes/backpack.png b/assets/stickers/clothes/backpack.png new file mode 100644 index 0000000000..da551cc24f Binary files /dev/null and b/assets/stickers/clothes/backpack.png differ diff --git a/assets/stickers/clothes/bathrobe.png b/assets/stickers/clothes/bathrobe.png new file mode 100644 index 0000000000..c0ed9a76ca Binary files /dev/null and b/assets/stickers/clothes/bathrobe.png differ diff --git a/assets/stickers/clothes/belt.png b/assets/stickers/clothes/belt.png new file mode 100644 index 0000000000..0c8724a113 Binary files /dev/null and b/assets/stickers/clothes/belt.png differ diff --git a/assets/stickers/clothes/boot.png b/assets/stickers/clothes/boot.png new file mode 100644 index 0000000000..2bbc116e62 Binary files /dev/null and b/assets/stickers/clothes/boot.png differ diff --git a/assets/stickers/clothes/bow-tie.png b/assets/stickers/clothes/bow-tie.png new file mode 100644 index 0000000000..5cb39bf95e Binary files /dev/null and b/assets/stickers/clothes/bow-tie.png differ diff --git a/assets/stickers/clothes/bowler-hat.png b/assets/stickers/clothes/bowler-hat.png new file mode 100644 index 0000000000..05f9675f56 Binary files /dev/null and b/assets/stickers/clothes/bowler-hat.png differ diff --git a/assets/stickers/clothes/boxers.png b/assets/stickers/clothes/boxers.png new file mode 100644 index 0000000000..38eba6fbf2 Binary files /dev/null and b/assets/stickers/clothes/boxers.png differ diff --git a/assets/stickers/clothes/bra.png b/assets/stickers/clothes/bra.png new file mode 100644 index 0000000000..6e98519f60 Binary files /dev/null and b/assets/stickers/clothes/bra.png differ diff --git a/assets/stickers/clothes/cap.png b/assets/stickers/clothes/cap.png new file mode 100644 index 0000000000..c1abd23ed6 Binary files /dev/null and b/assets/stickers/clothes/cap.png differ diff --git a/assets/stickers/clothes/dress-1.png b/assets/stickers/clothes/dress-1.png new file mode 100644 index 0000000000..67e961b94f Binary files /dev/null and b/assets/stickers/clothes/dress-1.png differ diff --git a/assets/stickers/clothes/dress-2.png b/assets/stickers/clothes/dress-2.png new file mode 100644 index 0000000000..af94d5f40c Binary files /dev/null and b/assets/stickers/clothes/dress-2.png differ diff --git a/assets/stickers/clothes/dress-3.png b/assets/stickers/clothes/dress-3.png new file mode 100644 index 0000000000..74ff0cb9a5 Binary files /dev/null and b/assets/stickers/clothes/dress-3.png differ diff --git a/assets/stickers/clothes/dress.png b/assets/stickers/clothes/dress.png new file mode 100644 index 0000000000..73a85cc10a Binary files /dev/null and b/assets/stickers/clothes/dress.png differ diff --git a/assets/stickers/clothes/glasses.png b/assets/stickers/clothes/glasses.png new file mode 100644 index 0000000000..5de0e5bf1d Binary files /dev/null and b/assets/stickers/clothes/glasses.png differ diff --git a/assets/stickers/clothes/hat.png b/assets/stickers/clothes/hat.png new file mode 100644 index 0000000000..f383ab1429 Binary files /dev/null and b/assets/stickers/clothes/hat.png differ diff --git a/assets/stickers/clothes/high-heel.png b/assets/stickers/clothes/high-heel.png new file mode 100644 index 0000000000..b488e7cc1a Binary files /dev/null and b/assets/stickers/clothes/high-heel.png differ diff --git a/assets/stickers/clothes/jacket-1.png b/assets/stickers/clothes/jacket-1.png new file mode 100644 index 0000000000..0d18f1b992 Binary files /dev/null and b/assets/stickers/clothes/jacket-1.png differ diff --git a/assets/stickers/clothes/jacket-2.png b/assets/stickers/clothes/jacket-2.png new file mode 100644 index 0000000000..7b87ef8b1f Binary files /dev/null and b/assets/stickers/clothes/jacket-2.png differ diff --git a/assets/stickers/clothes/jacket-3.png b/assets/stickers/clothes/jacket-3.png new file mode 100644 index 0000000000..fc9229ab47 Binary files /dev/null and b/assets/stickers/clothes/jacket-3.png differ diff --git a/assets/stickers/clothes/jacket-4.png b/assets/stickers/clothes/jacket-4.png new file mode 100644 index 0000000000..fd8fa1df1f Binary files /dev/null and b/assets/stickers/clothes/jacket-4.png differ diff --git a/assets/stickers/clothes/jacket.png b/assets/stickers/clothes/jacket.png new file mode 100644 index 0000000000..952e1cc03e Binary files /dev/null and b/assets/stickers/clothes/jacket.png differ diff --git a/assets/stickers/clothes/jeans.png b/assets/stickers/clothes/jeans.png new file mode 100644 index 0000000000..f6dc108d2c Binary files /dev/null and b/assets/stickers/clothes/jeans.png differ diff --git a/assets/stickers/clothes/lingerie.png b/assets/stickers/clothes/lingerie.png new file mode 100644 index 0000000000..5c569eeaec Binary files /dev/null and b/assets/stickers/clothes/lingerie.png differ diff --git a/assets/stickers/clothes/overall.png b/assets/stickers/clothes/overall.png new file mode 100644 index 0000000000..50afb35108 Binary files /dev/null and b/assets/stickers/clothes/overall.png differ diff --git a/assets/stickers/clothes/polo.png b/assets/stickers/clothes/polo.png new file mode 100644 index 0000000000..d61d39c05c Binary files /dev/null and b/assets/stickers/clothes/polo.png differ diff --git a/assets/stickers/clothes/pullover.png b/assets/stickers/clothes/pullover.png new file mode 100644 index 0000000000..fa0763bde8 Binary files /dev/null and b/assets/stickers/clothes/pullover.png differ diff --git a/assets/stickers/clothes/purse-1.png b/assets/stickers/clothes/purse-1.png new file mode 100644 index 0000000000..f0c8e71583 Binary files /dev/null and b/assets/stickers/clothes/purse-1.png differ diff --git a/assets/stickers/clothes/purse.png b/assets/stickers/clothes/purse.png new file mode 100644 index 0000000000..1e80ded3d6 Binary files /dev/null and b/assets/stickers/clothes/purse.png differ diff --git a/assets/stickers/clothes/scarf.png b/assets/stickers/clothes/scarf.png new file mode 100644 index 0000000000..6502e06b67 Binary files /dev/null and b/assets/stickers/clothes/scarf.png differ diff --git a/assets/stickers/clothes/shirt-1.png b/assets/stickers/clothes/shirt-1.png new file mode 100644 index 0000000000..a161e73491 Binary files /dev/null and b/assets/stickers/clothes/shirt-1.png differ diff --git a/assets/stickers/clothes/shirt-2.png b/assets/stickers/clothes/shirt-2.png new file mode 100644 index 0000000000..ee7761926d Binary files /dev/null and b/assets/stickers/clothes/shirt-2.png differ diff --git a/assets/stickers/clothes/shirt.png b/assets/stickers/clothes/shirt.png new file mode 100644 index 0000000000..834d74ccbf Binary files /dev/null and b/assets/stickers/clothes/shirt.png differ diff --git a/assets/stickers/clothes/shoe.png b/assets/stickers/clothes/shoe.png new file mode 100644 index 0000000000..e2470e3573 Binary files /dev/null and b/assets/stickers/clothes/shoe.png differ diff --git a/assets/stickers/clothes/shorts.png b/assets/stickers/clothes/shorts.png new file mode 100644 index 0000000000..a28db71323 Binary files /dev/null and b/assets/stickers/clothes/shorts.png differ diff --git a/assets/stickers/clothes/skirt.png b/assets/stickers/clothes/skirt.png new file mode 100644 index 0000000000..f584573b75 Binary files /dev/null and b/assets/stickers/clothes/skirt.png differ diff --git a/assets/stickers/clothes/sleeveless.png b/assets/stickers/clothes/sleeveless.png new file mode 100644 index 0000000000..c7f8eed064 Binary files /dev/null and b/assets/stickers/clothes/sleeveless.png differ diff --git a/assets/stickers/clothes/slippers.png b/assets/stickers/clothes/slippers.png new file mode 100644 index 0000000000..d1856025b9 Binary files /dev/null and b/assets/stickers/clothes/slippers.png differ diff --git a/assets/stickers/clothes/sneakers-1.png b/assets/stickers/clothes/sneakers-1.png new file mode 100644 index 0000000000..aec9611d09 Binary files /dev/null and b/assets/stickers/clothes/sneakers-1.png differ diff --git a/assets/stickers/clothes/sneakers.png b/assets/stickers/clothes/sneakers.png new file mode 100644 index 0000000000..7819f1015d Binary files /dev/null and b/assets/stickers/clothes/sneakers.png differ diff --git a/assets/stickers/clothes/socks.png b/assets/stickers/clothes/socks.png new file mode 100644 index 0000000000..115efb4dac Binary files /dev/null and b/assets/stickers/clothes/socks.png differ diff --git a/assets/stickers/clothes/suitcase.png b/assets/stickers/clothes/suitcase.png new file mode 100644 index 0000000000..22f1ec57ed Binary files /dev/null and b/assets/stickers/clothes/suitcase.png differ diff --git a/assets/stickers/clothes/sweatshirt.png b/assets/stickers/clothes/sweatshirt.png new file mode 100644 index 0000000000..1f0d71816c Binary files /dev/null and b/assets/stickers/clothes/sweatshirt.png differ diff --git a/assets/stickers/clothes/swimsuit-1.png b/assets/stickers/clothes/swimsuit-1.png new file mode 100644 index 0000000000..3ca05d0329 Binary files /dev/null and b/assets/stickers/clothes/swimsuit-1.png differ diff --git a/assets/stickers/clothes/swimsuit.png b/assets/stickers/clothes/swimsuit.png new file mode 100644 index 0000000000..e0b3207dc6 Binary files /dev/null and b/assets/stickers/clothes/swimsuit.png differ diff --git a/assets/stickers/clothes/tie.png b/assets/stickers/clothes/tie.png new file mode 100644 index 0000000000..a881563c8f Binary files /dev/null and b/assets/stickers/clothes/tie.png differ diff --git a/assets/stickers/clothes/trench-coat.png b/assets/stickers/clothes/trench-coat.png new file mode 100644 index 0000000000..9d5a92b0ec Binary files /dev/null and b/assets/stickers/clothes/trench-coat.png differ diff --git a/assets/stickers/clothes/trousers.png b/assets/stickers/clothes/trousers.png new file mode 100644 index 0000000000..53e9d47884 Binary files /dev/null and b/assets/stickers/clothes/trousers.png differ diff --git a/assets/stickers/clothes/underpants.png b/assets/stickers/clothes/underpants.png new file mode 100644 index 0000000000..e381a692a0 Binary files /dev/null and b/assets/stickers/clothes/underpants.png differ diff --git a/assets/stickers/clothes/vest.png b/assets/stickers/clothes/vest.png new file mode 100644 index 0000000000..2a55a3dd0b Binary files /dev/null and b/assets/stickers/clothes/vest.png differ diff --git a/assets/stickers/clothes/winter-hat.png b/assets/stickers/clothes/winter-hat.png new file mode 100644 index 0000000000..202a4383c0 Binary files /dev/null and b/assets/stickers/clothes/winter-hat.png differ diff --git a/assets/stickers/emoticons/angry-1.png b/assets/stickers/emoticons/angry-1.png new file mode 100644 index 0000000000..a881db9bee Binary files /dev/null and b/assets/stickers/emoticons/angry-1.png differ diff --git a/assets/stickers/emoticons/angry.png b/assets/stickers/emoticons/angry.png new file mode 100644 index 0000000000..d78a86d3bc Binary files /dev/null and b/assets/stickers/emoticons/angry.png differ diff --git a/assets/stickers/emoticons/bored-1.png b/assets/stickers/emoticons/bored-1.png new file mode 100644 index 0000000000..7655c2ba67 Binary files /dev/null and b/assets/stickers/emoticons/bored-1.png differ diff --git a/assets/stickers/emoticons/bored-2.png b/assets/stickers/emoticons/bored-2.png new file mode 100644 index 0000000000..cbd5769567 Binary files /dev/null and b/assets/stickers/emoticons/bored-2.png differ diff --git a/assets/stickers/emoticons/bored.png b/assets/stickers/emoticons/bored.png new file mode 100644 index 0000000000..1e56e6ba2e Binary files /dev/null and b/assets/stickers/emoticons/bored.png differ diff --git a/assets/stickers/emoticons/confused-1.png b/assets/stickers/emoticons/confused-1.png new file mode 100644 index 0000000000..5f81f7e8eb Binary files /dev/null and b/assets/stickers/emoticons/confused-1.png differ diff --git a/assets/stickers/emoticons/confused.png b/assets/stickers/emoticons/confused.png new file mode 100644 index 0000000000..2306f5c406 Binary files /dev/null and b/assets/stickers/emoticons/confused.png differ diff --git a/assets/stickers/emoticons/crying-1.png b/assets/stickers/emoticons/crying-1.png new file mode 100644 index 0000000000..b63927db8c Binary files /dev/null and b/assets/stickers/emoticons/crying-1.png differ diff --git a/assets/stickers/emoticons/crying.png b/assets/stickers/emoticons/crying.png new file mode 100644 index 0000000000..f8af637610 Binary files /dev/null and b/assets/stickers/emoticons/crying.png differ diff --git a/assets/stickers/emoticons/embarrassed.png b/assets/stickers/emoticons/embarrassed.png new file mode 100644 index 0000000000..32333eb8f8 Binary files /dev/null and b/assets/stickers/emoticons/embarrassed.png differ diff --git a/assets/stickers/emoticons/emoticons.png b/assets/stickers/emoticons/emoticons.png new file mode 100644 index 0000000000..c40f0b8a37 Binary files /dev/null and b/assets/stickers/emoticons/emoticons.png differ diff --git a/assets/stickers/emoticons/happy-1.png b/assets/stickers/emoticons/happy-1.png new file mode 100644 index 0000000000..32bedb0323 Binary files /dev/null and b/assets/stickers/emoticons/happy-1.png differ diff --git a/assets/stickers/emoticons/happy-2.png b/assets/stickers/emoticons/happy-2.png new file mode 100644 index 0000000000..5dafe9271f Binary files /dev/null and b/assets/stickers/emoticons/happy-2.png differ diff --git a/assets/stickers/emoticons/happy-3.png b/assets/stickers/emoticons/happy-3.png new file mode 100644 index 0000000000..65ef3d3cff Binary files /dev/null and b/assets/stickers/emoticons/happy-3.png differ diff --git a/assets/stickers/emoticons/happy-4.png b/assets/stickers/emoticons/happy-4.png new file mode 100644 index 0000000000..35f14c9a7d Binary files /dev/null and b/assets/stickers/emoticons/happy-4.png differ diff --git a/assets/stickers/emoticons/happy.png b/assets/stickers/emoticons/happy.png new file mode 100644 index 0000000000..090fe16809 Binary files /dev/null and b/assets/stickers/emoticons/happy.png differ diff --git a/assets/stickers/emoticons/ill.png b/assets/stickers/emoticons/ill.png new file mode 100644 index 0000000000..9c12470350 Binary files /dev/null and b/assets/stickers/emoticons/ill.png differ diff --git a/assets/stickers/emoticons/in-love.png b/assets/stickers/emoticons/in-love.png new file mode 100644 index 0000000000..256a8fbc13 Binary files /dev/null and b/assets/stickers/emoticons/in-love.png differ diff --git a/assets/stickers/emoticons/kissing.png b/assets/stickers/emoticons/kissing.png new file mode 100644 index 0000000000..2b2f7da188 Binary files /dev/null and b/assets/stickers/emoticons/kissing.png differ diff --git a/assets/stickers/emoticons/mad.png b/assets/stickers/emoticons/mad.png new file mode 100644 index 0000000000..cb0dcaa7fd Binary files /dev/null and b/assets/stickers/emoticons/mad.png differ diff --git a/assets/stickers/emoticons/nerd.png b/assets/stickers/emoticons/nerd.png new file mode 100644 index 0000000000..1a9f8fcbb7 Binary files /dev/null and b/assets/stickers/emoticons/nerd.png differ diff --git a/assets/stickers/emoticons/ninja.png b/assets/stickers/emoticons/ninja.png new file mode 100644 index 0000000000..d9ce9b8c69 Binary files /dev/null and b/assets/stickers/emoticons/ninja.png differ diff --git a/assets/stickers/emoticons/quiet.png b/assets/stickers/emoticons/quiet.png new file mode 100644 index 0000000000..7de2ea4f9d Binary files /dev/null and b/assets/stickers/emoticons/quiet.png differ diff --git a/assets/stickers/emoticons/sad.png b/assets/stickers/emoticons/sad.png new file mode 100644 index 0000000000..af670a5df4 Binary files /dev/null and b/assets/stickers/emoticons/sad.png differ diff --git a/assets/stickers/emoticons/secret.png b/assets/stickers/emoticons/secret.png new file mode 100644 index 0000000000..6071355744 Binary files /dev/null and b/assets/stickers/emoticons/secret.png differ diff --git a/assets/stickers/emoticons/smart.png b/assets/stickers/emoticons/smart.png new file mode 100644 index 0000000000..e607932aac Binary files /dev/null and b/assets/stickers/emoticons/smart.png differ diff --git a/assets/stickers/emoticons/smile.png b/assets/stickers/emoticons/smile.png new file mode 100644 index 0000000000..7deeb0c248 Binary files /dev/null and b/assets/stickers/emoticons/smile.png differ diff --git a/assets/stickers/emoticons/smiling.png b/assets/stickers/emoticons/smiling.png new file mode 100644 index 0000000000..0d3e413809 Binary files /dev/null and b/assets/stickers/emoticons/smiling.png differ diff --git a/assets/stickers/emoticons/surprised-1.png b/assets/stickers/emoticons/surprised-1.png new file mode 100644 index 0000000000..1b8da1e358 Binary files /dev/null and b/assets/stickers/emoticons/surprised-1.png differ diff --git a/assets/stickers/emoticons/surprised.png b/assets/stickers/emoticons/surprised.png new file mode 100644 index 0000000000..554201279f Binary files /dev/null and b/assets/stickers/emoticons/surprised.png differ diff --git a/assets/stickers/emoticons/suspicious-1.png b/assets/stickers/emoticons/suspicious-1.png new file mode 100644 index 0000000000..d9b4aab263 Binary files /dev/null and b/assets/stickers/emoticons/suspicious-1.png differ diff --git a/assets/stickers/emoticons/suspicious.png b/assets/stickers/emoticons/suspicious.png new file mode 100644 index 0000000000..3d33e5bc2b Binary files /dev/null and b/assets/stickers/emoticons/suspicious.png differ diff --git a/assets/stickers/emoticons/tongue-out-1.png b/assets/stickers/emoticons/tongue-out-1.png new file mode 100644 index 0000000000..98341abad8 Binary files /dev/null and b/assets/stickers/emoticons/tongue-out-1.png differ diff --git a/assets/stickers/emoticons/tongue-out.png b/assets/stickers/emoticons/tongue-out.png new file mode 100644 index 0000000000..62616f0e40 Binary files /dev/null and b/assets/stickers/emoticons/tongue-out.png differ diff --git a/assets/stickers/emoticons/unhappy.png b/assets/stickers/emoticons/unhappy.png new file mode 100644 index 0000000000..29e0b19d67 Binary files /dev/null and b/assets/stickers/emoticons/unhappy.png differ diff --git a/assets/stickers/emoticons/wink.png b/assets/stickers/emoticons/wink.png new file mode 100644 index 0000000000..9902bd8e8e Binary files /dev/null and b/assets/stickers/emoticons/wink.png differ diff --git a/assets/stickers/food/apple.png b/assets/stickers/food/apple.png new file mode 100644 index 0000000000..c0c87ad7a3 Binary files /dev/null and b/assets/stickers/food/apple.png differ diff --git a/assets/stickers/food/artichoke.png b/assets/stickers/food/artichoke.png new file mode 100644 index 0000000000..618b2d5744 Binary files /dev/null and b/assets/stickers/food/artichoke.png differ diff --git a/assets/stickers/food/aubergine.png b/assets/stickers/food/aubergine.png new file mode 100644 index 0000000000..0c89cbe268 Binary files /dev/null and b/assets/stickers/food/aubergine.png differ diff --git a/assets/stickers/food/avocado.png b/assets/stickers/food/avocado.png new file mode 100644 index 0000000000..8183cfdf2c Binary files /dev/null and b/assets/stickers/food/avocado.png differ diff --git a/assets/stickers/food/bacon.png b/assets/stickers/food/bacon.png new file mode 100644 index 0000000000..b727419969 Binary files /dev/null and b/assets/stickers/food/bacon.png differ diff --git a/assets/stickers/food/banana.png b/assets/stickers/food/banana.png new file mode 100644 index 0000000000..78e87340c3 Binary files /dev/null and b/assets/stickers/food/banana.png differ diff --git a/assets/stickers/food/beans.png b/assets/stickers/food/beans.png new file mode 100644 index 0000000000..7cfd0ef42c Binary files /dev/null and b/assets/stickers/food/beans.png differ diff --git a/assets/stickers/food/bell-pepper.png b/assets/stickers/food/bell-pepper.png new file mode 100644 index 0000000000..74e18b68cd Binary files /dev/null and b/assets/stickers/food/bell-pepper.png differ diff --git a/assets/stickers/food/birthday-cake.png b/assets/stickers/food/birthday-cake.png new file mode 100644 index 0000000000..fbb80c4e49 Binary files /dev/null and b/assets/stickers/food/birthday-cake.png differ diff --git a/assets/stickers/food/biscuit.png b/assets/stickers/food/biscuit.png new file mode 100644 index 0000000000..660dff4a1a Binary files /dev/null and b/assets/stickers/food/biscuit.png differ diff --git a/assets/stickers/food/boiled-egg.png b/assets/stickers/food/boiled-egg.png new file mode 100644 index 0000000000..fb4a1e867e Binary files /dev/null and b/assets/stickers/food/boiled-egg.png differ diff --git a/assets/stickers/food/bread.png b/assets/stickers/food/bread.png new file mode 100644 index 0000000000..ed19ee7102 Binary files /dev/null and b/assets/stickers/food/bread.png differ diff --git a/assets/stickers/food/broccoli.png b/assets/stickers/food/broccoli.png new file mode 100644 index 0000000000..9a13522ce2 Binary files /dev/null and b/assets/stickers/food/broccoli.png differ diff --git a/assets/stickers/food/brochette.png b/assets/stickers/food/brochette.png new file mode 100644 index 0000000000..3658cbd777 Binary files /dev/null and b/assets/stickers/food/brochette.png differ diff --git a/assets/stickers/food/burger-1.png b/assets/stickers/food/burger-1.png new file mode 100644 index 0000000000..3f7f3ff662 Binary files /dev/null and b/assets/stickers/food/burger-1.png differ diff --git a/assets/stickers/food/burger.png b/assets/stickers/food/burger.png new file mode 100644 index 0000000000..203d336ffb Binary files /dev/null and b/assets/stickers/food/burger.png differ diff --git a/assets/stickers/food/burrito.png b/assets/stickers/food/burrito.png new file mode 100644 index 0000000000..3c3c3e5b08 Binary files /dev/null and b/assets/stickers/food/burrito.png differ diff --git a/assets/stickers/food/cake.png b/assets/stickers/food/cake.png new file mode 100644 index 0000000000..0d59a3e3cf Binary files /dev/null and b/assets/stickers/food/cake.png differ diff --git a/assets/stickers/food/candy-cane.png b/assets/stickers/food/candy-cane.png new file mode 100644 index 0000000000..3d8f048262 Binary files /dev/null and b/assets/stickers/food/candy-cane.png differ diff --git a/assets/stickers/food/candy.png b/assets/stickers/food/candy.png new file mode 100644 index 0000000000..1fb7d70819 Binary files /dev/null and b/assets/stickers/food/candy.png differ diff --git a/assets/stickers/food/carrot.png b/assets/stickers/food/carrot.png new file mode 100644 index 0000000000..1ca4617161 Binary files /dev/null and b/assets/stickers/food/carrot.png differ diff --git a/assets/stickers/food/cheese.png b/assets/stickers/food/cheese.png new file mode 100644 index 0000000000..df545a670a Binary files /dev/null and b/assets/stickers/food/cheese.png differ diff --git a/assets/stickers/food/cherry.png b/assets/stickers/food/cherry.png new file mode 100644 index 0000000000..c1081f47a3 Binary files /dev/null and b/assets/stickers/food/cherry.png differ diff --git a/assets/stickers/food/chicken-leg.png b/assets/stickers/food/chicken-leg.png new file mode 100644 index 0000000000..044084cb37 Binary files /dev/null and b/assets/stickers/food/chicken-leg.png differ diff --git a/assets/stickers/food/chili-pepper.png b/assets/stickers/food/chili-pepper.png new file mode 100644 index 0000000000..163271db49 Binary files /dev/null and b/assets/stickers/food/chili-pepper.png differ diff --git a/assets/stickers/food/chocolate.png b/assets/stickers/food/chocolate.png new file mode 100644 index 0000000000..dd2a7f27f1 Binary files /dev/null and b/assets/stickers/food/chocolate.png differ diff --git a/assets/stickers/food/chorizo.png b/assets/stickers/food/chorizo.png new file mode 100644 index 0000000000..fe524120a3 Binary files /dev/null and b/assets/stickers/food/chorizo.png differ diff --git a/assets/stickers/food/corn.png b/assets/stickers/food/corn.png new file mode 100644 index 0000000000..b1e5c32f49 Binary files /dev/null and b/assets/stickers/food/corn.png differ diff --git a/assets/stickers/food/cotton-candy.png b/assets/stickers/food/cotton-candy.png new file mode 100644 index 0000000000..1013eb2eeb Binary files /dev/null and b/assets/stickers/food/cotton-candy.png differ diff --git a/assets/stickers/food/crab.png b/assets/stickers/food/crab.png new file mode 100644 index 0000000000..1a1e277425 Binary files /dev/null and b/assets/stickers/food/crab.png differ diff --git a/assets/stickers/food/croissant.png b/assets/stickers/food/croissant.png new file mode 100644 index 0000000000..0cc7ff3686 Binary files /dev/null and b/assets/stickers/food/croissant.png differ diff --git a/assets/stickers/food/cupcake-1.png b/assets/stickers/food/cupcake-1.png new file mode 100644 index 0000000000..26fc0ed216 Binary files /dev/null and b/assets/stickers/food/cupcake-1.png differ diff --git a/assets/stickers/food/cupcake.png b/assets/stickers/food/cupcake.png new file mode 100644 index 0000000000..61b93ab6e0 Binary files /dev/null and b/assets/stickers/food/cupcake.png differ diff --git a/assets/stickers/food/doner-kebab.png b/assets/stickers/food/doner-kebab.png new file mode 100644 index 0000000000..47fc90d10e Binary files /dev/null and b/assets/stickers/food/doner-kebab.png differ diff --git a/assets/stickers/food/donut.png b/assets/stickers/food/donut.png new file mode 100644 index 0000000000..463b16438b Binary files /dev/null and b/assets/stickers/food/donut.png differ diff --git a/assets/stickers/food/drink.png b/assets/stickers/food/drink.png new file mode 100644 index 0000000000..269aad4de9 Binary files /dev/null and b/assets/stickers/food/drink.png differ diff --git a/assets/stickers/food/fish.png b/assets/stickers/food/fish.png new file mode 100644 index 0000000000..754e1df4a5 Binary files /dev/null and b/assets/stickers/food/fish.png differ diff --git a/assets/stickers/food/french-fries.png b/assets/stickers/food/french-fries.png new file mode 100644 index 0000000000..f26bf8aa35 Binary files /dev/null and b/assets/stickers/food/french-fries.png differ diff --git a/assets/stickers/food/fried-egg.png b/assets/stickers/food/fried-egg.png new file mode 100644 index 0000000000..ce6cd45cd2 Binary files /dev/null and b/assets/stickers/food/fried-egg.png differ diff --git a/assets/stickers/food/garlic.png b/assets/stickers/food/garlic.png new file mode 100644 index 0000000000..53a697ea0c Binary files /dev/null and b/assets/stickers/food/garlic.png differ diff --git a/assets/stickers/food/gingerbread-man.png b/assets/stickers/food/gingerbread-man.png new file mode 100644 index 0000000000..f3de236dcb Binary files /dev/null and b/assets/stickers/food/gingerbread-man.png differ diff --git a/assets/stickers/food/grapes.png b/assets/stickers/food/grapes.png new file mode 100644 index 0000000000..4b44de8833 Binary files /dev/null and b/assets/stickers/food/grapes.png differ diff --git a/assets/stickers/food/honey.png b/assets/stickers/food/honey.png new file mode 100644 index 0000000000..fcb5a88a65 Binary files /dev/null and b/assets/stickers/food/honey.png differ diff --git a/assets/stickers/food/hot-dog.png b/assets/stickers/food/hot-dog.png new file mode 100644 index 0000000000..ac7a7c9c14 Binary files /dev/null and b/assets/stickers/food/hot-dog.png differ diff --git a/assets/stickers/food/ice-cream.png b/assets/stickers/food/ice-cream.png new file mode 100644 index 0000000000..b7957676e1 Binary files /dev/null and b/assets/stickers/food/ice-cream.png differ diff --git a/assets/stickers/food/jam.png b/assets/stickers/food/jam.png new file mode 100644 index 0000000000..ec0ad544d8 Binary files /dev/null and b/assets/stickers/food/jam.png differ diff --git a/assets/stickers/food/jelly.png b/assets/stickers/food/jelly.png new file mode 100644 index 0000000000..d8da5f5c0b Binary files /dev/null and b/assets/stickers/food/jelly.png differ diff --git a/assets/stickers/food/ketchup.png b/assets/stickers/food/ketchup.png new file mode 100644 index 0000000000..6918b39337 Binary files /dev/null and b/assets/stickers/food/ketchup.png differ diff --git a/assets/stickers/food/kiwi.png b/assets/stickers/food/kiwi.png new file mode 100644 index 0000000000..dd6e03bf78 Binary files /dev/null and b/assets/stickers/food/kiwi.png differ diff --git a/assets/stickers/food/lemon.png b/assets/stickers/food/lemon.png new file mode 100644 index 0000000000..c9a4682722 Binary files /dev/null and b/assets/stickers/food/lemon.png differ diff --git a/assets/stickers/food/lettuce.png b/assets/stickers/food/lettuce.png new file mode 100644 index 0000000000..0e98a66811 Binary files /dev/null and b/assets/stickers/food/lettuce.png differ diff --git a/assets/stickers/food/lobster.png b/assets/stickers/food/lobster.png new file mode 100644 index 0000000000..e81ef92009 Binary files /dev/null and b/assets/stickers/food/lobster.png differ diff --git a/assets/stickers/food/lollipop-1.png b/assets/stickers/food/lollipop-1.png new file mode 100644 index 0000000000..0ac95d80ef Binary files /dev/null and b/assets/stickers/food/lollipop-1.png differ diff --git a/assets/stickers/food/lollipop.png b/assets/stickers/food/lollipop.png new file mode 100644 index 0000000000..6e80ca6b4a Binary files /dev/null and b/assets/stickers/food/lollipop.png differ diff --git a/assets/stickers/food/macarons.png b/assets/stickers/food/macarons.png new file mode 100644 index 0000000000..8235533ca0 Binary files /dev/null and b/assets/stickers/food/macarons.png differ diff --git a/assets/stickers/food/muffin.png b/assets/stickers/food/muffin.png new file mode 100644 index 0000000000..66c49b5d49 Binary files /dev/null and b/assets/stickers/food/muffin.png differ diff --git a/assets/stickers/food/mushroom.png b/assets/stickers/food/mushroom.png new file mode 100644 index 0000000000..787e88aaa3 Binary files /dev/null and b/assets/stickers/food/mushroom.png differ diff --git a/assets/stickers/food/mussel.png b/assets/stickers/food/mussel.png new file mode 100644 index 0000000000..bfbe8fb357 Binary files /dev/null and b/assets/stickers/food/mussel.png differ diff --git a/assets/stickers/food/noodles.png b/assets/stickers/food/noodles.png new file mode 100644 index 0000000000..e23b8b7498 Binary files /dev/null and b/assets/stickers/food/noodles.png differ diff --git a/assets/stickers/food/olive-oil.png b/assets/stickers/food/olive-oil.png new file mode 100644 index 0000000000..e23608592c Binary files /dev/null and b/assets/stickers/food/olive-oil.png differ diff --git a/assets/stickers/food/olives.png b/assets/stickers/food/olives.png new file mode 100644 index 0000000000..10f4447149 Binary files /dev/null and b/assets/stickers/food/olives.png differ diff --git a/assets/stickers/food/onion-rings.png b/assets/stickers/food/onion-rings.png new file mode 100644 index 0000000000..45e266c6e2 Binary files /dev/null and b/assets/stickers/food/onion-rings.png differ diff --git a/assets/stickers/food/onion.png b/assets/stickers/food/onion.png new file mode 100644 index 0000000000..9d9357066c Binary files /dev/null and b/assets/stickers/food/onion.png differ diff --git a/assets/stickers/food/orange.png b/assets/stickers/food/orange.png new file mode 100644 index 0000000000..23f35fc787 Binary files /dev/null and b/assets/stickers/food/orange.png differ diff --git a/assets/stickers/food/pancakes.png b/assets/stickers/food/pancakes.png new file mode 100644 index 0000000000..e7550258aa Binary files /dev/null and b/assets/stickers/food/pancakes.png differ diff --git a/assets/stickers/food/pasta.png b/assets/stickers/food/pasta.png new file mode 100644 index 0000000000..c68500294c Binary files /dev/null and b/assets/stickers/food/pasta.png differ diff --git a/assets/stickers/food/peach.png b/assets/stickers/food/peach.png new file mode 100644 index 0000000000..6826b4b779 Binary files /dev/null and b/assets/stickers/food/peach.png differ diff --git a/assets/stickers/food/pear.png b/assets/stickers/food/pear.png new file mode 100644 index 0000000000..21a09b3dc4 Binary files /dev/null and b/assets/stickers/food/pear.png differ diff --git a/assets/stickers/food/pepper.png b/assets/stickers/food/pepper.png new file mode 100644 index 0000000000..241bfe317e Binary files /dev/null and b/assets/stickers/food/pepper.png differ diff --git a/assets/stickers/food/pie.png b/assets/stickers/food/pie.png new file mode 100644 index 0000000000..cc10302a99 Binary files /dev/null and b/assets/stickers/food/pie.png differ diff --git a/assets/stickers/food/pineapple.png b/assets/stickers/food/pineapple.png new file mode 100644 index 0000000000..71eb128141 Binary files /dev/null and b/assets/stickers/food/pineapple.png differ diff --git a/assets/stickers/food/pizza.png b/assets/stickers/food/pizza.png new file mode 100644 index 0000000000..0161cf59c5 Binary files /dev/null and b/assets/stickers/food/pizza.png differ diff --git a/assets/stickers/food/popcorn.png b/assets/stickers/food/popcorn.png new file mode 100644 index 0000000000..43c8477f12 Binary files /dev/null and b/assets/stickers/food/popcorn.png differ diff --git a/assets/stickers/food/prawn.png b/assets/stickers/food/prawn.png new file mode 100644 index 0000000000..afe5431ab1 Binary files /dev/null and b/assets/stickers/food/prawn.png differ diff --git a/assets/stickers/food/pretzel.png b/assets/stickers/food/pretzel.png new file mode 100644 index 0000000000..bbe72aa23b Binary files /dev/null and b/assets/stickers/food/pretzel.png differ diff --git a/assets/stickers/food/pumpkin.png b/assets/stickers/food/pumpkin.png new file mode 100644 index 0000000000..4572125205 Binary files /dev/null and b/assets/stickers/food/pumpkin.png differ diff --git a/assets/stickers/food/radish.png b/assets/stickers/food/radish.png new file mode 100644 index 0000000000..5284a8b4d1 Binary files /dev/null and b/assets/stickers/food/radish.png differ diff --git a/assets/stickers/food/raspberry.png b/assets/stickers/food/raspberry.png new file mode 100644 index 0000000000..e0238f530f Binary files /dev/null and b/assets/stickers/food/raspberry.png differ diff --git a/assets/stickers/food/rice.png b/assets/stickers/food/rice.png new file mode 100644 index 0000000000..55bd59397c Binary files /dev/null and b/assets/stickers/food/rice.png differ diff --git a/assets/stickers/food/roast-chicken.png b/assets/stickers/food/roast-chicken.png new file mode 100644 index 0000000000..afff0a926a Binary files /dev/null and b/assets/stickers/food/roast-chicken.png differ diff --git a/assets/stickers/food/salad.png b/assets/stickers/food/salad.png new file mode 100644 index 0000000000..c9f0e4e6b1 Binary files /dev/null and b/assets/stickers/food/salad.png differ diff --git a/assets/stickers/food/salt.png b/assets/stickers/food/salt.png new file mode 100644 index 0000000000..6850890abf Binary files /dev/null and b/assets/stickers/food/salt.png differ diff --git a/assets/stickers/food/sandwich-1.png b/assets/stickers/food/sandwich-1.png new file mode 100644 index 0000000000..d7fd4ca4b4 Binary files /dev/null and b/assets/stickers/food/sandwich-1.png differ diff --git a/assets/stickers/food/sandwich.png b/assets/stickers/food/sandwich.png new file mode 100644 index 0000000000..ac6c5f8a43 Binary files /dev/null and b/assets/stickers/food/sandwich.png differ diff --git a/assets/stickers/food/sardine.png b/assets/stickers/food/sardine.png new file mode 100644 index 0000000000..41dcfeac6c Binary files /dev/null and b/assets/stickers/food/sardine.png differ diff --git a/assets/stickers/food/soup.png b/assets/stickers/food/soup.png new file mode 100644 index 0000000000..c40c4c172e Binary files /dev/null and b/assets/stickers/food/soup.png differ diff --git a/assets/stickers/food/soya.png b/assets/stickers/food/soya.png new file mode 100644 index 0000000000..3af760957b Binary files /dev/null and b/assets/stickers/food/soya.png differ diff --git a/assets/stickers/food/steak.png b/assets/stickers/food/steak.png new file mode 100644 index 0000000000..ed6b842dc5 Binary files /dev/null and b/assets/stickers/food/steak.png differ diff --git a/assets/stickers/food/strawberry.png b/assets/stickers/food/strawberry.png new file mode 100644 index 0000000000..e40ca7ad63 Binary files /dev/null and b/assets/stickers/food/strawberry.png differ diff --git a/assets/stickers/food/sushi.png b/assets/stickers/food/sushi.png new file mode 100644 index 0000000000..0f5cd26670 Binary files /dev/null and b/assets/stickers/food/sushi.png differ diff --git a/assets/stickers/food/taco.png b/assets/stickers/food/taco.png new file mode 100644 index 0000000000..75596a2d35 Binary files /dev/null and b/assets/stickers/food/taco.png differ diff --git a/assets/stickers/food/toaster.png b/assets/stickers/food/toaster.png new file mode 100644 index 0000000000..d0fa0d8c1b Binary files /dev/null and b/assets/stickers/food/toaster.png differ diff --git a/assets/stickers/food/tomato.png b/assets/stickers/food/tomato.png new file mode 100644 index 0000000000..db3b66cd64 Binary files /dev/null and b/assets/stickers/food/tomato.png differ diff --git a/assets/stickers/food/tuna.png b/assets/stickers/food/tuna.png new file mode 100644 index 0000000000..57c9a34c3d Binary files /dev/null and b/assets/stickers/food/tuna.png differ diff --git a/assets/stickers/food/vinegar.png b/assets/stickers/food/vinegar.png new file mode 100644 index 0000000000..ea3d718e1b Binary files /dev/null and b/assets/stickers/food/vinegar.png differ diff --git a/assets/stickers/food/watermelon.png b/assets/stickers/food/watermelon.png new file mode 100644 index 0000000000..6997e3d10a Binary files /dev/null and b/assets/stickers/food/watermelon.png differ diff --git a/assets/stickers/food/yogurt.png b/assets/stickers/food/yogurt.png new file mode 100644 index 0000000000..b3ea712573 Binary files /dev/null and b/assets/stickers/food/yogurt.png differ diff --git a/assets/stickers/weather/cloud.png b/assets/stickers/weather/cloud.png new file mode 100644 index 0000000000..ef4fb37cb8 Binary files /dev/null and b/assets/stickers/weather/cloud.png differ diff --git a/assets/stickers/weather/cloudy-1.png b/assets/stickers/weather/cloudy-1.png new file mode 100644 index 0000000000..0d3660034c Binary files /dev/null and b/assets/stickers/weather/cloudy-1.png differ diff --git a/assets/stickers/weather/cloudy-night.png b/assets/stickers/weather/cloudy-night.png new file mode 100644 index 0000000000..1bf8aea2c5 Binary files /dev/null and b/assets/stickers/weather/cloudy-night.png differ diff --git a/assets/stickers/weather/cloudy.png b/assets/stickers/weather/cloudy.png new file mode 100644 index 0000000000..11f835d669 Binary files /dev/null and b/assets/stickers/weather/cloudy.png differ diff --git a/assets/stickers/weather/eclipse.png b/assets/stickers/weather/eclipse.png new file mode 100644 index 0000000000..cf32f42ffd Binary files /dev/null and b/assets/stickers/weather/eclipse.png differ diff --git a/assets/stickers/weather/full-moon.png b/assets/stickers/weather/full-moon.png new file mode 100644 index 0000000000..0539783697 Binary files /dev/null and b/assets/stickers/weather/full-moon.png differ diff --git a/assets/stickers/weather/hail.png b/assets/stickers/weather/hail.png new file mode 100644 index 0000000000..0c816e8db9 Binary files /dev/null and b/assets/stickers/weather/hail.png differ diff --git a/assets/stickers/weather/lightning.png b/assets/stickers/weather/lightning.png new file mode 100644 index 0000000000..3ea01115f6 Binary files /dev/null and b/assets/stickers/weather/lightning.png differ diff --git a/assets/stickers/weather/moon-phases-1.png b/assets/stickers/weather/moon-phases-1.png new file mode 100644 index 0000000000..862d9530e4 Binary files /dev/null and b/assets/stickers/weather/moon-phases-1.png differ diff --git a/assets/stickers/weather/moon-phases-2.png b/assets/stickers/weather/moon-phases-2.png new file mode 100644 index 0000000000..3060dce031 Binary files /dev/null and b/assets/stickers/weather/moon-phases-2.png differ diff --git a/assets/stickers/weather/moon-phases-3.png b/assets/stickers/weather/moon-phases-3.png new file mode 100644 index 0000000000..91c9beac54 Binary files /dev/null and b/assets/stickers/weather/moon-phases-3.png differ diff --git a/assets/stickers/weather/moon-phases-4.png b/assets/stickers/weather/moon-phases-4.png new file mode 100644 index 0000000000..48b851c965 Binary files /dev/null and b/assets/stickers/weather/moon-phases-4.png differ diff --git a/assets/stickers/weather/moon-phases-5.png b/assets/stickers/weather/moon-phases-5.png new file mode 100644 index 0000000000..9e3808340c Binary files /dev/null and b/assets/stickers/weather/moon-phases-5.png differ diff --git a/assets/stickers/weather/moon-phases.png b/assets/stickers/weather/moon-phases.png new file mode 100644 index 0000000000..bc07545363 Binary files /dev/null and b/assets/stickers/weather/moon-phases.png differ diff --git a/assets/stickers/weather/planet-earth.png b/assets/stickers/weather/planet-earth.png new file mode 100644 index 0000000000..43e150a9f9 Binary files /dev/null and b/assets/stickers/weather/planet-earth.png differ diff --git a/assets/stickers/weather/rain-1.png b/assets/stickers/weather/rain-1.png new file mode 100644 index 0000000000..d0675e7eb8 Binary files /dev/null and b/assets/stickers/weather/rain-1.png differ diff --git a/assets/stickers/weather/rain.png b/assets/stickers/weather/rain.png new file mode 100644 index 0000000000..8d1e8d6771 Binary files /dev/null and b/assets/stickers/weather/rain.png differ diff --git a/assets/stickers/weather/raindrop.png b/assets/stickers/weather/raindrop.png new file mode 100644 index 0000000000..7cfdb25ea5 Binary files /dev/null and b/assets/stickers/weather/raindrop.png differ diff --git a/assets/stickers/weather/rainy-1.png b/assets/stickers/weather/rainy-1.png new file mode 100644 index 0000000000..ea3c6d9633 Binary files /dev/null and b/assets/stickers/weather/rainy-1.png differ diff --git a/assets/stickers/weather/rainy.png b/assets/stickers/weather/rainy.png new file mode 100644 index 0000000000..811543b772 Binary files /dev/null and b/assets/stickers/weather/rainy.png differ diff --git a/assets/stickers/weather/snowflake.png b/assets/stickers/weather/snowflake.png new file mode 100644 index 0000000000..5c29926032 Binary files /dev/null and b/assets/stickers/weather/snowflake.png differ diff --git a/assets/stickers/weather/storm.png b/assets/stickers/weather/storm.png new file mode 100644 index 0000000000..3ef7df8e57 Binary files /dev/null and b/assets/stickers/weather/storm.png differ diff --git a/assets/stickers/weather/sun.png b/assets/stickers/weather/sun.png new file mode 100644 index 0000000000..79112507eb Binary files /dev/null and b/assets/stickers/weather/sun.png differ diff --git a/assets/stickers/weather/temperature-1.png b/assets/stickers/weather/temperature-1.png new file mode 100644 index 0000000000..9a42e118bc Binary files /dev/null and b/assets/stickers/weather/temperature-1.png differ diff --git a/assets/stickers/weather/temperature.png b/assets/stickers/weather/temperature.png new file mode 100644 index 0000000000..3aed200474 Binary files /dev/null and b/assets/stickers/weather/temperature.png differ diff --git a/assets/stickers/weather/thermometer-1.png b/assets/stickers/weather/thermometer-1.png new file mode 100644 index 0000000000..7a61974e9d Binary files /dev/null and b/assets/stickers/weather/thermometer-1.png differ diff --git a/assets/stickers/weather/thermometer-2.png b/assets/stickers/weather/thermometer-2.png new file mode 100644 index 0000000000..b4a67d9e3e Binary files /dev/null and b/assets/stickers/weather/thermometer-2.png differ diff --git a/assets/stickers/weather/thermometer.png b/assets/stickers/weather/thermometer.png new file mode 100644 index 0000000000..2d1e7baec8 Binary files /dev/null and b/assets/stickers/weather/thermometer.png differ diff --git a/assets/stickers/weather/tornado.png b/assets/stickers/weather/tornado.png new file mode 100644 index 0000000000..fbe95eab51 Binary files /dev/null and b/assets/stickers/weather/tornado.png differ diff --git a/assets/stickers/weather/wind.png b/assets/stickers/weather/wind.png new file mode 100644 index 0000000000..450c052d9b Binary files /dev/null and b/assets/stickers/weather/wind.png differ diff --git a/build.gradle b/build.gradle index 0ccd2fc52d..9f61aff1bf 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:2.1.3' + classpath 'com.android.tools.build:gradle:2.2.3' classpath files('libs/gradle-witness.jar') } } diff --git a/res/drawable-hdpi/ic_brush_white_24dp.png b/res/drawable-hdpi/ic_brush_white_24dp.png new file mode 100644 index 0000000000..c8aa20ca2b Binary files /dev/null and b/res/drawable-hdpi/ic_brush_white_24dp.png differ diff --git a/res/drawable-hdpi/ic_check_white_36dp.png b/res/drawable-hdpi/ic_check_white_36dp.png new file mode 100644 index 0000000000..9e3f948c93 Binary files /dev/null and b/res/drawable-hdpi/ic_check_white_36dp.png differ diff --git a/res/drawable-hdpi/ic_local_dining_white_24dp.png b/res/drawable-hdpi/ic_local_dining_white_24dp.png new file mode 100644 index 0000000000..04dec6088b Binary files /dev/null and b/res/drawable-hdpi/ic_local_dining_white_24dp.png differ diff --git a/res/drawable-hdpi/ic_pets_white_24dp.png b/res/drawable-hdpi/ic_pets_white_24dp.png new file mode 100644 index 0000000000..9094bb55a5 Binary files /dev/null and b/res/drawable-hdpi/ic_pets_white_24dp.png differ diff --git a/res/drawable-hdpi/ic_replay_white_24dp.png b/res/drawable-hdpi/ic_replay_white_24dp.png new file mode 100644 index 0000000000..5ef425a170 Binary files /dev/null and b/res/drawable-hdpi/ic_replay_white_24dp.png differ diff --git a/res/drawable-hdpi/ic_tag_faces_white_24dp.png b/res/drawable-hdpi/ic_tag_faces_white_24dp.png new file mode 100644 index 0000000000..3aeae65b14 Binary files /dev/null and b/res/drawable-hdpi/ic_tag_faces_white_24dp.png differ diff --git a/res/drawable-hdpi/ic_text_fields_white_24dp.png b/res/drawable-hdpi/ic_text_fields_white_24dp.png new file mode 100644 index 0000000000..a675d51c6e Binary files /dev/null and b/res/drawable-hdpi/ic_text_fields_white_24dp.png differ diff --git a/res/drawable-hdpi/ic_wb_sunny_white_24dp.png b/res/drawable-hdpi/ic_wb_sunny_white_24dp.png new file mode 100644 index 0000000000..e0bdc4934d Binary files /dev/null and b/res/drawable-hdpi/ic_wb_sunny_white_24dp.png differ diff --git a/res/drawable-hdpi/ic_work_white_24dp.png b/res/drawable-hdpi/ic_work_white_24dp.png new file mode 100644 index 0000000000..87c5a053d1 Binary files /dev/null and b/res/drawable-hdpi/ic_work_white_24dp.png differ diff --git a/res/drawable-mdpi/ic_brush_white_24dp.png b/res/drawable-mdpi/ic_brush_white_24dp.png new file mode 100644 index 0000000000..ae4dd03dc9 Binary files /dev/null and b/res/drawable-mdpi/ic_brush_white_24dp.png differ diff --git a/res/drawable-mdpi/ic_check_white_36dp.png b/res/drawable-mdpi/ic_check_white_36dp.png new file mode 100644 index 0000000000..729f290104 Binary files /dev/null and b/res/drawable-mdpi/ic_check_white_36dp.png differ diff --git a/res/drawable-mdpi/ic_local_dining_white_24dp.png b/res/drawable-mdpi/ic_local_dining_white_24dp.png new file mode 100644 index 0000000000..5b68bb59ae Binary files /dev/null and b/res/drawable-mdpi/ic_local_dining_white_24dp.png differ diff --git a/res/drawable-mdpi/ic_pets_white_24dp.png b/res/drawable-mdpi/ic_pets_white_24dp.png new file mode 100644 index 0000000000..1194342fb5 Binary files /dev/null and b/res/drawable-mdpi/ic_pets_white_24dp.png differ diff --git a/res/drawable-mdpi/ic_replay_white_24dp.png b/res/drawable-mdpi/ic_replay_white_24dp.png new file mode 100644 index 0000000000..5a79970a98 Binary files /dev/null and b/res/drawable-mdpi/ic_replay_white_24dp.png differ diff --git a/res/drawable-mdpi/ic_tag_faces_white_24dp.png b/res/drawable-mdpi/ic_tag_faces_white_24dp.png new file mode 100644 index 0000000000..e6cc505f9f Binary files /dev/null and b/res/drawable-mdpi/ic_tag_faces_white_24dp.png differ diff --git a/res/drawable-mdpi/ic_text_fields_white_24dp.png b/res/drawable-mdpi/ic_text_fields_white_24dp.png new file mode 100644 index 0000000000..1b45ea0b41 Binary files /dev/null and b/res/drawable-mdpi/ic_text_fields_white_24dp.png differ diff --git a/res/drawable-mdpi/ic_wb_sunny_white_24dp.png b/res/drawable-mdpi/ic_wb_sunny_white_24dp.png new file mode 100644 index 0000000000..58458b22a5 Binary files /dev/null and b/res/drawable-mdpi/ic_wb_sunny_white_24dp.png differ diff --git a/res/drawable-mdpi/ic_work_white_24dp.png b/res/drawable-mdpi/ic_work_white_24dp.png new file mode 100644 index 0000000000..ba06d79a76 Binary files /dev/null and b/res/drawable-mdpi/ic_work_white_24dp.png differ diff --git a/res/drawable-xhdpi/ic_brush_white_24dp.png b/res/drawable-xhdpi/ic_brush_white_24dp.png new file mode 100644 index 0000000000..6a7239478f Binary files /dev/null and b/res/drawable-xhdpi/ic_brush_white_24dp.png differ diff --git a/res/drawable-xhdpi/ic_check_white_36dp.png b/res/drawable-xhdpi/ic_check_white_36dp.png new file mode 100644 index 0000000000..2c2ad771f7 Binary files /dev/null and b/res/drawable-xhdpi/ic_check_white_36dp.png differ diff --git a/res/drawable-xhdpi/ic_local_dining_white_24dp.png b/res/drawable-xhdpi/ic_local_dining_white_24dp.png new file mode 100644 index 0000000000..e081e1afb8 Binary files /dev/null and b/res/drawable-xhdpi/ic_local_dining_white_24dp.png differ diff --git a/res/drawable-xhdpi/ic_pets_white_24dp.png b/res/drawable-xhdpi/ic_pets_white_24dp.png new file mode 100644 index 0000000000..f28287f359 Binary files /dev/null and b/res/drawable-xhdpi/ic_pets_white_24dp.png differ diff --git a/res/drawable-xhdpi/ic_replay_white_24dp.png b/res/drawable-xhdpi/ic_replay_white_24dp.png new file mode 100644 index 0000000000..3b41913257 Binary files /dev/null and b/res/drawable-xhdpi/ic_replay_white_24dp.png differ diff --git a/res/drawable-xhdpi/ic_tag_faces_white_24dp.png b/res/drawable-xhdpi/ic_tag_faces_white_24dp.png new file mode 100644 index 0000000000..c97abc49f1 Binary files /dev/null and b/res/drawable-xhdpi/ic_tag_faces_white_24dp.png differ diff --git a/res/drawable-xhdpi/ic_text_fields_white_24dp.png b/res/drawable-xhdpi/ic_text_fields_white_24dp.png new file mode 100644 index 0000000000..612d143869 Binary files /dev/null and b/res/drawable-xhdpi/ic_text_fields_white_24dp.png differ diff --git a/res/drawable-xhdpi/ic_wb_sunny_white_24dp.png b/res/drawable-xhdpi/ic_wb_sunny_white_24dp.png new file mode 100644 index 0000000000..123f780c71 Binary files /dev/null and b/res/drawable-xhdpi/ic_wb_sunny_white_24dp.png differ diff --git a/res/drawable-xhdpi/ic_work_white_24dp.png b/res/drawable-xhdpi/ic_work_white_24dp.png new file mode 100644 index 0000000000..10ddce1027 Binary files /dev/null and b/res/drawable-xhdpi/ic_work_white_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_brush_white_24dp.png b/res/drawable-xxhdpi/ic_brush_white_24dp.png new file mode 100644 index 0000000000..300529d20e Binary files /dev/null and b/res/drawable-xxhdpi/ic_brush_white_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_check_white_36dp.png b/res/drawable-xxhdpi/ic_check_white_36dp.png new file mode 100644 index 0000000000..9b91350bc2 Binary files /dev/null and b/res/drawable-xxhdpi/ic_check_white_36dp.png differ diff --git a/res/drawable-xxhdpi/ic_local_dining_white_24dp.png b/res/drawable-xxhdpi/ic_local_dining_white_24dp.png new file mode 100644 index 0000000000..8c43c1c243 Binary files /dev/null and b/res/drawable-xxhdpi/ic_local_dining_white_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_pets_white_24dp.png b/res/drawable-xxhdpi/ic_pets_white_24dp.png new file mode 100644 index 0000000000..6996e7cad4 Binary files /dev/null and b/res/drawable-xxhdpi/ic_pets_white_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_replay_white_24dp.png b/res/drawable-xxhdpi/ic_replay_white_24dp.png new file mode 100644 index 0000000000..fcddcf02dd Binary files /dev/null and b/res/drawable-xxhdpi/ic_replay_white_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_tag_faces_white_24dp.png b/res/drawable-xxhdpi/ic_tag_faces_white_24dp.png new file mode 100644 index 0000000000..4bfd751867 Binary files /dev/null and b/res/drawable-xxhdpi/ic_tag_faces_white_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_text_fields_white_24dp.png b/res/drawable-xxhdpi/ic_text_fields_white_24dp.png new file mode 100644 index 0000000000..2d76a1da72 Binary files /dev/null and b/res/drawable-xxhdpi/ic_text_fields_white_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_wb_sunny_white_24dp.png b/res/drawable-xxhdpi/ic_wb_sunny_white_24dp.png new file mode 100644 index 0000000000..f0b22b6eff Binary files /dev/null and b/res/drawable-xxhdpi/ic_wb_sunny_white_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_work_white_24dp.png b/res/drawable-xxhdpi/ic_work_white_24dp.png new file mode 100644 index 0000000000..af82415d5f Binary files /dev/null and b/res/drawable-xxhdpi/ic_work_white_24dp.png differ diff --git a/res/drawable-xxxhdpi/ic_brush_white_24dp.png b/res/drawable-xxxhdpi/ic_brush_white_24dp.png new file mode 100644 index 0000000000..8297819ffd Binary files /dev/null and b/res/drawable-xxxhdpi/ic_brush_white_24dp.png differ diff --git a/res/drawable-xxxhdpi/ic_check_white_36dp.png b/res/drawable-xxxhdpi/ic_check_white_36dp.png new file mode 100644 index 0000000000..bfd7b82aaa Binary files /dev/null and b/res/drawable-xxxhdpi/ic_check_white_36dp.png differ diff --git a/res/drawable-xxxhdpi/ic_local_dining_white_24dp.png b/res/drawable-xxxhdpi/ic_local_dining_white_24dp.png new file mode 100644 index 0000000000..8ad36ced7d Binary files /dev/null and b/res/drawable-xxxhdpi/ic_local_dining_white_24dp.png differ diff --git a/res/drawable-xxxhdpi/ic_pets_white_24dp.png b/res/drawable-xxxhdpi/ic_pets_white_24dp.png new file mode 100644 index 0000000000..f1fe74c154 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_pets_white_24dp.png differ diff --git a/res/drawable-xxxhdpi/ic_replay_white_24dp.png b/res/drawable-xxxhdpi/ic_replay_white_24dp.png new file mode 100644 index 0000000000..1573fb111b Binary files /dev/null and b/res/drawable-xxxhdpi/ic_replay_white_24dp.png differ diff --git a/res/drawable-xxxhdpi/ic_tag_faces_white_24dp.png b/res/drawable-xxxhdpi/ic_tag_faces_white_24dp.png new file mode 100644 index 0000000000..319a13a387 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_tag_faces_white_24dp.png differ diff --git a/res/drawable-xxxhdpi/ic_text_fields_white_24dp.png b/res/drawable-xxxhdpi/ic_text_fields_white_24dp.png new file mode 100644 index 0000000000..f4e597a8e2 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_text_fields_white_24dp.png differ diff --git a/res/drawable-xxxhdpi/ic_wb_sunny_white_24dp.png b/res/drawable-xxxhdpi/ic_wb_sunny_white_24dp.png new file mode 100644 index 0000000000..97c34a0e00 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_wb_sunny_white_24dp.png differ diff --git a/res/drawable-xxxhdpi/ic_work_white_24dp.png b/res/drawable-xxxhdpi/ic_work_white_24dp.png new file mode 100644 index 0000000000..1bc11a8bbb Binary files /dev/null and b/res/drawable-xxxhdpi/ic_work_white_24dp.png differ diff --git a/res/drawable/conversation_attachment_edit.xml b/res/drawable/conversation_attachment_edit.xml new file mode 100644 index 0000000000..4e3413c1cb --- /dev/null +++ b/res/drawable/conversation_attachment_edit.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/res/drawable/vertical_separator.xml b/res/drawable/vertical_separator.xml new file mode 100644 index 0000000000..ac3f47aa34 --- /dev/null +++ b/res/drawable/vertical_separator.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/res/layout/audio_view.xml b/res/layout/audio_view.xml index dd3dff5437..37783c58c4 100644 --- a/res/layout/audio_view.xml +++ b/res/layout/audio_view.xml @@ -1,7 +1,8 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + tools:context="org.thoughtcrime.securesms.components.AudioView"> - - + diff --git a/res/layout/media_view_edit_button.xml b/res/layout/media_view_edit_button.xml new file mode 100644 index 0000000000..a850254421 --- /dev/null +++ b/res/layout/media_view_edit_button.xml @@ -0,0 +1,8 @@ + + diff --git a/res/layout/scribble_activity.xml b/res/layout/scribble_activity.xml new file mode 100644 index 0000000000..cd2b89c6a1 --- /dev/null +++ b/res/layout/scribble_activity.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + diff --git a/res/layout/scribble_select_sticker_activity.xml b/res/layout/scribble_select_sticker_activity.xml new file mode 100644 index 0000000000..d05cc2b602 --- /dev/null +++ b/res/layout/scribble_select_sticker_activity.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/res/layout/scribble_select_sticker_fragment.xml b/res/layout/scribble_select_sticker_fragment.xml new file mode 100644 index 0000000000..805a3bc80d --- /dev/null +++ b/res/layout/scribble_select_sticker_fragment.xml @@ -0,0 +1,6 @@ + + diff --git a/res/layout/scribble_sticker_item.xml b/res/layout/scribble_sticker_item.xml new file mode 100644 index 0000000000..a335969595 --- /dev/null +++ b/res/layout/scribble_sticker_item.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/res/layout/scribble_toolbar.xml b/res/layout/scribble_toolbar.xml new file mode 100644 index 0000000000..4d460d0d80 --- /dev/null +++ b/res/layout/scribble_toolbar.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/scribble_view.xml b/res/layout/scribble_view.xml new file mode 100644 index 0000000000..5dbd35fbfa --- /dev/null +++ b/res/layout/scribble_view.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/values/arrays.xml b/res/values/arrays.xml index e7279e9aab..dfa47c9b9d 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -253,4 +253,12 @@ 604800 + + #000000 + #ff0000 + #ffff00 + #00ffff + #ff00ff + + diff --git a/res/values/attrs.xml b/res/values/attrs.xml index b88153e552..ebbce1eaae 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -154,6 +154,7 @@ + @@ -183,4 +184,10 @@ + + + + + + diff --git a/res/values/colors.xml b/res/values/colors.xml index 310ad2f07d..bd380c8220 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -52,4 +52,7 @@ #ff222222 #400099cc #40ffffff + + #8cf437 + #00FFFFFF diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 100d0c4a75..72b4318cdd 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -25,6 +25,7 @@ 210dp 3dp 24dp + 24dp 3 10dp @@ -63,4 +64,6 @@ 140dp 34sp 20sp + + 3dp diff --git a/res/values/material_colors.xml b/res/values/material_colors.xml index 2815510985..9f02548771 100644 --- a/res/values/material_colors.xml +++ b/res/values/material_colors.xml @@ -273,4 +273,6 @@ #424242 #212121 + #44BDBDBD + \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index f013c9a927..953f8a6b99 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -119,6 +119,7 @@ import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients.RecipientsModifiedListener; +import org.thoughtcrime.securesms.scribbles.ScribbleActivity; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage; @@ -375,6 +376,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity case PICK_GIF: setMedia(data.getData(), MediaType.GIF); break; + case ScribbleActivity.SCRIBBLE_REQUEST_CODE: + setMedia(data.getData(), MediaType.IMAGE); + break; } } diff --git a/src/org/thoughtcrime/securesms/components/AudioView.java b/src/org/thoughtcrime/securesms/components/AudioView.java index ee9c024e7d..9616f045f1 100644 --- a/src/org/thoughtcrime/securesms/components/AudioView.java +++ b/src/org/thoughtcrime/securesms/components/AudioView.java @@ -13,6 +13,7 @@ import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.view.View; +import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.SeekBar; @@ -39,6 +40,7 @@ public class AudioView extends FrameLayout implements AudioSlidePlayer.Listener private static final String TAG = AudioView.class.getSimpleName(); private final @NonNull AnimatingToggle controlToggle; + private final @NonNull ViewGroup container; private final @NonNull ImageView playButton; private final @NonNull ImageView pauseButton; private final @NonNull ImageView downloadButton; @@ -62,6 +64,7 @@ public class AudioView extends FrameLayout implements AudioSlidePlayer.Listener super(context, attrs, defStyleAttr); inflate(context, R.layout.audio_view, this); + this.container = (ViewGroup) findViewById(R.id.audio_widget_container); this.controlToggle = (AnimatingToggle) findViewById(R.id.control_toggle); this.playButton = (ImageView) findViewById(R.id.play); this.pauseButton = (ImageView) findViewById(R.id.pause); @@ -85,6 +88,7 @@ public class AudioView extends FrameLayout implements AudioSlidePlayer.Listener TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AudioView, 0, 0); setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE), typedArray.getColor(R.styleable.AudioView_backgroundTintColor, Color.WHITE)); + container.setBackgroundColor(typedArray.getColor(R.styleable.AudioView_widgetBackground, Color.TRANSPARENT)); typedArray.recycle(); } } diff --git a/src/org/thoughtcrime/securesms/components/RemovableMediaView.java b/src/org/thoughtcrime/securesms/components/RemovableEditableMediaView.java similarity index 57% rename from src/org/thoughtcrime/securesms/components/RemovableMediaView.java rename to src/org/thoughtcrime/securesms/components/RemovableEditableMediaView.java index 44ef5ef067..edf3022360 100644 --- a/src/org/thoughtcrime/securesms/components/RemovableMediaView.java +++ b/src/org/thoughtcrime/securesms/components/RemovableEditableMediaView.java @@ -11,49 +11,59 @@ import android.widget.ImageView; import org.thoughtcrime.securesms.R; -public class RemovableMediaView extends FrameLayout { +public class RemovableEditableMediaView extends FrameLayout { private final @NonNull ImageView remove; + private final @NonNull ImageView edit; + private final int removeSize; + private final int editSize; private @Nullable View current; - public RemovableMediaView(Context context) { + public RemovableEditableMediaView(Context context) { this(context, null); } - public RemovableMediaView(Context context, AttributeSet attrs) { + public RemovableEditableMediaView(Context context, AttributeSet attrs) { this(context, attrs, 0); } - public RemovableMediaView(Context context, AttributeSet attrs, int defStyleAttr) { + public RemovableEditableMediaView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.remove = (ImageView)LayoutInflater.from(context).inflate(R.layout.media_view_remove_button, this, false); + this.edit = (ImageView)LayoutInflater.from(context).inflate(R.layout.media_view_edit_button, this, false); + this.removeSize = getResources().getDimensionPixelSize(R.dimen.media_bubble_remove_button_size); + this.editSize = getResources().getDimensionPixelSize(R.dimen.media_bubble_edit_button_size); this.remove.setVisibility(View.GONE); + this.edit.setVisibility(View.GONE); } @Override public void onFinishInflate() { super.onFinishInflate(); this.addView(remove); + this.addView(edit); } - public void display(@Nullable View view) { + public void display(@Nullable View view, boolean editable) { + edit.setVisibility(editable ? View.VISIBLE : View.GONE); + if (view == current) return; if (current != null) current.setVisibility(View.GONE); if (view != null) { - MarginLayoutParams params = (MarginLayoutParams)view.getLayoutParams(); - params.setMargins(0, removeSize / 2, removeSize / 2, 0); - view.setLayoutParams(params); + view.setPadding(0, removeSize / 2, removeSize / 2, 0); + edit.setPadding(0, 0, removeSize / 2, 0); view.setVisibility(View.VISIBLE); remove.setVisibility(View.VISIBLE); } else { remove.setVisibility(View.GONE); + edit.setVisibility(View.GONE); } current = view; @@ -62,4 +72,8 @@ public class RemovableMediaView extends FrameLayout { public void setRemoveClickListener(View.OnClickListener listener) { this.remove.setOnClickListener(listener); } + + public void setEditClickListener(View.OnClickListener listener) { + this.edit.setOnClickListener(listener); + } } diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index e6e9f9b19d..a26e782ded 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -40,13 +40,14 @@ import com.google.android.gms.location.places.ui.PlacePicker; import org.thoughtcrime.securesms.MediaPreviewActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.AudioView; -import org.thoughtcrime.securesms.components.RemovableMediaView; +import org.thoughtcrime.securesms.components.RemovableEditableMediaView; import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.location.SignalMapView; import org.thoughtcrime.securesms.components.location.SignalPlace; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.providers.PersistentBlobProvider; +import org.thoughtcrime.securesms.scribbles.ScribbleActivity; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.ViewUtil; @@ -67,13 +68,13 @@ public class AttachmentManager { private final static String TAG = AttachmentManager.class.getSimpleName(); - private final @NonNull Context context; - private final @NonNull View attachmentView; - private final @NonNull RemovableMediaView removableMediaView; - private final @NonNull ThumbnailView thumbnail; - private final @NonNull AudioView audioView; - private final @NonNull SignalMapView mapView; - private final @NonNull AttachmentListener attachmentListener; + private final @NonNull Context context; + private final @NonNull View attachmentView; + private final @NonNull RemovableEditableMediaView removableMediaView; + private final @NonNull ThumbnailView thumbnail; + private final @NonNull AudioView audioView; + private final @NonNull SignalMapView mapView; + private final @NonNull AttachmentListener attachmentListener; private @NonNull List garbage = new LinkedList<>(); private @NonNull Optional slide = Optional.absent(); @@ -89,6 +90,7 @@ public class AttachmentManager { this.attachmentListener = listener; removableMediaView.setRemoveClickListener(new RemoveButtonListener()); + removableMediaView.setEditClickListener(new EditButtonListener()); thumbnail.setOnClickListener(new ThumbnailClickListener()); } @@ -154,7 +156,7 @@ public class AttachmentManager { ListenableFuture future = mapView.display(place); attachmentView.setVisibility(View.VISIBLE); - removableMediaView.display(mapView); + removableMediaView.display(mapView, false); future.addListener(new AssertedSuccessListener() { @Override @@ -215,10 +217,10 @@ public class AttachmentManager { if (slide.hasAudio()) { audioView.setAudio(masterSecret, (AudioSlide)slide, false); - removableMediaView.display(audioView); + removableMediaView.display(audioView, false); } else { thumbnail.setImageResource(masterSecret, slide, false); - removableMediaView.display(thumbnail); + removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE); } attachmentListener.onAttachmentChanged(); @@ -350,6 +352,15 @@ public class AttachmentManager { } } + private class EditButtonListener implements View.OnClickListener { + @Override + public void onClick(View v) { + Intent intent = new Intent(context, ScribbleActivity.class); + intent.setData(getSlideUri()); + ((Activity)context).startActivityForResult(intent, ScribbleActivity.SCRIBBLE_REQUEST_CODE); + } + } + public interface AttachmentListener { void onAttachmentChanged(); } diff --git a/src/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java b/src/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java new file mode 100644 index 0000000000..ecc4ef5bf2 --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java @@ -0,0 +1,278 @@ +package org.thoughtcrime.securesms.scribbles; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.PointF; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; +import android.view.View; + +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.providers.PersistentBlobProvider; +import org.thoughtcrime.securesms.scribbles.viewmodel.Font; +import org.thoughtcrime.securesms.scribbles.viewmodel.Layer; +import org.thoughtcrime.securesms.scribbles.viewmodel.TextLayer; +import org.thoughtcrime.securesms.scribbles.widget.MotionView; +import org.thoughtcrime.securesms.scribbles.widget.ScribbleView; +import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker; +import org.thoughtcrime.securesms.scribbles.widget.entity.ImageEntity; +import org.thoughtcrime.securesms.scribbles.widget.entity.MotionEntity; +import org.thoughtcrime.securesms.scribbles.widget.entity.TextEntity; +import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +import ws.com.google.android.mms.ContentType; + +@TargetApi(Build.VERSION_CODES.JELLY_BEAN) +public class ScribbleActivity extends PassphraseRequiredActionBarActivity implements ScribbleToolbar.ScribbleToolbarListener, VerticalSlideColorPicker.OnColorChangeListener { + + private static final String TAG = ScribbleActivity.class.getName(); + + public static final int SELECT_STICKER_REQUEST_CODE = 123; + public static final int SCRIBBLE_REQUEST_CODE = 31424; + + private VerticalSlideColorPicker colorPicker; + private ScribbleToolbar toolbar; + private ScribbleView scribbleView; + private MasterSecret masterSecret; + + @Override + protected void onCreate(Bundle savedInstanceState, @NonNull MasterSecret masterSecret) { + setContentView(R.layout.scribble_activity); + + this.masterSecret = masterSecret; + this.scribbleView = (ScribbleView) findViewById(R.id.scribble_view); + this.toolbar = (ScribbleToolbar) findViewById(R.id.toolbar); + this.colorPicker = (VerticalSlideColorPicker) findViewById(R.id.scribble_color_picker); + + this.toolbar.setListener(this); + this.toolbar.setToolColor(Color.RED); + + scribbleView.setMotionViewCallback(motionViewCallback); + scribbleView.setDrawingMode(false); + scribbleView.setImage(getIntent().getData(), masterSecret); + + colorPicker.setOnColorChangeListener(this); + colorPicker.setVisibility(View.GONE); + + setSupportActionBar(toolbar); + + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + getSupportActionBar().setTitle(null); + } + + private void addSticker(final Bitmap pica) { + scribbleView.post(new Runnable() { + @Override + public void run() { + Layer layer = new Layer(); + ImageEntity entity = new ImageEntity(layer, pica, scribbleView.getWidth(), scribbleView.getHeight()); + + scribbleView.addEntityAndPosition(entity); + } + }); + } + + private void changeTextEntityColor(int selectedColor) { + TextEntity textEntity = currentTextEntity(); + + if (textEntity == null) { + return; + } + + textEntity.getLayer().getFont().setColor(selectedColor); + textEntity.updateEntity(); + scribbleView.invalidate(); + } + + private void startTextEntityEditing() { + TextEntity textEntity = currentTextEntity(); + if (textEntity != null) { + scribbleView.startEditing(textEntity); + } + } + + @Nullable + private TextEntity currentTextEntity() { + if (scribbleView != null && scribbleView.getSelectedEntity() instanceof TextEntity) { + return ((TextEntity) scribbleView.getSelectedEntity()); + } else { + return null; + } + } + + protected void addTextSticker() { + TextLayer textLayer = createTextLayer(); + TextEntity textEntity = new TextEntity(textLayer, scribbleView.getWidth(), scribbleView.getHeight()); + scribbleView.addEntityAndPosition(textEntity); + + // move text sticker up so that its not hidden under keyboard + PointF center = textEntity.absoluteCenter(); + center.y = center.y * 0.5F; + textEntity.moveCenterTo(center); + + // redraw + scribbleView.invalidate(); + + startTextEntityEditing(); + changeTextEntityColor(toolbar.getToolColor()); + } + + private TextLayer createTextLayer() { + TextLayer textLayer = new TextLayer(); + Font font = new Font(); + + font.setColor(TextLayer.Limits.INITIAL_FONT_COLOR); + font.setSize(TextLayer.Limits.INITIAL_FONT_SIZE); + + textLayer.setFont(font); + + return textLayer; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == SELECT_STICKER_REQUEST_CODE) { + if (data != null) { + toolbar.setStickerSelected(true); + final String stickerFile = data.getStringExtra(StickerSelectActivity.EXTRA_STICKER_FILE); + + new AsyncTask() { + @Override + protected @Nullable + Bitmap doInBackground(Void... params) { + try { + return BitmapFactory.decodeStream(getAssets().open(stickerFile)); + } catch (IOException e) { + Log.w(TAG, e); + return null; + } + } + + @Override + protected void onPostExecute(@Nullable Bitmap bitmap) { + addSticker(bitmap); + } + }.execute(); + } + } + } + } + + @Override + public void onBrushSelected(boolean enabled) { + scribbleView.setDrawingMode(enabled); + colorPicker.setVisibility(enabled ? View.VISIBLE : View.GONE); + } + + @Override + public void onPaintUndo() { + scribbleView.undoDrawing(); + } + + @Override + public void onTextSelected(boolean enabled) { + if (enabled) { + addTextSticker(); + scribbleView.setDrawingMode(false); + colorPicker.setVisibility(View.VISIBLE); + } else { + scribbleView.clearSelection(); + colorPicker.setVisibility(View.GONE); + } + } + + @Override + public void onStickerSelected(boolean enabled) { + colorPicker.setVisibility(View.GONE); + + if (!enabled) { + scribbleView.clearSelection(); + } else { + scribbleView.setDrawingMode(false); + Intent intent = new Intent(this, StickerSelectActivity.class); + startActivityForResult(intent, SELECT_STICKER_REQUEST_CODE); + } + } + + public void onDeleteSelected() { + scribbleView.deleteSelected(); + colorPicker.setVisibility(View.GONE); + } + + @Override + public void onSave() { + ListenableFuture future = scribbleView.getRenderedImage(); + + future.addListener(new ListenableFuture.Listener() { + @Override + public void onSuccess(Bitmap result) { + PersistentBlobProvider provider = PersistentBlobProvider.getInstance(ScribbleActivity.this); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + result.compress(Bitmap.CompressFormat.JPEG, 80, baos); + + byte[] data = baos.toByteArray(); + baos = null; + result = null; + + Uri uri = provider.create(masterSecret, data, ContentType.IMAGE_JPEG); + Intent intent = new Intent(); + intent.setData(uri); + setResult(RESULT_OK, intent); + + finish(); + } + + @Override + public void onFailure(ExecutionException e) { + Log.w(TAG, e); + } + }); + } + + private final MotionView.MotionViewCallback motionViewCallback = new MotionView.MotionViewCallback() { + @Override + public void onEntitySelected(@Nullable MotionEntity entity) { + if (entity == null) { + toolbar.setNoneSelected(); + colorPicker.setVisibility(View.GONE); + } else if (entity instanceof TextEntity) { + toolbar.setTextSelected(true); + colorPicker.setVisibility(View.VISIBLE); + } else { + toolbar.setStickerSelected(true); + colorPicker.setVisibility(View.GONE); + } + } + + @Override + public void onEntityDoubleTap(@NonNull MotionEntity entity) { + startTextEntityEditing(); + } + }; + + @Override + public void onColorChange(int color) { + if (color == 0) color = Color.RED; + + toolbar.setToolColor(color); + scribbleView.setDrawingBrushColor(color); + + changeTextEntityColor(color); + } +} diff --git a/src/org/thoughtcrime/securesms/scribbles/ScribbleToolbar.java b/src/org/thoughtcrime/securesms/scribbles/ScribbleToolbar.java new file mode 100644 index 0000000000..75718b0acb --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/ScribbleToolbar.java @@ -0,0 +1,240 @@ +/** + * Copyright (C) 2016 Open Whisper Systems + * + * 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 org.thoughtcrime.securesms.scribbles; + +import android.animation.LayoutTransition; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.v7.widget.Toolbar; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import org.thoughtcrime.securesms.R; + +@TargetApi(Build.VERSION_CODES.JELLY_BEAN) +public class ScribbleToolbar extends Toolbar implements View.OnClickListener { + + private enum Selected { + NONE, + STICKER, + TEXT, + BRUSH + } + + private int foregroundSelectedTint; + private int foregroundUnselectedTint; + + private LinearLayout toolsView; + + private ImageView saveView; + private ImageView brushView; + private ImageView textView; + private ImageView stickerView; + + private ImageView separatorView; + + private ImageView undoView; + private ImageView deleteView; + + private Drawable background; + + @Nullable + private ScribbleToolbarListener listener; + + private int toolColor = Color.RED; + private Selected selected = Selected.NONE; + + public ScribbleToolbar(Context context) { + super(context); + init(context); + } + + public ScribbleToolbar(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public ScribbleToolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + inflate(context, R.layout.scribble_toolbar, this); + + this.toolsView = (LinearLayout) findViewById(R.id.tools_view); + this.brushView = (ImageView) findViewById(R.id.brush_button); + this.textView = (ImageView) findViewById(R.id.text_button); + this.stickerView = (ImageView) findViewById(R.id.sticker_button); + this.separatorView = (ImageView) findViewById(R.id.separator); + this.saveView = (ImageView) findViewById(R.id.save); + + this.undoView = (ImageView) findViewById(R.id.undo); + this.deleteView = (ImageView) findViewById(R.id.delete); + + this.background = getResources().getDrawable(R.drawable.circle_tintable); + this.foregroundSelectedTint = getResources().getColor(R.color.white); + this.foregroundUnselectedTint = getResources().getColor(R.color.grey_800); + + this.undoView.setOnClickListener(this); + this.brushView.setOnClickListener(this); + this.textView.setOnClickListener(this); + this.stickerView.setOnClickListener(this); + this.separatorView.setOnClickListener(this); + this.deleteView.setOnClickListener(this); + this.saveView.setOnClickListener(this); + } + + public void setListener(@Nullable ScribbleToolbarListener listener) { + this.listener = listener; + } + + public void setToolColor(int toolColor) { + this.toolColor = toolColor; + this.background.setColorFilter(new PorterDuffColorFilter(toolColor, PorterDuff.Mode.MULTIPLY)); + } + + public int getToolColor() { + return this.toolColor; + } + + @Override + public void onClick(View v) { + this.toolsView.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING); + + if (v == this.brushView) { + boolean enabled = selected != Selected.BRUSH; + setBrushSelected(enabled); + if (listener != null) listener.onBrushSelected(enabled); + } else if (v == this.stickerView) { + setNoneSelected(); + if (listener != null) listener.onStickerSelected(true); + } else if (v == this.textView) { + boolean enabled = selected != Selected.TEXT; + setTextSelected(enabled); + if (listener != null) listener.onTextSelected(enabled); + } else if (v == this.deleteView) { + setNoneSelected(); + if (listener != null) listener.onDeleteSelected(); + } else if (v == this.undoView) { + if (listener != null) listener.onPaintUndo(); + } else if (v == this.saveView) { + if (listener != null) listener.onSave(); + } + } + + private void setBrushSelected(boolean enabled) { + if (enabled) { + + this.textView.setBackground(null); + this.textView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY)); + + this.brushView.setBackground(background); + this.brushView.setColorFilter(new PorterDuffColorFilter(foregroundSelectedTint, PorterDuff.Mode.MULTIPLY)); + + this.stickerView.setBackground(null); + this.stickerView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY)); + + this.separatorView.setVisibility(View.VISIBLE); + this.undoView.setVisibility(View.VISIBLE); + this.deleteView.setVisibility(View.GONE); + + this.selected = Selected.BRUSH; + } else { + this.brushView.setBackground(null); + this.brushView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY)); + this.separatorView.setVisibility(View.GONE); + this.undoView.setVisibility(View.GONE); + + this.selected = Selected.NONE; + } + } + + public void setTextSelected(boolean enabled) { + if (enabled) { + this.brushView.setBackground(null); + this.brushView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY)); + + this.textView.setBackground(background); + this.textView.setColorFilter(new PorterDuffColorFilter(foregroundSelectedTint, PorterDuff.Mode.MULTIPLY)); + + this.stickerView.setBackground(null); + this.stickerView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY)); + + this.separatorView.setVisibility(View.VISIBLE); + this.undoView.setVisibility(View.GONE); + this.deleteView.setVisibility(View.VISIBLE); + + this.selected = Selected.TEXT; + } else { + this.textView.setBackground(null); + this.textView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY)); + + this.separatorView.setVisibility(View.GONE); + this.deleteView.setVisibility(View.GONE); + + this.selected = Selected.NONE; + } + } + + public void setStickerSelected(boolean enabled) { + if (enabled) { + this.brushView.setBackground(null); + this.brushView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY)); + + this.textView.setBackground(null); + this.textView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY)); + + this.separatorView.setVisibility(View.VISIBLE); + this.undoView.setVisibility(View.GONE); + this.deleteView.setVisibility(View.VISIBLE); + + this.selected = Selected.STICKER; + } else { + this.separatorView.setVisibility(View.GONE); + this.deleteView.setVisibility(View.GONE); + + this.selected = Selected.NONE; + } + } + + public void setNoneSelected() { + setBrushSelected(false); + setStickerSelected(false); + setTextSelected(false); + + this.selected = Selected.NONE; + } + + public interface ScribbleToolbarListener { + public void onBrushSelected(boolean enabled); + public void onPaintUndo(); + public void onTextSelected(boolean enabled); + public void onStickerSelected(boolean enabled); + public void onDeleteSelected(); + public void onSave(); + } +} diff --git a/src/org/thoughtcrime/securesms/scribbles/StickerLoader.java b/src/org/thoughtcrime/securesms/scribbles/StickerLoader.java new file mode 100644 index 0000000000..213e618389 --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/StickerLoader.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2016 Open Whisper Systems + * + * 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 org.thoughtcrime.securesms.scribbles; + + +import android.content.Context; +import android.support.annotation.NonNull; +import android.util.Log; + +import org.thoughtcrime.securesms.util.AsyncLoader; + +import java.io.IOException; + +class StickerLoader extends AsyncLoader { + + private static final String TAG = StickerLoader.class.getName(); + + private final String assetDirectory; + + StickerLoader(Context context, String assetDirectory) { + super(context); + this.assetDirectory = assetDirectory; + } + + @Override + public @NonNull + String[] loadInBackground() { + try { + String[] files = getContext().getAssets().list(assetDirectory); + + for (int i=0;i. + */ +package org.thoughtcrime.securesms.scribbles; + +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.ViewPager; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ImageSpan; +import android.view.MenuItem; + +import org.thoughtcrime.securesms.BaseActionBarActivity; +import org.thoughtcrime.securesms.R; + +public class StickerSelectActivity extends FragmentActivity implements StickerSelectFragment.StickerSelectionListener { + + private static final String TAG = StickerSelectActivity.class.getName(); + + public static final String EXTRA_STICKER_FILE = "extra_sticker_file"; + + private static final int[] TAB_TITLES = new int[] { + R.drawable.ic_tag_faces_white_24dp, + R.drawable.ic_work_white_24dp, + R.drawable.ic_pets_white_24dp, + R.drawable.ic_local_dining_white_24dp, + R.drawable.ic_wb_sunny_white_24dp + }; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.scribble_select_sticker_activity); + + ViewPager viewPager = (ViewPager) findViewById(R.id.pager); + viewPager.setAdapter(new StickerPagerAdapter(getSupportFragmentManager(), this)); + + TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); + tabLayout.setupWithViewPager(viewPager); + + for (int i=0;i. + */ +package org.thoughtcrime.securesms.scribbles; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; + +import org.thoughtcrime.securesms.R; + +public class StickerSelectFragment extends Fragment implements LoaderManager.LoaderCallbacks { + + private RecyclerView recyclerView; + private String assetDirectory; + private StickerSelectionListener listener; + + public static StickerSelectFragment newInstance(String assetDirectory) { + StickerSelectFragment fragment = new StickerSelectFragment(); + + Bundle args = new Bundle(); + args.putString("assetDirectory", assetDirectory); + fragment.setArguments(args); + + return fragment; + } + + @Nullable + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.scribble_select_sticker_fragment, container, false); + this.recyclerView = (RecyclerView)view.findViewById(R.id.stickers_recycler_view); + + return view; + } + + @Override + public void onActivityCreated(Bundle bundle) { + super.onActivityCreated(bundle); + + this.assetDirectory = getArguments().getString("assetDirectory"); + + getLoaderManager().initLoader(0, null, this); + this.recyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 3)); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new StickerLoader(getActivity(), assetDirectory); + } + + @Override + public void onLoadFinished(Loader loader, String[] data) { + recyclerView.setAdapter(new StickersAdapter(getActivity(), data)); + } + + @Override + public void onLoaderReset(Loader loader) { + recyclerView.setAdapter(null); + } + + public void setListener(StickerSelectionListener listener) { + this.listener = listener; + } + + class StickersAdapter extends RecyclerView.Adapter { + + private final Context context; + private final String[] stickerFiles; + private final LayoutInflater layoutInflater; + + StickersAdapter(@NonNull Context context, @NonNull String[] stickerFiles) { + this.context = context; + this.stickerFiles = stickerFiles; + this.layoutInflater = LayoutInflater.from(context); + } + + @Override + public StickerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new StickerViewHolder(layoutInflater.inflate(R.layout.scribble_sticker_item, parent, false)); + } + + @Override + public void onBindViewHolder(StickerViewHolder holder, int position) { + holder.fileName = stickerFiles[position]; + + Glide.with(context) + .load(Uri.parse("file:///android_asset/" + holder.fileName)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(holder.image); + } + + @Override + public int getItemCount() { + return stickerFiles.length; + } + + @Override + public void onViewRecycled(StickerViewHolder holder) { + super.onViewRecycled(holder); + Glide.clear(holder.image); + } + + private void onStickerSelected(String fileName) { + if (listener != null) listener.onStickerSelected(fileName); + } + + class StickerViewHolder extends RecyclerView.ViewHolder { + + private String fileName; + private ImageView image; + + StickerViewHolder(View itemView) { + super(itemView); + image = (ImageView) itemView.findViewById(R.id.sticker_image); + itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + int pos = getAdapterPosition(); + if (pos >= 0) { + onStickerSelected(fileName); + } + } + }); + } + } + } + + public interface StickerSelectionListener { + public void onStickerSelected(String name); + } + + +} diff --git a/src/org/thoughtcrime/securesms/scribbles/multitouch/BaseGestureDetector.java b/src/org/thoughtcrime/securesms/scribbles/multitouch/BaseGestureDetector.java new file mode 100644 index 0000000000..0d3930b001 --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/multitouch/BaseGestureDetector.java @@ -0,0 +1,149 @@ +package org.thoughtcrime.securesms.scribbles.multitouch; + +import android.content.Context; +import android.view.MotionEvent; + +/** + * @author Almer Thie (code.almeros.com) + * Copyright (c) 2013, Almer Thie (code.almeros.com) + *

+ * All rights reserved. + *

+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + *

+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the distribution. + *

+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ +public abstract class BaseGestureDetector { + /** + * This value is the threshold ratio between the previous combined pressure + * and the current combined pressure. When pressure decreases rapidly + * between events the position values can often be imprecise, as it usually + * indicates that the user is in the process of lifting a pointer off of the + * device. This value was tuned experimentally. + */ + protected static final float PRESSURE_THRESHOLD = 0.67f; + protected final Context mContext; + protected boolean mGestureInProgress; + protected MotionEvent mPrevEvent; + protected MotionEvent mCurrEvent; + protected float mCurrPressure; + protected float mPrevPressure; + protected long mTimeDelta; + + + public BaseGestureDetector(Context context) { + mContext = context; + } + + /** + * All gesture detectors need to be called through this method to be able to + * detect gestures. This method delegates work to handler methods + * (handleStartProgressEvent, handleInProgressEvent) implemented in + * extending classes. + * + * @param event + * @return + */ + public boolean onTouchEvent(MotionEvent event) { + final int actionCode = event.getAction() & MotionEvent.ACTION_MASK; + if (!mGestureInProgress) { + handleStartProgressEvent(actionCode, event); + } else { + handleInProgressEvent(actionCode, event); + } + return true; + } + + /** + * Called when the current event occurred when NO gesture is in progress + * yet. The handling in this implementation may set the gesture in progress + * (via mGestureInProgress) or out of progress + * + * @param actionCode + * @param event + */ + protected abstract void handleStartProgressEvent(int actionCode, MotionEvent event); + + /** + * Called when the current event occurred when a gesture IS in progress. The + * handling in this implementation may set the gesture out of progress (via + * mGestureInProgress). + * + * @param actionCode + * @param event + */ + protected abstract void handleInProgressEvent(int actionCode, MotionEvent event); + + + protected void updateStateByEvent(MotionEvent curr) { + final MotionEvent prev = mPrevEvent; + + // Reset mCurrEvent + if (mCurrEvent != null) { + mCurrEvent.recycle(); + mCurrEvent = null; + } + mCurrEvent = MotionEvent.obtain(curr); + + + // Delta time + mTimeDelta = curr.getEventTime() - prev.getEventTime(); + + // Pressure + mCurrPressure = curr.getPressure(curr.getActionIndex()); + mPrevPressure = prev.getPressure(prev.getActionIndex()); + } + + protected void resetState() { + if (mPrevEvent != null) { + mPrevEvent.recycle(); + mPrevEvent = null; + } + if (mCurrEvent != null) { + mCurrEvent.recycle(); + mCurrEvent = null; + } + mGestureInProgress = false; + } + + + /** + * Returns {@code true} if a gesture is currently in progress. + * + * @return {@code true} if a gesture is currently in progress, {@code false} otherwise. + */ + public boolean isInProgress() { + return mGestureInProgress; + } + + /** + * Return the time difference in milliseconds between the previous accepted + * GestureDetector event and the current GestureDetector event. + * + * @return Time difference since the last move event in milliseconds. + */ + public long getTimeDelta() { + return mTimeDelta; + } + + /** + * Return the event time of the current GestureDetector event being + * processed. + * + * @return Current GestureDetector event time in milliseconds. + */ + public long getEventTime() { + return mCurrEvent.getEventTime(); + } + +} diff --git a/src/org/thoughtcrime/securesms/scribbles/multitouch/MoveGestureDetector.java b/src/org/thoughtcrime/securesms/scribbles/multitouch/MoveGestureDetector.java new file mode 100644 index 0000000000..f623a202a6 --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/multitouch/MoveGestureDetector.java @@ -0,0 +1,170 @@ +package org.thoughtcrime.securesms.scribbles.multitouch; + +import android.content.Context; +import android.graphics.PointF; +import android.view.MotionEvent; + +/** + * @author Almer Thie (code.almeros.com) + * Copyright (c) 2013, Almer Thie (code.almeros.com) + *

+ * All rights reserved. + *

+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + *

+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the distribution. + *

+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ +public class MoveGestureDetector extends BaseGestureDetector { + + private static final PointF FOCUS_DELTA_ZERO = new PointF(); + private final OnMoveGestureListener mListener; + private PointF mCurrFocusInternal; + private PointF mPrevFocusInternal; + private PointF mFocusExternal = new PointF(); + private PointF mFocusDeltaExternal = new PointF(); + public MoveGestureDetector(Context context, OnMoveGestureListener listener) { + super(context); + mListener = listener; + } + + @Override + protected void handleStartProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_DOWN: + resetState(); // In case we missed an UP/CANCEL event + + mPrevEvent = MotionEvent.obtain(event); + mTimeDelta = 0; + + updateStateByEvent(event); + break; + + case MotionEvent.ACTION_MOVE: + mGestureInProgress = mListener.onMoveBegin(this); + break; + } + } + + @Override + protected void handleInProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mListener.onMoveEnd(this); + resetState(); + break; + + case MotionEvent.ACTION_MOVE: + updateStateByEvent(event); + + // Only accept the event if our relative pressure is within + // a certain limit. This can help filter shaky data as a + // finger is lifted. + if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) { + final boolean updatePrevious = mListener.onMove(this); + if (updatePrevious) { + mPrevEvent.recycle(); + mPrevEvent = MotionEvent.obtain(event); + } + } + break; + } + } + + protected void updateStateByEvent(MotionEvent curr) { + super.updateStateByEvent(curr); + + final MotionEvent prev = mPrevEvent; + + // Focus intenal + mCurrFocusInternal = determineFocalPoint(curr); + mPrevFocusInternal = determineFocalPoint(prev); + + // Focus external + // - Prevent skipping of focus delta when a finger is added or removed + boolean mSkipNextMoveEvent = prev.getPointerCount() != curr.getPointerCount(); + mFocusDeltaExternal = mSkipNextMoveEvent ? FOCUS_DELTA_ZERO : new PointF(mCurrFocusInternal.x - mPrevFocusInternal.x, mCurrFocusInternal.y - mPrevFocusInternal.y); + + // - Don't directly use mFocusInternal (or skipping will occur). Add + // unskipped delta values to mFocusExternal instead. + mFocusExternal.x += mFocusDeltaExternal.x; + mFocusExternal.y += mFocusDeltaExternal.y; + } + + /** + * Determine (multi)finger focal point (a.k.a. center point between all + * fingers) + * + * @return PointF focal point + */ + private PointF determineFocalPoint(MotionEvent e) { + // Number of fingers on screen + final int pCount = e.getPointerCount(); + float x = 0f; + float y = 0f; + + for (int i = 0; i < pCount; i++) { + x += e.getX(i); + y += e.getY(i); + } + + return new PointF(x / pCount, y / pCount); + } + + public float getFocusX() { + return mFocusExternal.x; + } + + public float getFocusY() { + return mFocusExternal.y; + } + + public PointF getFocusDelta() { + return mFocusDeltaExternal; + } + + /** + * Listener which must be implemented which is used by MoveGestureDetector + * to perform callbacks to any implementing class which is registered to a + * MoveGestureDetector via the constructor. + * + * @see MoveGestureDetector.SimpleOnMoveGestureListener + */ + public interface OnMoveGestureListener { + public boolean onMove(MoveGestureDetector detector); + + public boolean onMoveBegin(MoveGestureDetector detector); + + public void onMoveEnd(MoveGestureDetector detector); + } + + /** + * Helper class which may be extended and where the methods may be + * implemented. This way it is not necessary to implement all methods + * of OnMoveGestureListener. + */ + public static class SimpleOnMoveGestureListener implements OnMoveGestureListener { + public boolean onMove(MoveGestureDetector detector) { + return false; + } + + public boolean onMoveBegin(MoveGestureDetector detector) { + return true; + } + + public void onMoveEnd(MoveGestureDetector detector) { + // Do nothing, overridden implementation may be used + } + } + +} diff --git a/src/org/thoughtcrime/securesms/scribbles/multitouch/RotateGestureDetector.java b/src/org/thoughtcrime/securesms/scribbles/multitouch/RotateGestureDetector.java new file mode 100644 index 0000000000..839f7b658b --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/multitouch/RotateGestureDetector.java @@ -0,0 +1,170 @@ +package org.thoughtcrime.securesms.scribbles.multitouch; + +import android.content.Context; +import android.view.MotionEvent; + +/** + * @author Almer Thie (code.almeros.com) + * Copyright (c) 2013, Almer Thie (code.almeros.com) + *

+ * All rights reserved. + *

+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + *

+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the distribution. + *

+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ +public class RotateGestureDetector extends TwoFingerGestureDetector { + + private static final String TAG = RotateGestureDetector.class.getName(); + private final OnRotateGestureListener mListener; + private boolean mSloppyGesture; + + + public RotateGestureDetector(Context context, OnRotateGestureListener listener) { + super(context); + mListener = listener; + } + + @Override + protected void handleStartProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_POINTER_DOWN: + // At least the second finger is on screen now + + resetState(); // In case we missed an UP/CANCEL event + mPrevEvent = MotionEvent.obtain(event); + mTimeDelta = 0; + + updateStateByEvent(event); + + // See if we have a sloppy gesture + mSloppyGesture = isSloppyGesture(event); + if (!mSloppyGesture) { + // No, start gesture now + mGestureInProgress = mListener.onRotateBegin(this); + } + break; + + case MotionEvent.ACTION_MOVE: + if (!mSloppyGesture) { + break; + } + + // See if we still have a sloppy gesture + mSloppyGesture = isSloppyGesture(event); + if (!mSloppyGesture) { + // No, start normal gesture now + mGestureInProgress = mListener.onRotateBegin(this); + } + + break; + + case MotionEvent.ACTION_POINTER_UP: + if (!mSloppyGesture) { + break; + } + + break; + } + } + + @Override + protected void handleInProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_POINTER_UP: + // Gesture ended but + updateStateByEvent(event); + + if (!mSloppyGesture) { + mListener.onRotateEnd(this); + } + + resetState(); + break; + + case MotionEvent.ACTION_CANCEL: + if (!mSloppyGesture) { + mListener.onRotateEnd(this); + } + + resetState(); + break; + + case MotionEvent.ACTION_MOVE: + updateStateByEvent(event); + + // Only accept the event if our relative pressure is within + // a certain limit. This can help filter shaky data as a + // finger is lifted. + if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) { + final boolean updatePrevious = mListener.onRotate(this); + if (updatePrevious) { + mPrevEvent.recycle(); + mPrevEvent = MotionEvent.obtain(event); + } + } + break; + } + } + + @Override + protected void resetState() { + super.resetState(); + mSloppyGesture = false; + } + + /** + * Return the rotation difference from the previous rotate event to the current + * event. + * + * @return The current rotation //difference in degrees. + */ + public float getRotationDegreesDelta() { + double diffRadians = Math.atan2(mPrevFingerDiffY, mPrevFingerDiffX) - Math.atan2(mCurrFingerDiffY, mCurrFingerDiffX); + return (float) (diffRadians * 180 / Math.PI); + } + + /** + * Listener which must be implemented which is used by RotateGestureDetector + * to perform callbacks to any implementing class which is registered to a + * RotateGestureDetector via the constructor. + * + * @see RotateGestureDetector.SimpleOnRotateGestureListener + */ + public interface OnRotateGestureListener { + public boolean onRotate(RotateGestureDetector detector); + + public boolean onRotateBegin(RotateGestureDetector detector); + + public void onRotateEnd(RotateGestureDetector detector); + } + + /** + * Helper class which may be extended and where the methods may be + * implemented. This way it is not necessary to implement all methods + * of OnRotateGestureListener. + */ + public static class SimpleOnRotateGestureListener implements OnRotateGestureListener { + public boolean onRotate(RotateGestureDetector detector) { + return false; + } + + public boolean onRotateBegin(RotateGestureDetector detector) { + return true; + } + + public void onRotateEnd(RotateGestureDetector detector) { + // Do nothing, overridden implementation may be used + } + } +} diff --git a/src/org/thoughtcrime/securesms/scribbles/multitouch/ShoveGestureDetector.java b/src/org/thoughtcrime/securesms/scribbles/multitouch/ShoveGestureDetector.java new file mode 100644 index 0000000000..cb4eb339e1 --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/multitouch/ShoveGestureDetector.java @@ -0,0 +1,201 @@ +package org.thoughtcrime.securesms.scribbles.multitouch; + +import android.content.Context; +import android.view.MotionEvent; + +/** + * @author Robert Nordan (robert.nordan@norkart.no) + *

+ * Copyright (c) 2013, Norkart AS + *

+ * All rights reserved. + *

+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + *

+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the distribution. + *

+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ +public class ShoveGestureDetector extends TwoFingerGestureDetector { + + private final OnShoveGestureListener mListener; + private float mPrevAverageY; + private float mCurrAverageY; + private boolean mSloppyGesture; + + public ShoveGestureDetector(Context context, OnShoveGestureListener listener) { + super(context); + mListener = listener; + } + + @Override + protected void handleStartProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_POINTER_DOWN: + // At least the second finger is on screen now + + resetState(); // In case we missed an UP/CANCEL event + mPrevEvent = MotionEvent.obtain(event); + mTimeDelta = 0; + + updateStateByEvent(event); + + // See if we have a sloppy gesture + mSloppyGesture = isSloppyGesture(event); + if (!mSloppyGesture) { + // No, start gesture now + mGestureInProgress = mListener.onShoveBegin(this); + } + break; + + case MotionEvent.ACTION_MOVE: + if (!mSloppyGesture) { + break; + } + + // See if we still have a sloppy gesture + mSloppyGesture = isSloppyGesture(event); + if (!mSloppyGesture) { + // No, start normal gesture now + mGestureInProgress = mListener.onShoveBegin(this); + } + + break; + + case MotionEvent.ACTION_POINTER_UP: + if (!mSloppyGesture) { + break; + } + + break; + } + } + + @Override + protected void handleInProgressEvent(int actionCode, MotionEvent event) { + switch (actionCode) { + case MotionEvent.ACTION_POINTER_UP: + // Gesture ended but + updateStateByEvent(event); + + if (!mSloppyGesture) { + mListener.onShoveEnd(this); + } + + resetState(); + break; + + case MotionEvent.ACTION_CANCEL: + if (!mSloppyGesture) { + mListener.onShoveEnd(this); + } + + resetState(); + break; + + case MotionEvent.ACTION_MOVE: + updateStateByEvent(event); + + // Only accept the event if our relative pressure is within + // a certain limit. This can help filter shaky data as a + // finger is lifted. Also check that shove is meaningful. + if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD + && Math.abs(getShovePixelsDelta()) > 0.5f) { + final boolean updatePrevious = mListener.onShove(this); + if (updatePrevious) { + mPrevEvent.recycle(); + mPrevEvent = MotionEvent.obtain(event); + } + } + break; + } + } + + @Override + protected void updateStateByEvent(MotionEvent curr) { + super.updateStateByEvent(curr); + + final MotionEvent prev = mPrevEvent; + float py0 = prev.getY(0); + float py1 = prev.getY(1); + mPrevAverageY = (py0 + py1) / 2.0f; + + float cy0 = curr.getY(0); + float cy1 = curr.getY(1); + mCurrAverageY = (cy0 + cy1) / 2.0f; + } + + @Override + protected boolean isSloppyGesture(MotionEvent event) { + boolean sloppy = super.isSloppyGesture(event); + if (sloppy) + return true; + + // If it's not traditionally sloppy, we check if the angle between fingers + // is acceptable. + double angle = Math.abs(Math.atan2(mCurrFingerDiffY, mCurrFingerDiffX)); + //about 20 degrees, left or right + return !((0.0f < angle && angle < 0.35f) + || 2.79f < angle && angle < Math.PI); + } + + /** + * Return the distance in pixels from the previous shove event to the current + * event. + * + * @return The current distance in pixels. + */ + public float getShovePixelsDelta() { + return mCurrAverageY - mPrevAverageY; + } + + @Override + protected void resetState() { + super.resetState(); + mSloppyGesture = false; + mPrevAverageY = 0.0f; + mCurrAverageY = 0.0f; + } + + /** + * Listener which must be implemented which is used by ShoveGestureDetector + * to perform callbacks to any implementing class which is registered to a + * ShoveGestureDetector via the constructor. + * + * @see ShoveGestureDetector.SimpleOnShoveGestureListener + */ + public interface OnShoveGestureListener { + public boolean onShove(ShoveGestureDetector detector); + + public boolean onShoveBegin(ShoveGestureDetector detector); + + public void onShoveEnd(ShoveGestureDetector detector); + } + + /** + * Helper class which may be extended and where the methods may be + * implemented. This way it is not necessary to implement all methods + * of OnShoveGestureListener. + */ + public static class SimpleOnShoveGestureListener implements OnShoveGestureListener { + public boolean onShove(ShoveGestureDetector detector) { + return false; + } + + public boolean onShoveBegin(ShoveGestureDetector detector) { + return true; + } + + public void onShoveEnd(ShoveGestureDetector detector) { + // Do nothing, overridden implementation may be used + } + } +} diff --git a/src/org/thoughtcrime/securesms/scribbles/multitouch/TwoFingerGestureDetector.java b/src/org/thoughtcrime/securesms/scribbles/multitouch/TwoFingerGestureDetector.java new file mode 100644 index 0000000000..ef94a9e63c --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/multitouch/TwoFingerGestureDetector.java @@ -0,0 +1,186 @@ +package org.thoughtcrime.securesms.scribbles.multitouch; + +import android.content.Context; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +/** + * @author Almer Thie (code.almeros.com) + * Copyright (c) 2013, Almer Thie (code.almeros.com) + *

+ * All rights reserved. + *

+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + *

+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the distribution. + *

+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ +public abstract class TwoFingerGestureDetector extends BaseGestureDetector { + + private final float mEdgeSlop; + protected float mPrevFingerDiffX; + protected float mPrevFingerDiffY; + protected float mCurrFingerDiffX; + protected float mCurrFingerDiffY; + private float mRightSlopEdge; + private float mBottomSlopEdge; + private float mCurrLen; + private float mPrevLen; + + public TwoFingerGestureDetector(Context context) { + super(context); + + ViewConfiguration config = ViewConfiguration.get(context); + mEdgeSlop = config.getScaledEdgeSlop(); + } + + @Override + protected abstract void handleStartProgressEvent(int actionCode, MotionEvent event); + + @Override + protected abstract void handleInProgressEvent(int actionCode, MotionEvent event); + + protected void updateStateByEvent(MotionEvent curr) { + super.updateStateByEvent(curr); + + final MotionEvent prev = mPrevEvent; + + mCurrLen = -1; + mPrevLen = -1; + + // Previous + final float px0 = prev.getX(0); + final float py0 = prev.getY(0); + final float px1 = prev.getX(1); + final float py1 = prev.getY(1); + final float pvx = px1 - px0; + final float pvy = py1 - py0; + mPrevFingerDiffX = pvx; + mPrevFingerDiffY = pvy; + + // Current + final float cx0 = curr.getX(0); + final float cy0 = curr.getY(0); + final float cx1 = curr.getX(1); + final float cy1 = curr.getY(1); + final float cvx = cx1 - cx0; + final float cvy = cy1 - cy0; + mCurrFingerDiffX = cvx; + mCurrFingerDiffY = cvy; + } + + /** + * Return the current distance between the two pointers forming the + * gesture in progress. + * + * @return Distance between pointers in pixels. + */ + public float getCurrentSpan() { + if (mCurrLen == -1) { + final float cvx = mCurrFingerDiffX; + final float cvy = mCurrFingerDiffY; + mCurrLen = (float) Math.sqrt(cvx * cvx + cvy * cvy); + } + return mCurrLen; + } + + /** + * Return the previous distance between the two pointers forming the + * gesture in progress. + * + * @return Previous distance between pointers in pixels. + */ + public float getPreviousSpan() { + if (mPrevLen == -1) { + final float pvx = mPrevFingerDiffX; + final float pvy = mPrevFingerDiffY; + mPrevLen = (float) Math.sqrt(pvx * pvx + pvy * pvy); + } + return mPrevLen; + } + + /** + * Check if we have a sloppy gesture. Sloppy gestures can happen if the edge + * of the user's hand is touching the screen, for example. + * + * @param event + * @return + */ + protected boolean isSloppyGesture(MotionEvent event) { + // As orientation can change, query the metrics in touch down + DisplayMetrics metrics = mContext.getResources().getDisplayMetrics(); + mRightSlopEdge = metrics.widthPixels - mEdgeSlop; + mBottomSlopEdge = metrics.heightPixels - mEdgeSlop; + + final float edgeSlop = mEdgeSlop; + final float rightSlop = mRightSlopEdge; + final float bottomSlop = mBottomSlopEdge; + + final float x0 = event.getRawX(); + final float y0 = event.getRawY(); + final float x1 = getRawX(event, 1); + final float y1 = getRawY(event, 1); + + + Log.w("TwoFinger", + String.format("x0: %f, y0: %f, x1: %f, y1: %f, EdgeSlop: %f, RightSlop: %f, BottomSlop: %f", + x0, y0, x1, y1, edgeSlop, rightSlop, bottomSlop)); + + + boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop + || x0 > rightSlop || y0 > bottomSlop; + boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop + || x1 > rightSlop || y1 > bottomSlop; + + if (p0sloppy && p1sloppy) { + return true; + } else if (p0sloppy) { + return true; + } else if (p1sloppy) { + return true; + } + return false; + } + + /** + * MotionEvent has no getRawX(int) method; simulate it pending future API approval. + * + * @param event + * @param pointerIndex + * @return + */ + protected static float getRawX(MotionEvent event, int pointerIndex) { + float offset = event.getX() - event.getRawX(); + if (pointerIndex < event.getPointerCount()) { + return event.getX(pointerIndex) + offset; + } + return 0f; + } + + /** + * MotionEvent has no getRawY(int) method; simulate it pending future API approval. + * + * @param event + * @param pointerIndex + * @return + */ + protected static float getRawY(MotionEvent event, int pointerIndex) { + float offset = Math.abs(event.getY() - event.getRawY()); + if (pointerIndex < event.getPointerCount()) { + return event.getY(pointerIndex) + offset; + } + return 0f; + } + +} diff --git a/src/org/thoughtcrime/securesms/scribbles/viewmodel/Font.java b/src/org/thoughtcrime/securesms/scribbles/viewmodel/Font.java new file mode 100644 index 0000000000..2de6decf48 --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/viewmodel/Font.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2016 UPTech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.thoughtcrime.securesms.scribbles.viewmodel; + + +public class Font { + + /** + * color value (ex: 0xFF00FF) + */ + private int color; + /** + * name of the font + */ + private String typeface; + /** + * size of the font, relative to parent + */ + private float size; + + public Font() { + } + + public void increaseSize(float diff) { + if (size + diff <= Limits.MAX_FONT_SIZE) { + size = size + diff; + } + } + + public void decreaseSize(float diff) { + if (size - diff >= Limits.MIN_FONT_SIZE) { + size = size - diff; + } + } + + public int getColor() { + return color; + } + + public void setColor(int color) { + this.color = color; + } + + public String getTypeface() { + return typeface; + } + + public void setTypeface(String typeface) { + this.typeface = typeface; + } + + public float getSize() { + return size; + } + + public void setSize(float size) { + this.size = size; + } + + private interface Limits { + float MIN_FONT_SIZE = 0.01F; + float MAX_FONT_SIZE = 0.46F; + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/scribbles/viewmodel/Layer.java b/src/org/thoughtcrime/securesms/scribbles/viewmodel/Layer.java new file mode 100644 index 0000000000..3b49bd8524 --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/viewmodel/Layer.java @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2016 UPTech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.thoughtcrime.securesms.scribbles.viewmodel; + +import android.support.annotation.FloatRange; +import android.util.Log; + +public class Layer { + + /** + * rotation relative to the layer center, in degrees + */ + @FloatRange(from = 0.0F, to = 360.0F) + private float rotationInDegrees; + + private float scale; + /** + * top left X coordinate, relative to parent canvas + */ + private float x; + /** + * top left Y coordinate, relative to parent canvas + */ + private float y; + /** + * is layer flipped horizontally (by X-coordinate) + */ + private boolean isFlipped; + + public Layer() { + reset(); + } + + protected void reset() { + this.rotationInDegrees = 0.0F; + this.scale = 1.0F; + this.isFlipped = false; + this.x = 0.0F; + this.y = 0.0F; + } + + public void postScale(float scaleDiff) { + Log.w("Layer", "ScaleDiff: " + scaleDiff); + float newVal = scale + scaleDiff; + if (newVal >= getMinScale() && newVal <= getMaxScale()) { + scale = newVal; + } + } + + protected float getMaxScale() { + return Limits.MAX_SCALE; + } + + protected float getMinScale() { + return Limits.MIN_SCALE; + } + + public void postRotate(float rotationInDegreesDiff) { + this.rotationInDegrees += rotationInDegreesDiff; + this.rotationInDegrees %= 360.0F; + } + + public void postTranslate(float dx, float dy) { + this.x += dx; + this.y += dy; + } + + public void flip() { + this.isFlipped = !isFlipped; + } + + public float initialScale() { + return Limits.INITIAL_ENTITY_SCALE; + } + + public float getRotationInDegrees() { + return rotationInDegrees; + } + + public void setRotationInDegrees(@FloatRange(from = 0.0, to = 360.0) float rotationInDegrees) { + this.rotationInDegrees = rotationInDegrees; + } + + public float getScale() { + return scale; + } + + public void setScale(float scale) { + this.scale = scale; + } + + public float getX() { + return x; + } + + public void setX(float x) { + this.x = x; + } + + public float getY() { + return y; + } + + public void setY(float y) { + this.y = y; + } + + public boolean isFlipped() { + return isFlipped; + } + + public void setFlipped(boolean flipped) { + isFlipped = flipped; + } + + interface Limits { + float MIN_SCALE = 0.06F; + float MAX_SCALE = 4.0F; + float INITIAL_ENTITY_SCALE = 0.4F; + } +} diff --git a/src/org/thoughtcrime/securesms/scribbles/viewmodel/TextLayer.java b/src/org/thoughtcrime/securesms/scribbles/viewmodel/TextLayer.java new file mode 100644 index 0000000000..c367510827 --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/viewmodel/TextLayer.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2016 UPTech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.thoughtcrime.securesms.scribbles.viewmodel; + +import android.util.Log; + +public class TextLayer extends Layer { + + private String text; + private Font font; + + public TextLayer() { + } + + @Override + protected void reset() { + super.reset(); + this.text = ""; + this.font = new Font(); + } + + @Override + protected float getMaxScale() { + return Limits.MAX_SCALE; + } + + @Override + protected float getMinScale() { + return Limits.MIN_SCALE; + } + + @Override + public float initialScale() { + return Limits.INITIAL_SCALE; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public Font getFont() { + return font; + } + + public void setFont(Font font) { + this.font = font; + } + + @Override + public void postScale(float scaleDiff) { + if (scaleDiff > 0) font.increaseSize(scaleDiff); + else if (scaleDiff < 0) font.decreaseSize(Math.abs(scaleDiff)); + } + + public interface Limits { + /** + * limit text size to view bounds + * so that users don't put small font size and scale it 100+ times + */ + float MAX_SCALE = 1.0F; + float MIN_SCALE = 0.2F; + + float MIN_BITMAP_HEIGHT = 0.13F; + + float FONT_SIZE_STEP = 0.008F; + + float INITIAL_FONT_SIZE = 0.075F; + int INITIAL_FONT_COLOR = 0xff000000; + + float INITIAL_SCALE = 0.8F; // set the same to avoid text scaling + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/scribbles/widget/CanvasView.java b/src/org/thoughtcrime/securesms/scribbles/widget/CanvasView.java new file mode 100644 index 0000000000..79a393a42f --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/widget/CanvasView.java @@ -0,0 +1,901 @@ +/** + * CanvasView.java + * + * Copyright (c) 2014 Tomohiro IKEDA (Korilakkuma) + * Released under the MIT license + */ + +package org.thoughtcrime.securesms.scribbles.widget; + + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * This class defines fields and methods for drawing. + */ +public class CanvasView extends View { + + // Enumeration for Mode + public enum Mode { + DRAW, + TEXT, + ERASER; + } + + // Enumeration for Drawer + public enum Drawer { + PEN, + LINE, + RECTANGLE, + CIRCLE, + ELLIPSE, + QUADRATIC_BEZIER, + QUBIC_BEZIER; + } + + private Context context = null; + private Canvas canvas = null; + private Bitmap bitmap = null; + + private List pathLists = new ArrayList(); + private List paintLists = new ArrayList(); + + // for Eraser +// private int baseColor = Color.WHITE; + private int baseColor = Color.TRANSPARENT; + + // for Undo, Redo + private int historyPointer = 0; + + // Flags + private Mode mode = Mode.DRAW; + private Drawer drawer = Drawer.PEN; + private boolean isDown = false; + + // for Paint + private Paint.Style paintStyle = Paint.Style.STROKE; + private int paintStrokeColor = Color.BLACK; + private int paintFillColor = Color.BLACK; + private float paintStrokeWidth = 15F; + private int opacity = 255; + private float blur = 0F; + private Paint.Cap lineCap = Paint.Cap.ROUND; + + // for Text + private String text = ""; + private Typeface fontFamily = Typeface.DEFAULT; + private float fontSize = 32F; + private Paint.Align textAlign = Paint.Align.RIGHT; // fixed + private Paint textPaint = new Paint(); + private float textX = 0F; + private float textY = 0F; + + // for Drawer + private float startX = 0F; + private float startY = 0F; + private float controlX = 0F; + private float controlY = 0F; + + private boolean active = false; + + /** + * Copy Constructor + * + * @param context + * @param attrs + * @param defStyle + */ + public CanvasView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + this.setup(context); + } + + /** + * Copy Constructor + * + * @param context + * @param attrs + */ + public CanvasView(Context context, AttributeSet attrs) { + super(context, attrs); + this.setup(context); + } + + /** + * Copy Constructor + * + * @param context + */ + public CanvasView(Context context) { + super(context); + this.setup(context); + } + + /** + * Common initialization. + * + * @param context + */ + private void setup(Context context) { + this.context = context; + + this.pathLists.add(new Path()); + this.paintLists.add(this.createPaint()); + this.historyPointer++; + + this.textPaint.setARGB(0, 255, 255, 255); + } + + /** + * This method creates the instance of Paint. + * In addition, this method sets styles for Paint. + * + * @return paint This is returned as the instance of Paint + */ + private Paint createPaint() { + Paint paint = new Paint(); + + paint.setAntiAlias(true); + paint.setStyle(this.paintStyle); + paint.setStrokeWidth(this.paintStrokeWidth); + paint.setStrokeCap(this.lineCap); + paint.setStrokeJoin(Paint.Join.MITER); // fixed + + // for Text + if (this.mode == Mode.TEXT) { + paint.setTypeface(this.fontFamily); + paint.setTextSize(this.fontSize); + paint.setTextAlign(this.textAlign); + paint.setStrokeWidth(0F); + } + + if (this.mode == Mode.ERASER) { + // Eraser + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + paint.setARGB(0, 0, 0, 0); + + // paint.setColor(this.baseColor); + // paint.setShadowLayer(this.blur, 0F, 0F, this.baseColor); + } else { + // Otherwise + paint.setColor(this.paintStrokeColor); + paint.setShadowLayer(this.blur, 0F, 0F, this.paintStrokeColor); + paint.setAlpha(this.opacity); + } + + return paint; + } + + /** + * This method initialize Path. + * Namely, this method creates the instance of Path, + * and moves current position. + * + * @param event This is argument of onTouchEvent method + * @return path This is returned as the instance of Path + */ + private Path createPath(MotionEvent event) { + Path path = new Path(); + + // Save for ACTION_MOVE + this.startX = event.getX(); + this.startY = event.getY(); + + path.moveTo(this.startX, this.startY); + + return path; + } + + /** + * This method updates the lists for the instance of Path and Paint. + * "Undo" and "Redo" are enabled by this method. + * + * @param path the instance of Path + */ + private void updateHistory(Path path) { + if (this.historyPointer == this.pathLists.size()) { + this.pathLists.add(path); + this.paintLists.add(this.createPaint()); + this.historyPointer++; + } else { + // On the way of Undo or Redo + this.pathLists.set(this.historyPointer, path); + this.paintLists.set(this.historyPointer, this.createPaint()); + this.historyPointer++; + + for (int i = this.historyPointer, size = this.paintLists.size(); i < size; i++) { + this.pathLists.remove(this.historyPointer); + this.paintLists.remove(this.historyPointer); + } + } + } + + /** + * This method gets the instance of Path that pointer indicates. + * + * @return the instance of Path + */ + private Path getCurrentPath() { + return this.pathLists.get(this.historyPointer - 1); + } + + /** + * This method draws text. + * + * @param canvas the instance of Canvas + */ + private void drawText(Canvas canvas) { + if (this.text.length() <= 0) { + return; + } + + if (this.mode == Mode.TEXT) { + this.textX = this.startX; + this.textY = this.startY; + + this.textPaint = this.createPaint(); + } + + float textX = this.textX; + float textY = this.textY; + + Paint paintForMeasureText = new Paint(); + + // Line break automatically + float textLength = paintForMeasureText.measureText(this.text); + float lengthOfChar = textLength / (float)this.text.length(); + float restWidth = this.canvas.getWidth() - textX; // text-align : right + int numChars = (lengthOfChar <= 0) ? 1 : (int) Math.floor((double)(restWidth / lengthOfChar)); // The number of characters at 1 line + int modNumChars = (numChars < 1) ? 1 : numChars; + float y = textY; + + for (int i = 0, len = this.text.length(); i < len; i += modNumChars) { + String substring = ""; + + if ((i + modNumChars) < len) { + substring = this.text.substring(i, (i + modNumChars)); + } else { + substring = this.text.substring(i, len); + } + + y += this.fontSize; + + canvas.drawText(substring, textX, y, this.textPaint); + } + } + + /** + * This method defines processes on MotionEvent.ACTION_DOWN + * + * @param event This is argument of onTouchEvent method + */ + private void onActionDown(MotionEvent event) { + switch (this.mode) { + case DRAW : + case ERASER : + if ((this.drawer != Drawer.QUADRATIC_BEZIER) && (this.drawer != Drawer.QUBIC_BEZIER)) { + // Oherwise + this.updateHistory(this.createPath(event)); + this.isDown = true; + } else { + // Bezier + if ((this.startX == 0F) && (this.startY == 0F)) { + // The 1st tap + this.updateHistory(this.createPath(event)); + } else { + // The 2nd tap + this.controlX = event.getX(); + this.controlY = event.getY(); + + this.isDown = true; + } + } + + break; + case TEXT : + this.startX = event.getX(); + this.startY = event.getY(); + + break; + default : + break; + } + } + + /** + * This method defines processes on MotionEvent.ACTION_MOVE + * + * @param event This is argument of onTouchEvent method + */ + private void onActionMove(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + + switch (this.mode) { + case DRAW : + case ERASER : + + if ((this.drawer != Drawer.QUADRATIC_BEZIER) && (this.drawer != Drawer.QUBIC_BEZIER)) { + if (!isDown) { + return; + } + + Path path = this.getCurrentPath(); + + switch (this.drawer) { + case PEN : + path.lineTo(x, y); + break; + case LINE : + path.reset(); + path.moveTo(this.startX, this.startY); + path.lineTo(x, y); + break; + case RECTANGLE : + path.reset(); + path.addRect(this.startX, this.startY, x, y, Path.Direction.CCW); + break; + case CIRCLE : + double distanceX = Math.abs((double)(this.startX - x)); + double distanceY = Math.abs((double)(this.startX - y)); + double radius = Math.sqrt(Math.pow(distanceX, 2.0) + Math.pow(distanceY, 2.0)); + + path.reset(); + path.addCircle(this.startX, this.startY, (float)radius, Path.Direction.CCW); + break; + case ELLIPSE : + RectF rect = new RectF(this.startX, this.startY, x, y); + + path.reset(); + path.addOval(rect, Path.Direction.CCW); + break; + default : + break; + } + } else { + if (!isDown) { + return; + } + + Path path = this.getCurrentPath(); + + path.reset(); + path.moveTo(this.startX, this.startY); + path.quadTo(this.controlX, this.controlY, x, y); + } + + break; + case TEXT : + this.startX = x; + this.startY = y; + + break; + default : + break; + } + } + + /** + * This method defines processes on MotionEvent.ACTION_DOWN + * + * @param event This is argument of onTouchEvent method + */ + private void onActionUp(MotionEvent event) { + if (isDown) { + this.startX = 0F; + this.startY = 0F; + this.isDown = false; + } + } + + public void setActive(boolean active) { + this.active = active; + } + + /** + * This method updates the instance of Canvas (View) + * + * @param canvas the new instance of Canvas + */ + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Before "drawPath" + canvas.drawColor(this.baseColor); + + if (this.bitmap != null) { + canvas.drawBitmap(this.bitmap, 0F, 0F, new Paint()); + } + + for (int i = 0; i < this.historyPointer; i++) { + Path path = this.pathLists.get(i); + Paint paint = this.paintLists.get(i); + + canvas.drawPath(path, paint); + } + + this.drawText(canvas); + + this.canvas = canvas; + } + + public void render(Canvas canvas) { + if (this.canvas == null) return; + + float scaleX = 1.0F * canvas.getWidth() / this.canvas.getWidth(); + float scaleY = 1.0F * canvas.getHeight() / this.canvas.getHeight(); + + Matrix matrix = new Matrix(); + matrix.setScale(scaleX, scaleY); + + for (int i = 0; i < this.historyPointer; i++) { + Path path = this.pathLists.get(i); + Paint paint = this.paintLists.get(i); + + Path scaledPath = new Path(); + path.transform(matrix, scaledPath); + + Paint scaledPaint = new Paint(paint); + scaledPaint.setStrokeWidth(scaledPaint.getStrokeWidth() * scaleX); + + canvas.drawPath(scaledPath, scaledPaint); + } + } + + /** + * This method set event listener for drawing. + * + * @param event the instance of MotionEvent + * @return + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!active) return false; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + this.onActionDown(event); + break; + case MotionEvent.ACTION_MOVE : + this.onActionMove(event); + break; + case MotionEvent.ACTION_UP : + this.onActionUp(event); + break; + default : + break; + } + + // Re draw + this.invalidate(); + + return true; + } + + /** + * This method is getter for mode. + * + * @return + */ + public Mode getMode() { + return this.mode; + } + + /** + * This method is setter for mode. + * + * @param mode + */ + public void setMode(Mode mode) { + this.mode = mode; + } + + /** + * This method is getter for drawer. + * + * @return + */ + public Drawer getDrawer() { + return this.drawer; + } + + /** + * This method is setter for drawer. + * + * @param drawer + */ + public void setDrawer(Drawer drawer) { + this.drawer = drawer; + } + + /** + * This method draws canvas again for Undo. + * + * @return If Undo is enabled, this is returned as true. Otherwise, this is returned as false. + */ + public boolean undo() { + if (this.historyPointer > 1) { + this.historyPointer--; + this.invalidate(); + + return true; + } else { + return false; + } + } + + /** + * This method draws canvas again for Redo. + * + * @return If Redo is enabled, this is returned as true. Otherwise, this is returned as false. + */ + public boolean redo() { + if (this.historyPointer < this.pathLists.size()) { + this.historyPointer++; + this.invalidate(); + + return true; + } else { + return false; + } + } + + /** + * This method initializes canvas. + * + * @return + */ + public void clear() { + Path path = new Path(); + path.moveTo(0F, 0F); + path.addRect(0F, 0F, 1000F, 1000F, Path.Direction.CCW); + path.close(); + + Paint paint = new Paint(); + paint.setColor(Color.WHITE); + paint.setStyle(Paint.Style.FILL); + + if (this.historyPointer == this.pathLists.size()) { + this.pathLists.add(path); + this.paintLists.add(paint); + this.historyPointer++; + } else { + // On the way of Undo or Redo + this.pathLists.set(this.historyPointer, path); + this.paintLists.set(this.historyPointer, paint); + this.historyPointer++; + + for (int i = this.historyPointer, size = this.paintLists.size(); i < size; i++) { + this.pathLists.remove(this.historyPointer); + this.paintLists.remove(this.historyPointer); + } + } + + this.text = ""; + + // Clear + this.invalidate(); + } + + /** + * This method is getter for canvas background color + * + * @return + */ + public int getBaseColor() { + return this.baseColor; + } + + /** + * This method is setter for canvas background color + * + * @param color + */ + public void setBaseColor(int color) { + this.baseColor = color; + } + + /** + * This method is getter for drawn text. + * + * @return + */ + public String getText() { + return this.text; + } + + /** + * This method is setter for drawn text. + * + * @param text + */ + public void setText(String text) { + this.text = text; + } + + /** + * This method is getter for stroke or fill. + * + * @return + */ + public Paint.Style getPaintStyle() { + return this.paintStyle; + } + + /** + * This method is setter for stroke or fill. + * + * @param style + */ + public void setPaintStyle(Paint.Style style) { + this.paintStyle = style; + } + + /** + * This method is getter for stroke color. + * + * @return + */ + public int getPaintStrokeColor() { + return this.paintStrokeColor; + } + + /** + * This method is setter for stroke color. + * + * @param color + */ + public void setPaintStrokeColor(int color) { + this.paintStrokeColor = color; + } + + /** + * This method is getter for fill color. + * But, current Android API cannot set fill color (?). + * + * @return + */ + public int getPaintFillColor() { + return this.paintFillColor; + }; + + /** + * This method is setter for fill color. + * But, current Android API cannot set fill color (?). + * + * @param color + */ + public void setPaintFillColor(int color) { + this.paintFillColor = color; + } + + /** + * This method is getter for stroke width. + * + * @return + */ + public float getPaintStrokeWidth() { + return this.paintStrokeWidth; + } + + /** + * This method is setter for stroke width. + * + * @param width + */ + public void setPaintStrokeWidth(float width) { + if (width >= 0) { + this.paintStrokeWidth = width; + } else { + this.paintStrokeWidth = 3F; + } + } + + /** + * This method is getter for alpha. + * + * @return + */ + public int getOpacity() { + return this.opacity; + } + + /** + * This method is setter for alpha. + * The 1st argument must be between 0 and 255. + * + * @param opacity + */ + public void setOpacity(int opacity) { + if ((opacity >= 0) && (opacity <= 255)) { + this.opacity = opacity; + } else { + this.opacity= 255; + } + } + + /** + * This method is getter for amount of blur. + * + * @return + */ + public float getBlur() { + return this.blur; + } + + /** + * This method is setter for amount of blur. + * The 1st argument is greater than or equal to 0.0. + * + * @param blur + */ + public void setBlur(float blur) { + if (blur >= 0) { + this.blur = blur; + } else { + this.blur = 0F; + } + } + + /** + * This method is getter for line cap. + * + * @return + */ + public Paint.Cap getLineCap() { + return this.lineCap; + } + + /** + * This method is setter for line cap. + * + * @param cap + */ + public void setLineCap(Paint.Cap cap) { + this.lineCap = cap; + } + + /** + * This method is getter for font size, + * + * @return + */ + public float getFontSize() { + return this.fontSize; + } + + /** + * This method is setter for font size. + * The 1st argument is greater than or equal to 0.0. + * + * @param size + */ + public void setFontSize(float size) { + if (size >= 0F) { + this.fontSize = size; + } else { + this.fontSize = 32F; + } + } + + /** + * This method is getter for font-family. + * + * @return + */ + public Typeface getFontFamily() { + return this.fontFamily; + } + + /** + * This method is setter for font-family. + * + * @param face + */ + public void setFontFamily(Typeface face) { + this.fontFamily = face; + } + + /** + * This method gets current canvas as bitmap. + * + * @return This is returned as bitmap. + */ + public Bitmap getBitmap() { + this.setDrawingCacheEnabled(false); + this.setDrawingCacheEnabled(true); + + return Bitmap.createBitmap(this.getDrawingCache()); + } + + /** + * This method gets current canvas as scaled bitmap. + * + * @return This is returned as scaled bitmap. + */ + public Bitmap getScaleBitmap(int w, int h) { + this.setDrawingCacheEnabled(false); + this.setDrawingCacheEnabled(true); + + return Bitmap.createScaledBitmap(this.getDrawingCache(), w, h, true); + } + + /** + * This method draws the designated bitmap to canvas. + * + * @param bitmap + */ + public void drawBitmap(Bitmap bitmap) { + this.bitmap = bitmap; + this.invalidate(); + } + + /** + * This method draws the designated byte array of bitmap to canvas. + * + * @param byteArray This is returned as byte array of bitmap. + */ + public void drawBitmap(byte[] byteArray) { + this.drawBitmap(BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length)); + } + + /** + * This static method gets the designated bitmap as byte array. + * + * @param bitmap + * @param format + * @param quality + * @return This is returned as byte array of bitmap. + */ + public static byte[] getBitmapAsByteArray(Bitmap bitmap, CompressFormat format, int quality) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + bitmap.compress(format, quality, byteArrayOutputStream); + + return byteArrayOutputStream.toByteArray(); + } + + /** + * This method gets the bitmap as byte array. + * + * @param format + * @param quality + * @return This is returned as byte array of bitmap. + */ + public byte[] getBitmapAsByteArray(CompressFormat format, int quality) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + this.getBitmap().compress(format, quality, byteArrayOutputStream); + + return byteArrayOutputStream.toByteArray(); + } + + /** + * This method gets the bitmap as byte array. + * Bitmap format is PNG, and quality is 100. + * + * @return This is returned as byte array of bitmap. + */ + public byte[] getBitmapAsByteArray() { + return this.getBitmapAsByteArray(CompressFormat.PNG, 100); + } + +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/scribbles/widget/MotionView.java b/src/org/thoughtcrime/securesms/scribbles/widget/MotionView.java new file mode 100644 index 0000000000..670b861df7 --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/widget/MotionView.java @@ -0,0 +1,473 @@ +/** + * Copyright (c) 2016 UPTech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.thoughtcrime.securesms.scribbles.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.view.GestureDetectorCompat; +import android.support.v4.view.ViewCompat; +import android.text.Editable; +import android.text.InputType; +import android.text.Selection; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.FrameLayout; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.scribbles.multitouch.MoveGestureDetector; +import org.thoughtcrime.securesms.scribbles.multitouch.RotateGestureDetector; +import org.thoughtcrime.securesms.scribbles.widget.entity.MotionEntity; +import org.thoughtcrime.securesms.scribbles.widget.entity.TextEntity; + +import java.util.ArrayList; +import java.util.List; + +public class MotionView extends FrameLayout implements TextWatcher { + + private static final String TAG = MotionView.class.getSimpleName(); + + public interface Constants { + float SELECTED_LAYER_ALPHA = 0.15F; + } + + public interface MotionViewCallback { + void onEntitySelected(@Nullable MotionEntity entity); + void onEntityDoubleTap(@NonNull MotionEntity entity); + } + + // layers + private final List entities = new ArrayList<>(); + @Nullable + private MotionEntity selectedEntity; + + private Paint selectedLayerPaint; + + // callback + @Nullable + private MotionViewCallback motionViewCallback; + + private EditText editText; + + // gesture detection + private ScaleGestureDetector scaleGestureDetector; + private RotateGestureDetector rotateGestureDetector; + private MoveGestureDetector moveGestureDetector; + private GestureDetectorCompat gestureDetectorCompat; + + // constructors + public MotionView(Context context) { + super(context); + init(context, null); + } + + public MotionView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public MotionView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + @SuppressWarnings("unused") + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public MotionView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs); + } + + private void init(@NonNull Context context, @Nullable AttributeSet attrs) { + // I fucking love Android + setWillNotDraw(false); + + selectedLayerPaint = new Paint(); + selectedLayerPaint.setAlpha((int) (255 * Constants.SELECTED_LAYER_ALPHA)); + selectedLayerPaint.setAntiAlias(true); + + this.editText = new EditText(context, attrs); + ViewCompat.setAlpha(this.editText, 0); + this.editText.setLayoutParams(new LayoutParams(1, 1, Gravity.TOP | Gravity.LEFT)); + this.editText.setClickable(false); + this.editText.setBackgroundColor(Color.TRANSPARENT); + this.editText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 1); + this.editText.setInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); + this.addView(editText); + this.editText.clearFocus(); + this.editText.addTextChangedListener(this); + + // init listeners + this.scaleGestureDetector = new ScaleGestureDetector(context, new ScaleListener()); + this.rotateGestureDetector = new RotateGestureDetector(context, new RotateListener()); + this.moveGestureDetector = new MoveGestureDetector(context, new MoveListener()); + this.gestureDetectorCompat = new GestureDetectorCompat(context, new TapsListener()); + + setOnTouchListener(onTouchListener); + + updateUI(); + } + + public void startEditing(TextEntity entity) { + editText.setFocusableInTouchMode(true); + editText.setFocusable(true); + editText.requestFocus(); + editText.setText(entity.getLayer().getText()); + Selection.setSelection(editText.getText(), editText.length()); + + InputMethodManager ims = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + ims.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); + } + + public MotionEntity getSelectedEntity() { + return selectedEntity; + } + + public List getEntities() { + return entities; + } + + public void setMotionViewCallback(@Nullable MotionViewCallback callback) { + this.motionViewCallback = callback; + } + + public void addEntity(@Nullable MotionEntity entity) { + if (entity != null) { + entities.add(entity); + selectEntity(entity, false); + } + } + + public void addEntityAndPosition(@Nullable MotionEntity entity) { + if (entity != null) { + initEntityBorder(entity); + initialTranslateAndScale(entity); + entities.add(entity); + selectEntity(entity, true); + } + } + + private void initEntityBorder(@NonNull MotionEntity entity ) { + // init stroke + int strokeSize = getResources().getDimensionPixelSize(R.dimen.scribble_stroke_size); + Paint borderPaint = new Paint(); + borderPaint.setStrokeWidth(strokeSize); + borderPaint.setAntiAlias(true); + borderPaint.setColor(getContext().getResources().getColor(R.color.sticker_selected_color)); + + entity.setBorderPaint(borderPaint); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + // dispatch draw is called after child views is drawn. + // the idea that is we draw background stickers, than child views (if any), and than selected item + // to draw on top of child views - do it in dispatchDraw(Canvas) + // to draw below that - do it in onDraw(Canvas) + if (selectedEntity != null) { + selectedEntity.draw(canvas, selectedLayerPaint); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + drawAllEntities(canvas); + } + + public void render(Canvas canvas) { + unselectEntity(); + draw(canvas); + } + + /** + * draws all entities on the canvas + * @param canvas Canvas where to draw all entities + */ + private void drawAllEntities(Canvas canvas) { + for (int i = 0; i < entities.size(); i++) { + entities.get(i).draw(canvas, null); + } + } + + /** + * as a side effect - the method deselects Entity (if any selected) + * @return bitmap with all the Entities at their current positions + */ + public Bitmap getThumbnailImage() { + selectEntity(null, false); + + Bitmap bmp = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); + // IMPORTANT: always create white background, cos if the image is saved in JPEG format, + // which doesn't have transparent pixels, the background will be black + bmp.eraseColor(Color.WHITE); + Canvas canvas = new Canvas(bmp); + drawAllEntities(canvas); + + return bmp; + } + + private void updateUI() { + invalidate(); + } + + private void handleTranslate(PointF delta) { + if (selectedEntity != null) { + float newCenterX = selectedEntity.absoluteCenterX() + delta.x; + float newCenterY = selectedEntity.absoluteCenterY() + delta.y; + // limit entity center to screen bounds + boolean needUpdateUI = false; + if (newCenterX >= 0 && newCenterX <= getWidth()) { + selectedEntity.getLayer().postTranslate(delta.x / getWidth(), 0.0F); + needUpdateUI = true; + } + if (newCenterY >= 0 && newCenterY <= getHeight()) { + selectedEntity.getLayer().postTranslate(0.0F, delta.y / getHeight()); + needUpdateUI = true; + } + if (needUpdateUI) { + updateUI(); + } + } + } + + private void initialTranslateAndScale(@NonNull MotionEntity entity) { + entity.moveToCanvasCenter(); + entity.getLayer().setScale(entity.getLayer().initialScale()); + } + + private void selectEntity(@Nullable MotionEntity entity, boolean updateCallback) { + if (selectedEntity != null) { + selectedEntity.setIsSelected(false); + + if (selectedEntity instanceof TextEntity) { + editText.clearComposingText(); + editText.clearFocus(); + + InputMethodManager imm = (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(editText.getWindowToken(), 0); + } + + } + if (entity != null) { + entity.setIsSelected(true); + } + selectedEntity = entity; + invalidate(); + if (updateCallback && motionViewCallback != null) { + motionViewCallback.onEntitySelected(entity); + } + } + + public void unselectEntity() { + if (selectedEntity != null) { + selectEntity(null, false); + } + } + + @Nullable + private MotionEntity findEntityAtPoint(float x, float y) { + MotionEntity selected = null; + PointF p = new PointF(x, y); + for (int i = entities.size() - 1; i >= 0; i--) { + if (entities.get(i).pointInLayerRect(p)) { + selected = entities.get(i); + break; + } + } + return selected; + } + + private void updateSelectionOnTap(MotionEvent e) { + MotionEntity entity = findEntityAtPoint(e.getX(), e.getY()); + selectEntity(entity, true); + } + + private void updateOnLongPress(MotionEvent e) { + // if layer is currently selected and point inside layer - move it to front + if (selectedEntity != null) { + PointF p = new PointF(e.getX(), e.getY()); + if (selectedEntity.pointInLayerRect(p)) { + bringLayerToFront(selectedEntity); + } + } + } + + private void bringLayerToFront(@NonNull MotionEntity entity) { + // removing and adding brings layer to front + if (entities.remove(entity)) { + entities.add(entity); + invalidate(); + } + } + + private void moveEntityToBack(@Nullable MotionEntity entity) { + if (entity == null) { + return; + } + if (entities.remove(entity)) { + entities.add(0, entity); + invalidate(); + } + } + + public void flipSelectedEntity() { + if (selectedEntity == null) { + return; + } + selectedEntity.getLayer().flip(); + invalidate(); + } + + public void moveSelectedBack() { + moveEntityToBack(selectedEntity); + } + + public void deletedSelectedEntity() { + if (selectedEntity == null) { + return; + } + if (entities.remove(selectedEntity)) { + selectedEntity.release(); + selectedEntity = null; + invalidate(); + } + } + + // memory + public void release() { + for (MotionEntity entity : entities) { + entity.release(); + } + } + + // gesture detectors + + private final View.OnTouchListener onTouchListener = new View.OnTouchListener() { + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (scaleGestureDetector != null) { + scaleGestureDetector.onTouchEvent(event); + rotateGestureDetector.onTouchEvent(event); + moveGestureDetector.onTouchEvent(event); + gestureDetectorCompat.onTouchEvent(event); + } + return true; + } + }; + + private class TapsListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onDoubleTap(MotionEvent e) { + if (motionViewCallback != null && selectedEntity != null) { + motionViewCallback.onEntityDoubleTap(selectedEntity); + } + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + updateOnLongPress(e); + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + updateSelectionOnTap(e); + return true; + } + } + + private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScale(ScaleGestureDetector detector) { + if (selectedEntity != null) { + float scaleFactorDiff = detector.getScaleFactor(); + Log.w(TAG, "ScaleFactorDiff: " + scaleFactorDiff); + selectedEntity.getLayer().postScale(scaleFactorDiff - 1.0F); + selectedEntity.updateEntity(); + updateUI(); + } + return true; + } + } + + private class RotateListener extends RotateGestureDetector.SimpleOnRotateGestureListener { + @Override + public boolean onRotate(RotateGestureDetector detector) { + if (selectedEntity != null) { + selectedEntity.getLayer().postRotate(-detector.getRotationDegreesDelta()); + updateUI(); + } + return true; + } + } + + private class MoveListener extends MoveGestureDetector.SimpleOnMoveGestureListener { + @Override + public boolean onMove(MoveGestureDetector detector) { + handleTranslate(detector.getFocusDelta()); + return true; + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + String text = s.toString(); + MotionEntity entity = getSelectedEntity(); + + if (entity != null && entity instanceof TextEntity) { + TextEntity textEntity = (TextEntity)entity; + + if (!textEntity.getLayer().getText().equals(text)) { + textEntity.getLayer().setText(text); + textEntity.updateEntity(); + MotionView.this.invalidate(); + } + } + } + +} diff --git a/src/org/thoughtcrime/securesms/scribbles/widget/ScribbleView.java b/src/org/thoughtcrime/securesms/scribbles/widget/ScribbleView.java new file mode 100644 index 0000000000..4c205e6b8c --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/widget/ScribbleView.java @@ -0,0 +1,190 @@ +/** + * Copyright (C) 2016 Open Whisper Systems + * + * 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 org.thoughtcrime.securesms.scribbles.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; +import org.thoughtcrime.securesms.scribbles.widget.entity.MotionEntity; +import org.thoughtcrime.securesms.scribbles.widget.entity.TextEntity; +import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; +import org.thoughtcrime.securesms.util.concurrent.SettableFuture; + +import java.util.concurrent.ExecutionException; + +public class ScribbleView extends FrameLayout { + + private static final String TAG = ScribbleView.class.getSimpleName(); + + private ImageView imageView; + private MotionView motionView; + private CanvasView canvasView; + + private @Nullable Uri imageUri; + private @Nullable MasterSecret masterSecret; + + public ScribbleView(Context context) { + super(context); + initialize(context); + } + + public ScribbleView(Context context, AttributeSet attrs) { + super(context, attrs); + initialize(context); + } + + public ScribbleView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initialize(context); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public ScribbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initialize(context); + } + + public void setImage(@NonNull Uri uri, @NonNull MasterSecret masterSecret) { + this.imageUri = uri; + this.masterSecret = masterSecret; + + Glide.with(getContext()) + .load(new DecryptableUri(masterSecret, uri)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(imageView); + } + + public @NonNull ListenableFuture getRenderedImage() { + final SettableFuture future = new SettableFuture<>(); + final Context context = getContext(); + + if (imageUri == null || masterSecret == null) { + future.set(null); + return future; + } + + new AsyncTask() { + @Override + protected @Nullable Bitmap doInBackground(Void... params) { + try { + return Glide.with(context) + .load(new DecryptableUri(masterSecret, imageUri)) + .asBitmap() + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .into(-1, -1) + .get(); + } catch (InterruptedException | ExecutionException e) { + Log.w(TAG, e); + return null; + } + } + + @Override + protected void onPostExecute(@Nullable Bitmap bitmap) { + if (bitmap == null) { + future.set(null); + return; + } + + Canvas canvas = new Canvas(bitmap); + motionView.render(canvas); + canvasView.render(canvas); + future.set(bitmap); + } + }.execute(); + + return future; + } + + private void initialize(@NonNull Context context) { + inflate(context, R.layout.scribble_view, this); + + this.imageView = (ImageView) findViewById(R.id.image_view); + this.motionView = (MotionView) findViewById(R.id.motion_view); + this.canvasView = (CanvasView) findViewById(R.id.canvas_view); + } + + public void setMotionViewCallback(MotionView.MotionViewCallback callback) { + this.motionView.setMotionViewCallback(callback); + } + + public void setDrawingMode(boolean enabled) { + this.canvasView.setActive(enabled); + if (enabled) this.motionView.unselectEntity(); + } + + public void setDrawingBrushColor(int color) { + this.canvasView.setPaintFillColor(color); + this.canvasView.setPaintStrokeColor(color); + } + + public void addEntityAndPosition(MotionEntity entity) { + this.motionView.addEntityAndPosition(entity); + } + + public MotionEntity getSelectedEntity() { + return this.motionView.getSelectedEntity(); + } + + public void deleteSelected() { + this.motionView.deletedSelectedEntity(); + } + + public void clearSelection() { + this.motionView.unselectEntity(); + } + + public void undoDrawing() { + this.canvasView.undo(); + } + + public void startEditing(TextEntity entity) { + this.motionView.startEditing(entity); + } + + @Override + public void onMeasure(int width, int height) { + super.onMeasure(width, height); + + setMeasuredDimension(imageView.getMeasuredWidth(), imageView.getMeasuredHeight()); + + canvasView.measure(MeasureSpec.makeMeasureSpec(imageView.getMeasuredWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(imageView.getMeasuredHeight(), MeasureSpec.EXACTLY)); + + motionView.measure(MeasureSpec.makeMeasureSpec(imageView.getMeasuredWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(imageView.getMeasuredHeight(), MeasureSpec.EXACTLY)); + } + +} diff --git a/src/org/thoughtcrime/securesms/scribbles/widget/VerticalSlideColorPicker.java b/src/org/thoughtcrime/securesms/scribbles/widget/VerticalSlideColorPicker.java new file mode 100644 index 0000000000..ab6f0d5da5 --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/widget/VerticalSlideColorPicker.java @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2016 Mark Charles + * Copyright (c) 2016 Open Whisper Systems + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.thoughtcrime.securesms.scribbles.widget; + + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Shader; +import android.os.Build; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import org.thoughtcrime.securesms.R; + +public class VerticalSlideColorPicker extends View { + + private Paint paint; + private Paint strokePaint; + private Path path; + private Bitmap bitmap; + private Canvas bitmapCanvas; + + private int viewWidth; + private int viewHeight; + private int centerX; + private float colorPickerRadius; + private RectF colorPickerBody; + + private OnColorChangeListener onColorChangeListener; + + private int borderColor; + private float borderWidth; + private int[] colors; + + public VerticalSlideColorPicker(Context context) { + super(context); + init(); + } + + public VerticalSlideColorPicker(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.VerticalSlideColorPicker, 0, 0); + + try { + int colorsResourceId = a.getResourceId(R.styleable.VerticalSlideColorPicker_pickerColors, R.array.scribble_colors); + + colors = a.getResources().getIntArray(colorsResourceId); + borderColor = a.getColor(R.styleable.VerticalSlideColorPicker_pickerBorderColor, Color.WHITE); + borderWidth = a.getDimension(R.styleable.VerticalSlideColorPicker_pickerBorderWidth, 10f); + + } finally { + a.recycle(); + } + + init(); + } + + public VerticalSlideColorPicker(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public VerticalSlideColorPicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + setWillNotDraw(false); + + paint = new Paint(); + paint.setStyle(Paint.Style.FILL); + paint.setAntiAlias(true); + + path = new Path(); + + strokePaint = new Paint(); + strokePaint.setStyle(Paint.Style.STROKE); + strokePaint.setColor(borderColor); + strokePaint.setAntiAlias(true); + strokePaint.setStrokeWidth(borderWidth); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + path.addCircle(centerX, borderWidth + colorPickerRadius, colorPickerRadius, Path.Direction.CW); + path.addRect(colorPickerBody, Path.Direction.CW); + path.addCircle(centerX, viewHeight - (borderWidth + colorPickerRadius), colorPickerRadius, Path.Direction.CW); + + bitmapCanvas.drawColor(Color.TRANSPARENT); + + bitmapCanvas.drawPath(path, strokePaint); + bitmapCanvas.drawPath(path, paint); + + canvas.drawBitmap(bitmap, 0, 0, null); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + + float yPos = Math.min(event.getY(), colorPickerBody.bottom); + yPos = Math.max(colorPickerBody.top, yPos); + + int selectedColor = bitmap.getPixel(viewWidth/2, (int) yPos); + + if (onColorChangeListener != null) { + onColorChangeListener.onColorChange(selectedColor); + } + + return true; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + viewWidth = w; + viewHeight = h; + + centerX = viewWidth / 2; + colorPickerRadius = (viewWidth / 2) - borderWidth; + + colorPickerBody = new RectF(centerX - colorPickerRadius, borderWidth + colorPickerRadius, centerX + colorPickerRadius, viewHeight - (borderWidth + colorPickerRadius)); + + LinearGradient gradient = new LinearGradient(0, colorPickerBody.top, 0, colorPickerBody.bottom, colors, null, Shader.TileMode.CLAMP); + paint.setShader(gradient); + + if (bitmap != null) { + bitmap.recycle(); + } + + bitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888); + bitmapCanvas = new Canvas(bitmap); + + resetToDefault(); + } + + public void setBorderColor(int borderColor) { + this.borderColor = borderColor; + invalidate(); + } + + public void setBorderWidth(float borderWidth) { + this.borderWidth = borderWidth; + invalidate(); + } + + public void setColors(int[] colors) { + this.colors = colors; + invalidate(); + } + + public void resetToDefault() { + if (onColorChangeListener != null) { + onColorChangeListener.onColorChange(Color.RED); + } + + invalidate(); + } + + public void setOnColorChangeListener(OnColorChangeListener onColorChangeListener) { + this.onColorChangeListener = onColorChangeListener; + } + + public interface OnColorChangeListener { + + void onColorChange(int selectedColor); + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/scribbles/widget/entity/ImageEntity.java b/src/org/thoughtcrime/securesms/scribbles/widget/entity/ImageEntity.java new file mode 100644 index 0000000000..23bc3c619f --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/widget/entity/ImageEntity.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2016 UPTech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.thoughtcrime.securesms.scribbles.widget.entity; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.support.annotation.IntRange; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.thoughtcrime.securesms.scribbles.viewmodel.Layer; + + +public class ImageEntity extends MotionEntity { + + @NonNull + private final Bitmap bitmap; + + public ImageEntity(@NonNull Layer layer, + @NonNull Bitmap bitmap, + @IntRange(from = 1) int canvasWidth, + @IntRange(from = 1) int canvasHeight) { + super(layer, canvasWidth, canvasHeight); + + this.bitmap = bitmap; + float width = bitmap.getWidth(); + float height = bitmap.getHeight(); + + float widthAspect = 1.0F * canvasWidth / width; + float heightAspect = 1.0F * canvasHeight / height; + // fit the smallest size + holyScale = Math.min(widthAspect, heightAspect); + + // initial position of the entity + srcPoints[0] = 0; srcPoints[1] = 0; + srcPoints[2] = width; srcPoints[3] = 0; + srcPoints[4] = width; srcPoints[5] = height; + srcPoints[6] = 0; srcPoints[7] = height; + srcPoints[8] = 0; srcPoints[8] = 0; + } + + @Override + public void drawContent(@NonNull Canvas canvas, @Nullable Paint drawingPaint) { + canvas.drawBitmap(bitmap, matrix, drawingPaint); + } + + @Override + public int getWidth() { + return bitmap.getWidth(); + } + + @Override + public int getHeight() { + return bitmap.getHeight(); + } + + @Override + public void release() { + if (!bitmap.isRecycled()) { + bitmap.recycle(); + } + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/scribbles/widget/entity/MotionEntity.java b/src/org/thoughtcrime/securesms/scribbles/widget/entity/MotionEntity.java new file mode 100644 index 0000000000..8c72e922c0 --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/widget/entity/MotionEntity.java @@ -0,0 +1,290 @@ +/** + * Copyright (c) 2016 UPTech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.thoughtcrime.securesms.scribbles.widget.entity; + +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PointF; +import android.support.annotation.IntRange; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.thoughtcrime.securesms.util.MathUtils; +import org.thoughtcrime.securesms.scribbles.viewmodel.Layer; + + +@SuppressWarnings({"WeakerAccess"}) +public abstract class MotionEntity { + + /** + * data + */ + @NonNull + protected final Layer layer; + + /** + * transformation matrix for the entity + */ + protected final Matrix matrix = new Matrix(); + /** + * true - entity is selected and need to draw it's border + * false - not selected, no need to draw it's border + */ + private boolean isSelected; + + /** + * maximum scale of the initial image, so that + * the entity still fits within the parent canvas + */ + protected float holyScale; + + /** + * width of canvas the entity is drawn in + */ + @IntRange(from = 0) + protected int canvasWidth; + /** + * height of canvas the entity is drawn in + */ + @IntRange(from = 0) + protected int canvasHeight; + + /** + * Destination points of the entity + * 5 points. Size of array - 10; Starting upper left corner, clockwise + * last point is the same as first to close the circle + * NOTE: saved as a field variable in order to avoid creating array in draw()-like methods + */ + private final float[] destPoints = new float[10]; // x0, y0, x1, y1, x2, y2, x3, y3, x0, y0 + /** + * Initial points of the entity + * @see #destPoints + */ + protected final float[] srcPoints = new float[10]; // x0, y0, x1, y1, x2, y2, x3, y3, x0, y0 + + @NonNull + private Paint borderPaint = new Paint(); + + public MotionEntity(@NonNull Layer layer, + @IntRange(from = 1) int canvasWidth, + @IntRange(from = 1) int canvasHeight) { + this.layer = layer; + this.canvasWidth = canvasWidth; + this.canvasHeight = canvasHeight; + } + + private boolean isSelected() { + return isSelected; + } + + public void setIsSelected(boolean isSelected) { + this.isSelected = isSelected; + } + + /** + * S - scale matrix, R - rotate matrix, T - translate matrix, + * L - result transformation matrix + *

+ * The correct order of applying transformations is : L = S * R * T + *

+ * See more info: Game Dev: Transform Matrix multiplication order + *

+ * Preconcat works like M` = M * S, so we apply preScale -> preRotate -> preTranslate + * the result will be the same: L = S * R * T + *

+ * NOTE: postconcat (postScale, etc.) works the other way : M` = S * M, in order to use it + * we'd need to reverse the order of applying + * transformations : post holy scale -> postTranslate -> postRotate -> postScale + */ + protected void updateMatrix() { + // init matrix to E - identity matrix + matrix.reset(); + + float widthAspect = 1.0F * canvasWidth / getWidth(); + float heightAspect = 1.0F * canvasHeight / getHeight(); + // fit the smallest size + holyScale = Math.min(widthAspect, heightAspect); + + float topLeftX = layer.getX() * canvasWidth; + float topLeftY = layer.getY() * canvasHeight; + + float centerX = topLeftX + getWidth() * holyScale * 0.5F; + float centerY = topLeftY + getHeight() * holyScale * 0.5F; + + // calculate params + float rotationInDegree = layer.getRotationInDegrees(); + float scaleX = layer.getScale(); + float scaleY = layer.getScale(); + if (layer.isFlipped()) { + // flip (by X-coordinate) if needed + rotationInDegree *= -1.0F; + scaleX *= -1.0F; + } + + // applying transformations : L = S * R * T + + // scale + matrix.preScale(scaleX, scaleY, centerX, centerY); + + // rotate + matrix.preRotate(rotationInDegree, centerX, centerY); + + // translate + matrix.preTranslate(topLeftX, topLeftY); + + // applying holy scale - S`, the result will be : L = S * R * T * S` + matrix.preScale(holyScale, holyScale); + } + + public float absoluteCenterX() { + float topLeftX = layer.getX() * canvasWidth; + return topLeftX + getWidth() * holyScale * 0.5F; + } + + public float absoluteCenterY() { + float topLeftY = layer.getY() * canvasHeight; + + return topLeftY + getHeight() * holyScale * 0.5F; + } + + public PointF absoluteCenter() { + float topLeftX = layer.getX() * canvasWidth; + float topLeftY = layer.getY() * canvasHeight; + + float centerX = topLeftX + getWidth() * holyScale * 0.5F; + float centerY = topLeftY + getHeight() * holyScale * 0.5F; + + return new PointF(centerX, centerY); + } + + public void moveToCanvasCenter() { + moveCenterTo(new PointF(canvasWidth * 0.5F, canvasHeight * 0.5F)); + } + + public void moveCenterTo(PointF moveToCenter) { + PointF currentCenter = absoluteCenter(); + layer.postTranslate(1.0F * (moveToCenter.x - currentCenter.x) / canvasWidth, + 1.0F * (moveToCenter.y - currentCenter.y) / canvasHeight); + } + + private final PointF pA = new PointF(); + private final PointF pB = new PointF(); + private final PointF pC = new PointF(); + private final PointF pD = new PointF(); + + /** + * For more info: + * StackOverflow: How to check point is in rectangle + *

NOTE: it's easier to apply the same transformation matrix (calculated before) to the original source points, rather than + * calculate the result points ourselves + * @param point point + * @return true if point (x, y) is inside the triangle + */ + public boolean pointInLayerRect(PointF point) { + + updateMatrix(); + // map rect vertices + matrix.mapPoints(destPoints, srcPoints); + + pA.x = destPoints[0]; + pA.y = destPoints[1]; + pB.x = destPoints[2]; + pB.y = destPoints[3]; + pC.x = destPoints[4]; + pC.y = destPoints[5]; + pD.x = destPoints[6]; + pD.y = destPoints[7]; + + return MathUtils.pointInTriangle(point, pA, pB, pC) || MathUtils.pointInTriangle(point, pA, pD, pC); + } + + /** + * http://judepereira.com/blog/calculate-the-real-scale-factor-and-the-angle-of-rotation-from-an-android-matrix/ + * + * @param canvas Canvas to draw + * @param drawingPaint Paint to use during drawing + */ + public final void draw(@NonNull Canvas canvas, @Nullable Paint drawingPaint) { + + this.canvasWidth = canvas.getWidth(); + this.canvasHeight = canvas.getHeight(); + + updateMatrix(); + + canvas.save(); + + drawContent(canvas, drawingPaint); + + if (isSelected()) { + // get alpha from drawingPaint + int storedAlpha = borderPaint.getAlpha(); + if (drawingPaint != null) { + borderPaint.setAlpha(drawingPaint.getAlpha()); + } + drawSelectedBg(canvas); + // restore border alpha + borderPaint.setAlpha(storedAlpha); + } + + canvas.restore(); + } + + private void drawSelectedBg(Canvas canvas) { + matrix.mapPoints(destPoints, srcPoints); + //noinspection Range + canvas.drawLines(destPoints, 0, 8, borderPaint); + //noinspection Range + canvas.drawLines(destPoints, 2, 8, borderPaint); + } + + @NonNull + public Layer getLayer() { + return layer; + } + + public void setBorderPaint(@NonNull Paint borderPaint) { + this.borderPaint = borderPaint; + } + + protected abstract void drawContent(@NonNull Canvas canvas, @Nullable Paint drawingPaint); + + public abstract int getWidth(); + + public abstract int getHeight(); + + public void release() { + // free resources here + } + + public void updateEntity() {} + + @Override + protected void finalize() throws Throwable { + try { + release(); + } finally { + //noinspection ThrowFromFinallyBlock + super.finalize(); + } + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/scribbles/widget/entity/TextEntity.java b/src/org/thoughtcrime/securesms/scribbles/widget/entity/TextEntity.java new file mode 100644 index 0000000000..73bfd315ea --- /dev/null +++ b/src/org/thoughtcrime/securesms/scribbles/widget/entity/TextEntity.java @@ -0,0 +1,189 @@ +/** + * Copyright (c) 2016 UPTech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.thoughtcrime.securesms.scribbles.widget.entity; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.support.annotation.IntRange; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; + +import org.thoughtcrime.securesms.scribbles.viewmodel.TextLayer; + + +public class TextEntity extends MotionEntity { + + private final TextPaint textPaint; + + @Nullable + private Bitmap bitmap; + + public TextEntity(@NonNull TextLayer textLayer, + @IntRange(from = 1) int canvasWidth, + @IntRange(from = 1) int canvasHeight) + { + super(textLayer, canvasWidth, canvasHeight); + this.textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + + updateEntity(false); + } + + private void updateEntity(boolean moveToPreviousCenter) { + // save previous center + PointF oldCenter = absoluteCenter(); + + Bitmap newBmp = createBitmap(getLayer(), bitmap); + + // recycle previous bitmap (if not reused) as soon as possible + if (bitmap != null && bitmap != newBmp && !bitmap.isRecycled()) { + bitmap.recycle(); + } + + this.bitmap = newBmp; + + float width = bitmap.getWidth(); + float height = bitmap.getHeight(); + + @SuppressWarnings("UnnecessaryLocalVariable") + float widthAspect = 1.0F * canvasWidth / width; + + // for text we always match text width with parent width + this.holyScale = widthAspect; + + // initial position of the entity + srcPoints[0] = 0; + srcPoints[1] = 0; + srcPoints[2] = width; + srcPoints[3] = 0; + srcPoints[4] = width; + srcPoints[5] = height; + srcPoints[6] = 0; + srcPoints[7] = height; + srcPoints[8] = 0; + srcPoints[8] = 0; + + if (moveToPreviousCenter) { + // move to previous center + moveCenterTo(oldCenter); + } + } + + /** + * If reuseBmp is not null, and size of the new bitmap matches the size of the reuseBmp, + * new bitmap won't be created, reuseBmp it will be reused instead + * + * @param textLayer text to draw + * @param reuseBmp the bitmap that will be reused + * @return bitmap with the text + */ + @NonNull + private Bitmap createBitmap(@NonNull TextLayer textLayer, @Nullable Bitmap reuseBmp) { + + int boundsWidth = canvasWidth; + + // init params - size, color, typeface + textPaint.setStyle(Paint.Style.FILL); + textPaint.setTextSize(textLayer.getFont().getSize() * canvasWidth); + textPaint.setColor(textLayer.getFont().getColor()); +// textPaint.setTypeface(fontProvider.getTypeface(textLayer.getFont().getTypeface())); + + // drawing text guide : http://ivankocijan.xyz/android-drawing-multiline-text-on-canvas/ + // Static layout which will be drawn on canvas + StaticLayout sl = new StaticLayout( + textLayer.getText(), // - text which will be drawn + textPaint, + boundsWidth, // - width of the layout + Layout.Alignment.ALIGN_CENTER, // - layout alignment + 1, // 1 - text spacing multiply + 1, // 1 - text spacing add + true); // true - include padding + + // calculate height for the entity, min - Limits.MIN_BITMAP_HEIGHT + int boundsHeight = sl.getHeight(); + + // create bitmap not smaller than TextLayer.Limits.MIN_BITMAP_HEIGHT + int bmpHeight = (int) (canvasHeight * Math.max(TextLayer.Limits.MIN_BITMAP_HEIGHT, + 1.0F * boundsHeight / canvasHeight)); + + // create bitmap where text will be drawn + Bitmap bmp; + if (reuseBmp != null && reuseBmp.getWidth() == boundsWidth + && reuseBmp.getHeight() == bmpHeight) { + // if previous bitmap exists, and it's width/height is the same - reuse it + bmp = reuseBmp; + bmp.eraseColor(Color.TRANSPARENT); // erase color when reusing + } else { + bmp = Bitmap.createBitmap(boundsWidth, bmpHeight, Bitmap.Config.ARGB_8888); + } + + Canvas canvas = new Canvas(bmp); + canvas.save(); + + // move text to center if bitmap is bigger that text + if (boundsHeight < bmpHeight) { + //calculate Y coordinate - In this case we want to draw the text in the + //center of the canvas so we move Y coordinate to center. + float textYCoordinate = (bmpHeight - boundsHeight) / 2; + canvas.translate(0, textYCoordinate); + } + + //draws static layout on canvas + sl.draw(canvas); + canvas.restore(); + + return bmp; + } + + @Override + @NonNull + public TextLayer getLayer() { + return (TextLayer) layer; + } + + @Override + protected void drawContent(@NonNull Canvas canvas, @Nullable Paint drawingPaint) { + if (bitmap != null) { + canvas.drawBitmap(bitmap, matrix, drawingPaint); + } + } + + @Override + public int getWidth() { + return bitmap != null ? bitmap.getWidth() : 0; + } + + @Override + public int getHeight() { + return bitmap != null ? bitmap.getHeight() : 0; + } + + @Override + public void updateEntity() { + updateEntity(true); + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/util/MathUtils.java b/src/org/thoughtcrime/securesms/util/MathUtils.java new file mode 100644 index 0000000000..15a049dfeb --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/MathUtils.java @@ -0,0 +1,54 @@ +package org.thoughtcrime.securesms.util; + +import android.graphics.PointF; +import android.support.annotation.NonNull; + +public class MathUtils { + + /** + * For more info: + * StackOverflow: How to check point is in rectangle + * + * @param pt point to check + * @param v1 vertex 1 of the triangle + * @param v2 vertex 2 of the triangle + * @param v3 vertex 3 of the triangle + * @return true if point (x, y) is inside the triangle + */ + public static boolean pointInTriangle(@NonNull PointF pt, @NonNull PointF v1, + @NonNull PointF v2, @NonNull PointF v3) { + + boolean b1 = crossProduct(pt, v1, v2) < 0.0f; + boolean b2 = crossProduct(pt, v2, v3) < 0.0f; + boolean b3 = crossProduct(pt, v3, v1) < 0.0f; + + return (b1 == b2) && (b2 == b3); + } + + /** + * calculates cross product of vectors AB and AC + * + * @param a beginning of 2 vectors + * @param b end of vector 1 + * @param c enf of vector 2 + * @return cross product AB * AC + */ + private static float crossProduct(@NonNull PointF a, @NonNull PointF b, @NonNull PointF c) { + return crossProduct(a.x, a.y, b.x, b.y, c.x, c.y); + } + + /** + * calculates cross product of vectors AB and AC + * + * @param ax X coordinate of point A + * @param ay Y coordinate of point A + * @param bx X coordinate of point B + * @param by Y coordinate of point B + * @param cx X coordinate of point C + * @param cy Y coordinate of point C + * @return cross product AB * AC + */ + private static float crossProduct(float ax, float ay, float bx, float by, float cx, float cy) { + return (ax - cx) * (by - cy) - (bx - cx) * (ay - cy); + } +} \ No newline at end of file