├── .gitignore ├── Jenkinsfile ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── team │ │ └── uptech │ │ └── motionviews │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── fonts │ │ │ ├── Arial.ttf │ │ │ ├── Eutemia.ttf │ │ │ ├── GREENPIL.ttf │ │ │ ├── Grinched.ttf │ │ │ ├── Helvetica.ttf │ │ │ ├── Libertango.ttf │ │ │ ├── MetalMacabre.ttf │ │ │ ├── ParryHotter.ttf │ │ │ ├── SCRIPTIN.ttf │ │ │ ├── TheGodfather_v2.ttf │ │ │ ├── akaDora.ttf │ │ │ └── waltograph42.ttf │ ├── java │ │ ├── com │ │ │ └── almeros │ │ │ │ └── android │ │ │ │ └── multitouch │ │ │ │ ├── BaseGestureDetector.java │ │ │ │ ├── MoveGestureDetector.java │ │ │ │ ├── RotateGestureDetector.java │ │ │ │ ├── ShoveGestureDetector.java │ │ │ │ └── TwoFingerGestureDetector.java │ │ └── team │ │ │ └── uptech │ │ │ └── motionviews │ │ │ ├── ui │ │ │ ├── MainActivity.java │ │ │ ├── StickerSelectActivity.java │ │ │ ├── TextEditorDialogFragment.java │ │ │ └── adapter │ │ │ │ └── FontsAdapter.java │ │ │ ├── utils │ │ │ ├── FontProvider.java │ │ │ └── MathUtils.java │ │ │ ├── viewmodel │ │ │ ├── Font.java │ │ │ ├── Layer.java │ │ │ └── TextLayer.java │ │ │ └── widget │ │ │ ├── MotionView.java │ │ │ └── entity │ │ │ ├── ImageEntity.java │ │ │ ├── MotionEntity.java │ │ │ └── TextEntity.java │ └── res │ │ ├── drawable-anydpi │ │ ├── ic_add.xml │ │ ├── ic_add_text.xml │ │ ├── ic_format_color_text.xml │ │ ├── ic_mode_edit.xml │ │ ├── ic_neg_1.xml │ │ ├── ic_plus_1.xml │ │ └── ic_text_fields.xml │ │ ├── drawable-nodpi │ │ ├── abra.png │ │ ├── bellsprout.png │ │ ├── bracelet.png │ │ ├── bullbasaur.png │ │ ├── camera.png │ │ ├── candy.png │ │ ├── caterpie.png │ │ ├── charmander.png │ │ ├── mankey.png │ │ ├── map.png │ │ ├── mega_ball.png │ │ ├── meowth.png │ │ ├── pawprints.png │ │ ├── pidgey.png │ │ ├── pikachu.png │ │ ├── pikachu_1.png │ │ ├── pikachu_2.png │ │ ├── player.png │ │ ├── pointer.png │ │ ├── pokebag.png │ │ ├── pokeball.png │ │ ├── pokeballs.png │ │ ├── pokecoin.png │ │ ├── pokedex.png │ │ ├── potion.png │ │ ├── psyduck.png │ │ ├── rattata.png │ │ ├── revive.png │ │ ├── squirtle.png │ │ ├── star.png │ │ ├── star_1.png │ │ ├── superball.png │ │ ├── tornado.png │ │ ├── venonat.png │ │ ├── weedle.png │ │ └── zubat.png │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── select_sticker_activity.xml │ │ ├── sticker_item.xml │ │ └── text_editor_layout.xml │ │ ├── menu │ │ └── main_menu.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── team │ └── uptech │ └── motionviews │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/android,java,intellij,macos,linux,windows 3 | 4 | ### Android ### 5 | # Built application files 6 | *.apk 7 | *.ap_ 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # Intellij 40 | *.iml 41 | .idea/* 42 | .idea/workspace.xml 43 | .idea/libraries 44 | 45 | # Keystore files 46 | *.jks 47 | 48 | ### Android Patch ### 49 | gen-external-apklibs 50 | 51 | 52 | ### Intellij ### 53 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 54 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 55 | 56 | # User-specific stuff: 57 | .idea/workspace.xml 58 | .idea/tasks.xml 59 | .idea/dictionaries 60 | .idea/vcs.xml 61 | .idea/jsLibraryMappings.xml 62 | 63 | # Sensitive or high-churn files: 64 | .idea/dataSources.ids 65 | .idea/dataSources.xml 66 | .idea/dataSources.local.xml 67 | .idea/sqlDataSources.xml 68 | .idea/dynamic.xml 69 | .idea/uiDesigner.xml 70 | 71 | # Gradle: 72 | .idea/gradle.xml 73 | .idea/libraries 74 | 75 | # Mongo Explorer plugin: 76 | .idea/mongoSettings.xml 77 | 78 | ## File-based project format: 79 | *.iws 80 | 81 | ## Plugin-specific files: 82 | 83 | # IntelliJ 84 | /out/ 85 | 86 | # mpeltonen/sbt-idea plugin 87 | .idea_modules/ 88 | 89 | # JIRA plugin 90 | atlassian-ide-plugin.xml 91 | 92 | # Crashlytics plugin (for Android Studio and IntelliJ) 93 | com_crashlytics_export_strings.xml 94 | crashlytics.properties 95 | crashlytics-build.properties 96 | fabric.properties 97 | 98 | ### Intellij Patch ### 99 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 100 | 101 | # *.iml 102 | # modules.xml 103 | # .idea/misc.xml 104 | # *.ipr 105 | 106 | 107 | ### macOS ### 108 | *.DS_Store 109 | .AppleDouble 110 | .LSOverride 111 | 112 | # Icon must end with two \r 113 | Icon 114 | 115 | 116 | # Thumbnails 117 | ._* 118 | 119 | # Files that might appear in the root of a volume 120 | .DocumentRevisions-V100 121 | .fseventsd 122 | .Spotlight-V100 123 | .TemporaryItems 124 | .Trashes 125 | .VolumeIcon.icns 126 | .com.apple.timemachine.donotpresent 127 | 128 | # Directories potentially created on remote AFP share 129 | .AppleDB 130 | .AppleDesktop 131 | Network Trash Folder 132 | Temporary Items 133 | .apdisk 134 | 135 | 136 | ### Linux ### 137 | *~ 138 | 139 | # temporary files which can be created if a process still has a handle open of a deleted file 140 | .fuse_hidden* 141 | 142 | # KDE directory preferences 143 | .directory 144 | 145 | # Linux trash folder which might appear on any partition or disk 146 | .Trash-* 147 | 148 | # .nfs files are created when an open file is removed but is still being accessed 149 | .nfs* 150 | 151 | 152 | ### Windows ### 153 | # Windows image file caches 154 | Thumbs.db 155 | ehthumbs.db 156 | 157 | # Folder config file 158 | Desktop.ini 159 | 160 | # Recycle Bin used on file shares 161 | $RECYCLE.BIN/ 162 | 163 | # Windows Installer files 164 | *.cab 165 | *.msi 166 | *.msm 167 | *.msp 168 | 169 | # Windows shortcuts 170 | *.lnk 171 | 172 | 173 | ### Java ### 174 | *.class 175 | 176 | # Mobile Tools for Java (J2ME) 177 | .mtj.tmp/ 178 | 179 | # Package Files # 180 | *.jar 181 | *.war 182 | *.ear 183 | 184 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 185 | hs_err_pid* -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy​ 2 | pipeline { 3 | agent any 4 | stages { 5 | stage('Checkout') { steps { checkout scm } } 6 | 7 | stage("Test") { 8 | steps { 9 | echo "Test" 10 | } 11 | } 12 | 13 | stage("Deploy") { 14 | when { branch 'master' } 15 | steps { 16 | echo "Deploy" 17 | } 18 | } 19 | 20 | } 21 | 22 | 23 | post { 24 | success { 25 | echo "Build succeeded." 26 | } 27 | 28 | failure { 29 | echo "Build failed." 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 UPTech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MotionViews-Android 2 | 3 | ![alt tag](http://i.giphy.com/3o7TKJhBZiimAe6JDG.gif) 4 | 5 | ## Code Guide : How to create Snapchat-like image stickers and text stickers 6 | 7 | After spending 2000+ hours and releasing 4+ successful apps working with 8 | image transformations, we’ve decided to share our experience with the community. 9 | 10 | ## Task 11 | 12 | So the task is pretty simple: **add the ability to move, scale and rotate stickers on Android**. 13 | 14 | Even though it sounds easy, there are a couple of challenges as well. 15 | First, there is a zillion of screen sizes of Android devices, and we’d better 16 | support them all (or as many as we can). Moreover, it could be the case 17 | that you would need to enable users to save/edit their selfies. And if 18 | they open their custom works on other devices — the screen size might 19 | change, the loaded images might be of a different quality, etc. 20 | 21 | As you might have guessed, the task is getting more complicated now. 22 | 23 | **The solution needs to work on different screen sizes and be independent of the image quality**. 24 | 25 | **In the second part we've also added an ability to create text stickers, 26 | update them, and manipulate in the same way as with image stickers**. 27 | 28 | ## Solution 29 | 30 | **MotionViews-Android** - is fully functional app that meets the requirements. 31 | 32 | Check the Medium articles [How to create Snapchat-like stickers for Android](https://medium.com/uptech-team/how-to-create-snapchat-like-stickers-for-android-50512957c351) 33 | and [How to create beautiful text stickers for Android](https://medium.com/uptech-team/how-to-create-beautiful-text-stickers-for-android-10eeea0cee09) about the details of the implementation. 34 | 35 | Feel free to use the code for your own purposes. 36 | 37 | Check out the app on [Google Play](https://play.google.com/store/apps/details?id=team.uptech.motionviews). 38 | 39 | Play with the online app emulator on [Appetize.io](https://appetize.io/app/kd51amwzp7fg4f8wrrb5mz673w). 40 | 41 | The video of what we got in the end on the YouTube: [Image Stickers](https://www.youtube.com/watch?v=6IkmFmlrLPA) and [Text Stickers](https://www.youtube.com/watch?v=9q86Dx9-xTA). -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/android,java,intellij,macos,linux,windows 3 | 4 | ### Android ### 5 | # Built application files 6 | *.apk 7 | *.ap_ 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # Intellij 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/libraries 43 | 44 | # Keystore files 45 | *.jks 46 | 47 | ### Android Patch ### 48 | gen-external-apklibs 49 | 50 | 51 | ### Intellij ### 52 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 53 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 54 | 55 | # User-specific stuff: 56 | .idea/workspace.xml 57 | .idea/tasks.xml 58 | .idea/dictionaries 59 | .idea/vcs.xml 60 | .idea/jsLibraryMappings.xml 61 | 62 | # Sensitive or high-churn files: 63 | .idea/dataSources.ids 64 | .idea/dataSources.xml 65 | .idea/dataSources.local.xml 66 | .idea/sqlDataSources.xml 67 | .idea/dynamic.xml 68 | .idea/uiDesigner.xml 69 | 70 | # Gradle: 71 | .idea/gradle.xml 72 | .idea/libraries 73 | 74 | # Mongo Explorer plugin: 75 | .idea/mongoSettings.xml 76 | 77 | ## File-based project format: 78 | *.iws 79 | 80 | ## Plugin-specific files: 81 | 82 | # IntelliJ 83 | /out/ 84 | 85 | # mpeltonen/sbt-idea plugin 86 | .idea_modules/ 87 | 88 | # JIRA plugin 89 | atlassian-ide-plugin.xml 90 | 91 | # Crashlytics plugin (for Android Studio and IntelliJ) 92 | com_crashlytics_export_strings.xml 93 | crashlytics.properties 94 | crashlytics-build.properties 95 | fabric.properties 96 | 97 | ### Intellij Patch ### 98 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 99 | 100 | # *.iml 101 | # modules.xml 102 | # .idea/misc.xml 103 | # *.ipr 104 | 105 | 106 | ### macOS ### 107 | *.DS_Store 108 | .AppleDouble 109 | .LSOverride 110 | 111 | # Icon must end with two \r 112 | Icon 113 | 114 | 115 | # Thumbnails 116 | ._* 117 | 118 | # Files that might appear in the root of a volume 119 | .DocumentRevisions-V100 120 | .fseventsd 121 | .Spotlight-V100 122 | .TemporaryItems 123 | .Trashes 124 | .VolumeIcon.icns 125 | .com.apple.timemachine.donotpresent 126 | 127 | # Directories potentially created on remote AFP share 128 | .AppleDB 129 | .AppleDesktop 130 | Network Trash Folder 131 | Temporary Items 132 | .apdisk 133 | 134 | 135 | ### Linux ### 136 | *~ 137 | 138 | # temporary files which can be created if a process still has a handle open of a deleted file 139 | .fuse_hidden* 140 | 141 | # KDE directory preferences 142 | .directory 143 | 144 | # Linux trash folder which might appear on any partition or disk 145 | .Trash-* 146 | 147 | # .nfs files are created when an open file is removed but is still being accessed 148 | .nfs* 149 | 150 | 151 | ### Windows ### 152 | # Windows image file caches 153 | Thumbs.db 154 | ehthumbs.db 155 | 156 | # Folder config file 157 | Desktop.ini 158 | 159 | # Recycle Bin used on file shares 160 | $RECYCLE.BIN/ 161 | 162 | # Windows Installer files 163 | *.cab 164 | *.msi 165 | *.msm 166 | *.msp 167 | 168 | # Windows shortcuts 169 | *.lnk 170 | 171 | 172 | ### Java ### 173 | *.class 174 | 175 | # Mobile Tools for Java (J2ME) 176 | .mtj.tmp/ 177 | 178 | # Package Files # 179 | *.jar 180 | *.war 181 | *.ear 182 | 183 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 184 | hs_err_pid* -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | defaultConfig { 7 | applicationId "team.uptech.motionviews" 8 | minSdkVersion 16 9 | targetSdkVersion 25 10 | versionCode 3 11 | versionName "0.3" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | 25 | // support 26 | compile 'com.android.support:appcompat-v7:25.3.1' 27 | compile 'com.android.support:recyclerview-v7:25.3.1' 28 | 29 | // color picker 30 | compile 'com.github.QuadFlask:colorpicker:0.0.13' 31 | 32 | 33 | // test 34 | testCompile 'junit:junit:4.12' 35 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 36 | exclude group: 'com.android.support', module: 'support-annotations' 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/andriybas/Dev/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/team/uptech/motionviews/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("team.uptech.motionviews", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Arial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/assets/fonts/Arial.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Eutemia.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/assets/fonts/Eutemia.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/GREENPIL.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/assets/fonts/GREENPIL.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Grinched.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/assets/fonts/Grinched.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Helvetica.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/assets/fonts/Helvetica.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Libertango.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/assets/fonts/Libertango.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/MetalMacabre.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/assets/fonts/MetalMacabre.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/ParryHotter.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/assets/fonts/ParryHotter.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/SCRIPTIN.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/assets/fonts/SCRIPTIN.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/TheGodfather_v2.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/assets/fonts/TheGodfather_v2.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/akaDora.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/assets/fonts/akaDora.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/waltograph42.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/assets/fonts/waltograph42.ttf -------------------------------------------------------------------------------- /app/src/main/java/com/almeros/android/multitouch/BaseGestureDetector.java: -------------------------------------------------------------------------------- 1 | package com.almeros.android.multitouch; 2 | 3 | import android.content.Context; 4 | import android.view.MotionEvent; 5 | 6 | /** 7 | * @author Almer Thie (code.almeros.com) 8 | * Copyright (c) 2013, Almer Thie (code.almeros.com) 9 | *

10 | * All rights reserved. 11 | *

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

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

18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 19 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 20 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 21 | * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 22 | * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 24 | * OF SUCH DAMAGE. 25 | */ 26 | public abstract class BaseGestureDetector { 27 | /** 28 | * This value is the threshold ratio between the previous combined pressure 29 | * and the current combined pressure. When pressure decreases rapidly 30 | * between events the position values can often be imprecise, as it usually 31 | * indicates that the user is in the process of lifting a pointer off of the 32 | * device. This value was tuned experimentally. 33 | */ 34 | protected static final float PRESSURE_THRESHOLD = 0.67f; 35 | protected final Context mContext; 36 | protected boolean mGestureInProgress; 37 | protected MotionEvent mPrevEvent; 38 | protected MotionEvent mCurrEvent; 39 | protected float mCurrPressure; 40 | protected float mPrevPressure; 41 | protected long mTimeDelta; 42 | 43 | 44 | public BaseGestureDetector(Context context) { 45 | mContext = context; 46 | } 47 | 48 | /** 49 | * All gesture detectors need to be called through this method to be able to 50 | * detect gestures. This method delegates work to handler methods 51 | * (handleStartProgressEvent, handleInProgressEvent) implemented in 52 | * extending classes. 53 | * 54 | * @param event 55 | * @return 56 | */ 57 | public boolean onTouchEvent(MotionEvent event) { 58 | final int actionCode = event.getAction() & MotionEvent.ACTION_MASK; 59 | if (!mGestureInProgress) { 60 | handleStartProgressEvent(actionCode, event); 61 | } else { 62 | handleInProgressEvent(actionCode, event); 63 | } 64 | return true; 65 | } 66 | 67 | /** 68 | * Called when the current event occurred when NO gesture is in progress 69 | * yet. The handling in this implementation may set the gesture in progress 70 | * (via mGestureInProgress) or out of progress 71 | * 72 | * @param actionCode 73 | * @param event 74 | */ 75 | protected abstract void handleStartProgressEvent(int actionCode, MotionEvent event); 76 | 77 | /** 78 | * Called when the current event occurred when a gesture IS in progress. The 79 | * handling in this implementation may set the gesture out of progress (via 80 | * mGestureInProgress). 81 | * 82 | * @param actionCode 83 | * @param event 84 | */ 85 | protected abstract void handleInProgressEvent(int actionCode, MotionEvent event); 86 | 87 | 88 | protected void updateStateByEvent(MotionEvent curr) { 89 | final MotionEvent prev = mPrevEvent; 90 | 91 | // Reset mCurrEvent 92 | if (mCurrEvent != null) { 93 | mCurrEvent.recycle(); 94 | mCurrEvent = null; 95 | } 96 | mCurrEvent = MotionEvent.obtain(curr); 97 | 98 | 99 | // Delta time 100 | mTimeDelta = curr.getEventTime() - prev.getEventTime(); 101 | 102 | // Pressure 103 | mCurrPressure = curr.getPressure(curr.getActionIndex()); 104 | mPrevPressure = prev.getPressure(prev.getActionIndex()); 105 | } 106 | 107 | protected void resetState() { 108 | if (mPrevEvent != null) { 109 | mPrevEvent.recycle(); 110 | mPrevEvent = null; 111 | } 112 | if (mCurrEvent != null) { 113 | mCurrEvent.recycle(); 114 | mCurrEvent = null; 115 | } 116 | mGestureInProgress = false; 117 | } 118 | 119 | 120 | /** 121 | * Returns {@code true} if a gesture is currently in progress. 122 | * 123 | * @return {@code true} if a gesture is currently in progress, {@code false} otherwise. 124 | */ 125 | public boolean isInProgress() { 126 | return mGestureInProgress; 127 | } 128 | 129 | /** 130 | * Return the time difference in milliseconds between the previous accepted 131 | * GestureDetector event and the current GestureDetector event. 132 | * 133 | * @return Time difference since the last move event in milliseconds. 134 | */ 135 | public long getTimeDelta() { 136 | return mTimeDelta; 137 | } 138 | 139 | /** 140 | * Return the event time of the current GestureDetector event being 141 | * processed. 142 | * 143 | * @return Current GestureDetector event time in milliseconds. 144 | */ 145 | public long getEventTime() { 146 | return mCurrEvent.getEventTime(); 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /app/src/main/java/com/almeros/android/multitouch/MoveGestureDetector.java: -------------------------------------------------------------------------------- 1 | package com.almeros.android.multitouch; 2 | 3 | import android.content.Context; 4 | import android.graphics.PointF; 5 | import android.view.MotionEvent; 6 | 7 | /** 8 | * @author Almer Thie (code.almeros.com) 9 | * Copyright (c) 2013, Almer Thie (code.almeros.com) 10 | *

11 | * All rights reserved. 12 | *

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

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

19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 20 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 21 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 22 | * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 23 | * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 25 | * OF SUCH DAMAGE. 26 | */ 27 | public class MoveGestureDetector extends BaseGestureDetector { 28 | 29 | private static final PointF FOCUS_DELTA_ZERO = new PointF(); 30 | private final OnMoveGestureListener mListener; 31 | private PointF mCurrFocusInternal; 32 | private PointF mPrevFocusInternal; 33 | private PointF mFocusExternal = new PointF(); 34 | private PointF mFocusDeltaExternal = new PointF(); 35 | public MoveGestureDetector(Context context, OnMoveGestureListener listener) { 36 | super(context); 37 | mListener = listener; 38 | } 39 | 40 | @Override 41 | protected void handleStartProgressEvent(int actionCode, MotionEvent event) { 42 | switch (actionCode) { 43 | case MotionEvent.ACTION_DOWN: 44 | resetState(); // In case we missed an UP/CANCEL event 45 | 46 | mPrevEvent = MotionEvent.obtain(event); 47 | mTimeDelta = 0; 48 | 49 | updateStateByEvent(event); 50 | break; 51 | 52 | case MotionEvent.ACTION_MOVE: 53 | mGestureInProgress = mListener.onMoveBegin(this); 54 | break; 55 | } 56 | } 57 | 58 | @Override 59 | protected void handleInProgressEvent(int actionCode, MotionEvent event) { 60 | switch (actionCode) { 61 | case MotionEvent.ACTION_UP: 62 | case MotionEvent.ACTION_CANCEL: 63 | mListener.onMoveEnd(this); 64 | resetState(); 65 | break; 66 | 67 | case MotionEvent.ACTION_MOVE: 68 | updateStateByEvent(event); 69 | 70 | // Only accept the event if our relative pressure is within 71 | // a certain limit. This can help filter shaky data as a 72 | // finger is lifted. 73 | if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) { 74 | final boolean updatePrevious = mListener.onMove(this); 75 | if (updatePrevious) { 76 | mPrevEvent.recycle(); 77 | mPrevEvent = MotionEvent.obtain(event); 78 | } 79 | } 80 | break; 81 | } 82 | } 83 | 84 | protected void updateStateByEvent(MotionEvent curr) { 85 | super.updateStateByEvent(curr); 86 | 87 | final MotionEvent prev = mPrevEvent; 88 | 89 | // Focus intenal 90 | mCurrFocusInternal = determineFocalPoint(curr); 91 | mPrevFocusInternal = determineFocalPoint(prev); 92 | 93 | // Focus external 94 | // - Prevent skipping of focus delta when a finger is added or removed 95 | boolean mSkipNextMoveEvent = prev.getPointerCount() != curr.getPointerCount(); 96 | mFocusDeltaExternal = mSkipNextMoveEvent ? FOCUS_DELTA_ZERO : new PointF(mCurrFocusInternal.x - mPrevFocusInternal.x, mCurrFocusInternal.y - mPrevFocusInternal.y); 97 | 98 | // - Don't directly use mFocusInternal (or skipping will occur). Add 99 | // unskipped delta values to mFocusExternal instead. 100 | mFocusExternal.x += mFocusDeltaExternal.x; 101 | mFocusExternal.y += mFocusDeltaExternal.y; 102 | } 103 | 104 | /** 105 | * Determine (multi)finger focal point (a.k.a. center point between all 106 | * fingers) 107 | * 108 | * @param MotionEvent e 109 | * @return PointF focal point 110 | */ 111 | private PointF determineFocalPoint(MotionEvent e) { 112 | // Number of fingers on screen 113 | final int pCount = e.getPointerCount(); 114 | float x = 0f; 115 | float y = 0f; 116 | 117 | for (int i = 0; i < pCount; i++) { 118 | x += e.getX(i); 119 | y += e.getY(i); 120 | } 121 | 122 | return new PointF(x / pCount, y / pCount); 123 | } 124 | 125 | public float getFocusX() { 126 | return mFocusExternal.x; 127 | } 128 | 129 | public float getFocusY() { 130 | return mFocusExternal.y; 131 | } 132 | 133 | public PointF getFocusDelta() { 134 | return mFocusDeltaExternal; 135 | } 136 | 137 | /** 138 | * Listener which must be implemented which is used by MoveGestureDetector 139 | * to perform callbacks to any implementing class which is registered to a 140 | * MoveGestureDetector via the constructor. 141 | * 142 | * @see MoveGestureDetector.SimpleOnMoveGestureListener 143 | */ 144 | public interface OnMoveGestureListener { 145 | public boolean onMove(MoveGestureDetector detector); 146 | 147 | public boolean onMoveBegin(MoveGestureDetector detector); 148 | 149 | public void onMoveEnd(MoveGestureDetector detector); 150 | } 151 | 152 | /** 153 | * Helper class which may be extended and where the methods may be 154 | * implemented. This way it is not necessary to implement all methods 155 | * of OnMoveGestureListener. 156 | */ 157 | public static class SimpleOnMoveGestureListener implements OnMoveGestureListener { 158 | public boolean onMove(MoveGestureDetector detector) { 159 | return false; 160 | } 161 | 162 | public boolean onMoveBegin(MoveGestureDetector detector) { 163 | return true; 164 | } 165 | 166 | public void onMoveEnd(MoveGestureDetector detector) { 167 | // Do nothing, overridden implementation may be used 168 | } 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /app/src/main/java/com/almeros/android/multitouch/RotateGestureDetector.java: -------------------------------------------------------------------------------- 1 | package com.almeros.android.multitouch; 2 | 3 | import android.content.Context; 4 | import android.view.MotionEvent; 5 | 6 | /** 7 | * @author Almer Thie (code.almeros.com) 8 | * Copyright (c) 2013, Almer Thie (code.almeros.com) 9 | *

10 | * All rights reserved. 11 | *

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

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

18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 19 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 20 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 21 | * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 22 | * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 24 | * OF SUCH DAMAGE. 25 | */ 26 | public class RotateGestureDetector extends TwoFingerGestureDetector { 27 | 28 | private final OnRotateGestureListener mListener; 29 | private boolean mSloppyGesture; 30 | 31 | 32 | public RotateGestureDetector(Context context, OnRotateGestureListener listener) { 33 | super(context); 34 | mListener = listener; 35 | } 36 | 37 | @Override 38 | protected void handleStartProgressEvent(int actionCode, MotionEvent event) { 39 | switch (actionCode) { 40 | case MotionEvent.ACTION_POINTER_DOWN: 41 | // At least the second finger is on screen now 42 | 43 | resetState(); // In case we missed an UP/CANCEL event 44 | mPrevEvent = MotionEvent.obtain(event); 45 | mTimeDelta = 0; 46 | 47 | updateStateByEvent(event); 48 | 49 | // See if we have a sloppy gesture 50 | mSloppyGesture = isSloppyGesture(event); 51 | if (!mSloppyGesture) { 52 | // No, start gesture now 53 | mGestureInProgress = mListener.onRotateBegin(this); 54 | } 55 | break; 56 | 57 | case MotionEvent.ACTION_MOVE: 58 | if (!mSloppyGesture) { 59 | break; 60 | } 61 | 62 | // See if we still have a sloppy gesture 63 | mSloppyGesture = isSloppyGesture(event); 64 | if (!mSloppyGesture) { 65 | // No, start normal gesture now 66 | mGestureInProgress = mListener.onRotateBegin(this); 67 | } 68 | 69 | break; 70 | 71 | case MotionEvent.ACTION_POINTER_UP: 72 | if (!mSloppyGesture) { 73 | break; 74 | } 75 | 76 | break; 77 | } 78 | } 79 | 80 | @Override 81 | protected void handleInProgressEvent(int actionCode, MotionEvent event) { 82 | switch (actionCode) { 83 | case MotionEvent.ACTION_POINTER_UP: 84 | // Gesture ended but 85 | updateStateByEvent(event); 86 | 87 | if (!mSloppyGesture) { 88 | mListener.onRotateEnd(this); 89 | } 90 | 91 | resetState(); 92 | break; 93 | 94 | case MotionEvent.ACTION_CANCEL: 95 | if (!mSloppyGesture) { 96 | mListener.onRotateEnd(this); 97 | } 98 | 99 | resetState(); 100 | break; 101 | 102 | case MotionEvent.ACTION_MOVE: 103 | updateStateByEvent(event); 104 | 105 | // Only accept the event if our relative pressure is within 106 | // a certain limit. This can help filter shaky data as a 107 | // finger is lifted. 108 | if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) { 109 | final boolean updatePrevious = mListener.onRotate(this); 110 | if (updatePrevious) { 111 | mPrevEvent.recycle(); 112 | mPrevEvent = MotionEvent.obtain(event); 113 | } 114 | } 115 | break; 116 | } 117 | } 118 | 119 | @Override 120 | protected void resetState() { 121 | super.resetState(); 122 | mSloppyGesture = false; 123 | } 124 | 125 | /** 126 | * Return the rotation difference from the previous rotate event to the current 127 | * event. 128 | * 129 | * @return The current rotation //difference in degrees. 130 | */ 131 | public float getRotationDegreesDelta() { 132 | double diffRadians = Math.atan2(mPrevFingerDiffY, mPrevFingerDiffX) - Math.atan2(mCurrFingerDiffY, mCurrFingerDiffX); 133 | return (float) (diffRadians * 180 / Math.PI); 134 | } 135 | 136 | /** 137 | * Listener which must be implemented which is used by RotateGestureDetector 138 | * to perform callbacks to any implementing class which is registered to a 139 | * RotateGestureDetector via the constructor. 140 | * 141 | * @see RotateGestureDetector.SimpleOnRotateGestureListener 142 | */ 143 | public interface OnRotateGestureListener { 144 | public boolean onRotate(RotateGestureDetector detector); 145 | 146 | public boolean onRotateBegin(RotateGestureDetector detector); 147 | 148 | public void onRotateEnd(RotateGestureDetector detector); 149 | } 150 | 151 | /** 152 | * Helper class which may be extended and where the methods may be 153 | * implemented. This way it is not necessary to implement all methods 154 | * of OnRotateGestureListener. 155 | */ 156 | public static class SimpleOnRotateGestureListener implements OnRotateGestureListener { 157 | public boolean onRotate(RotateGestureDetector detector) { 158 | return false; 159 | } 160 | 161 | public boolean onRotateBegin(RotateGestureDetector detector) { 162 | return true; 163 | } 164 | 165 | public void onRotateEnd(RotateGestureDetector detector) { 166 | // Do nothing, overridden implementation may be used 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /app/src/main/java/com/almeros/android/multitouch/ShoveGestureDetector.java: -------------------------------------------------------------------------------- 1 | package com.almeros.android.multitouch; 2 | 3 | import android.content.Context; 4 | import android.view.MotionEvent; 5 | 6 | /** 7 | * @author Robert Nordan (robert.nordan@norkart.no) 8 | *

9 | * Copyright (c) 2013, Norkart AS 10 | *

11 | * All rights reserved. 12 | *

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

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

19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 20 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 21 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 22 | * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 23 | * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 25 | * OF SUCH DAMAGE. 26 | */ 27 | public class ShoveGestureDetector extends TwoFingerGestureDetector { 28 | 29 | private final OnShoveGestureListener mListener; 30 | private float mPrevAverageY; 31 | private float mCurrAverageY; 32 | private boolean mSloppyGesture; 33 | 34 | public ShoveGestureDetector(Context context, OnShoveGestureListener listener) { 35 | super(context); 36 | mListener = listener; 37 | } 38 | 39 | @Override 40 | protected void handleStartProgressEvent(int actionCode, MotionEvent event) { 41 | switch (actionCode) { 42 | case MotionEvent.ACTION_POINTER_DOWN: 43 | // At least the second finger is on screen now 44 | 45 | resetState(); // In case we missed an UP/CANCEL event 46 | mPrevEvent = MotionEvent.obtain(event); 47 | mTimeDelta = 0; 48 | 49 | updateStateByEvent(event); 50 | 51 | // See if we have a sloppy gesture 52 | mSloppyGesture = isSloppyGesture(event); 53 | if (!mSloppyGesture) { 54 | // No, start gesture now 55 | mGestureInProgress = mListener.onShoveBegin(this); 56 | } 57 | break; 58 | 59 | case MotionEvent.ACTION_MOVE: 60 | if (!mSloppyGesture) { 61 | break; 62 | } 63 | 64 | // See if we still have a sloppy gesture 65 | mSloppyGesture = isSloppyGesture(event); 66 | if (!mSloppyGesture) { 67 | // No, start normal gesture now 68 | mGestureInProgress = mListener.onShoveBegin(this); 69 | } 70 | 71 | break; 72 | 73 | case MotionEvent.ACTION_POINTER_UP: 74 | if (!mSloppyGesture) { 75 | break; 76 | } 77 | 78 | break; 79 | } 80 | } 81 | 82 | @Override 83 | protected void handleInProgressEvent(int actionCode, MotionEvent event) { 84 | switch (actionCode) { 85 | case MotionEvent.ACTION_POINTER_UP: 86 | // Gesture ended but 87 | updateStateByEvent(event); 88 | 89 | if (!mSloppyGesture) { 90 | mListener.onShoveEnd(this); 91 | } 92 | 93 | resetState(); 94 | break; 95 | 96 | case MotionEvent.ACTION_CANCEL: 97 | if (!mSloppyGesture) { 98 | mListener.onShoveEnd(this); 99 | } 100 | 101 | resetState(); 102 | break; 103 | 104 | case MotionEvent.ACTION_MOVE: 105 | updateStateByEvent(event); 106 | 107 | // Only accept the event if our relative pressure is within 108 | // a certain limit. This can help filter shaky data as a 109 | // finger is lifted. Also check that shove is meaningful. 110 | if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD 111 | && Math.abs(getShovePixelsDelta()) > 0.5f) { 112 | final boolean updatePrevious = mListener.onShove(this); 113 | if (updatePrevious) { 114 | mPrevEvent.recycle(); 115 | mPrevEvent = MotionEvent.obtain(event); 116 | } 117 | } 118 | break; 119 | } 120 | } 121 | 122 | @Override 123 | protected void updateStateByEvent(MotionEvent curr) { 124 | super.updateStateByEvent(curr); 125 | 126 | final MotionEvent prev = mPrevEvent; 127 | float py0 = prev.getY(0); 128 | float py1 = prev.getY(1); 129 | mPrevAverageY = (py0 + py1) / 2.0f; 130 | 131 | float cy0 = curr.getY(0); 132 | float cy1 = curr.getY(1); 133 | mCurrAverageY = (cy0 + cy1) / 2.0f; 134 | } 135 | 136 | @Override 137 | protected boolean isSloppyGesture(MotionEvent event) { 138 | boolean sloppy = super.isSloppyGesture(event); 139 | if (sloppy) 140 | return true; 141 | 142 | // If it's not traditionally sloppy, we check if the angle between fingers 143 | // is acceptable. 144 | double angle = Math.abs(Math.atan2(mCurrFingerDiffY, mCurrFingerDiffX)); 145 | //about 20 degrees, left or right 146 | return !((0.0f < angle && angle < 0.35f) 147 | || 2.79f < angle && angle < Math.PI); 148 | } 149 | 150 | /** 151 | * Return the distance in pixels from the previous shove event to the current 152 | * event. 153 | * 154 | * @return The current distance in pixels. 155 | */ 156 | public float getShovePixelsDelta() { 157 | return mCurrAverageY - mPrevAverageY; 158 | } 159 | 160 | @Override 161 | protected void resetState() { 162 | super.resetState(); 163 | mSloppyGesture = false; 164 | mPrevAverageY = 0.0f; 165 | mCurrAverageY = 0.0f; 166 | } 167 | 168 | /** 169 | * Listener which must be implemented which is used by ShoveGestureDetector 170 | * to perform callbacks to any implementing class which is registered to a 171 | * ShoveGestureDetector via the constructor. 172 | * 173 | * @see ShoveGestureDetector.SimpleOnShoveGestureListener 174 | */ 175 | public interface OnShoveGestureListener { 176 | public boolean onShove(ShoveGestureDetector detector); 177 | 178 | public boolean onShoveBegin(ShoveGestureDetector detector); 179 | 180 | public void onShoveEnd(ShoveGestureDetector detector); 181 | } 182 | 183 | /** 184 | * Helper class which may be extended and where the methods may be 185 | * implemented. This way it is not necessary to implement all methods 186 | * of OnShoveGestureListener. 187 | */ 188 | public static class SimpleOnShoveGestureListener implements OnShoveGestureListener { 189 | public boolean onShove(ShoveGestureDetector detector) { 190 | return false; 191 | } 192 | 193 | public boolean onShoveBegin(ShoveGestureDetector detector) { 194 | return true; 195 | } 196 | 197 | public void onShoveEnd(ShoveGestureDetector detector) { 198 | // Do nothing, overridden implementation may be used 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /app/src/main/java/com/almeros/android/multitouch/TwoFingerGestureDetector.java: -------------------------------------------------------------------------------- 1 | package com.almeros.android.multitouch; 2 | 3 | import android.content.Context; 4 | import android.util.DisplayMetrics; 5 | import android.view.MotionEvent; 6 | import android.view.ViewConfiguration; 7 | 8 | /** 9 | * @author Almer Thie (code.almeros.com) 10 | * Copyright (c) 2013, Almer Thie (code.almeros.com) 11 | *

12 | * All rights reserved. 13 | *

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

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

20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 21 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 22 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 23 | * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 24 | * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 26 | * OF SUCH DAMAGE. 27 | */ 28 | public abstract class TwoFingerGestureDetector extends BaseGestureDetector { 29 | 30 | private final float mEdgeSlop; 31 | protected float mPrevFingerDiffX; 32 | protected float mPrevFingerDiffY; 33 | protected float mCurrFingerDiffX; 34 | protected float mCurrFingerDiffY; 35 | private float mRightSlopEdge; 36 | private float mBottomSlopEdge; 37 | private float mCurrLen; 38 | private float mPrevLen; 39 | 40 | public TwoFingerGestureDetector(Context context) { 41 | super(context); 42 | 43 | ViewConfiguration config = ViewConfiguration.get(context); 44 | mEdgeSlop = config.getScaledEdgeSlop(); 45 | } 46 | 47 | @Override 48 | protected abstract void handleStartProgressEvent(int actionCode, MotionEvent event); 49 | 50 | @Override 51 | protected abstract void handleInProgressEvent(int actionCode, MotionEvent event); 52 | 53 | protected void updateStateByEvent(MotionEvent curr) { 54 | super.updateStateByEvent(curr); 55 | 56 | final MotionEvent prev = mPrevEvent; 57 | 58 | mCurrLen = -1; 59 | mPrevLen = -1; 60 | 61 | // Previous 62 | final float px0 = prev.getX(0); 63 | final float py0 = prev.getY(0); 64 | final float px1 = prev.getX(1); 65 | final float py1 = prev.getY(1); 66 | final float pvx = px1 - px0; 67 | final float pvy = py1 - py0; 68 | mPrevFingerDiffX = pvx; 69 | mPrevFingerDiffY = pvy; 70 | 71 | // Current 72 | final float cx0 = curr.getX(0); 73 | final float cy0 = curr.getY(0); 74 | final float cx1 = curr.getX(1); 75 | final float cy1 = curr.getY(1); 76 | final float cvx = cx1 - cx0; 77 | final float cvy = cy1 - cy0; 78 | mCurrFingerDiffX = cvx; 79 | mCurrFingerDiffY = cvy; 80 | } 81 | 82 | /** 83 | * Return the current distance between the two pointers forming the 84 | * gesture in progress. 85 | * 86 | * @return Distance between pointers in pixels. 87 | */ 88 | public float getCurrentSpan() { 89 | if (mCurrLen == -1) { 90 | final float cvx = mCurrFingerDiffX; 91 | final float cvy = mCurrFingerDiffY; 92 | mCurrLen = (float) Math.sqrt(cvx * cvx + cvy * cvy); 93 | } 94 | return mCurrLen; 95 | } 96 | 97 | /** 98 | * Return the previous distance between the two pointers forming the 99 | * gesture in progress. 100 | * 101 | * @return Previous distance between pointers in pixels. 102 | */ 103 | public float getPreviousSpan() { 104 | if (mPrevLen == -1) { 105 | final float pvx = mPrevFingerDiffX; 106 | final float pvy = mPrevFingerDiffY; 107 | mPrevLen = (float) Math.sqrt(pvx * pvx + pvy * pvy); 108 | } 109 | return mPrevLen; 110 | } 111 | 112 | /** 113 | * Check if we have a sloppy gesture. Sloppy gestures can happen if the edge 114 | * of the user's hand is touching the screen, for example. 115 | * 116 | * @param event 117 | * @return 118 | */ 119 | protected boolean isSloppyGesture(MotionEvent event) { 120 | // As orientation can change, query the metrics in touch down 121 | DisplayMetrics metrics = mContext.getResources().getDisplayMetrics(); 122 | mRightSlopEdge = metrics.widthPixels - mEdgeSlop; 123 | mBottomSlopEdge = metrics.heightPixels - mEdgeSlop; 124 | 125 | final float edgeSlop = mEdgeSlop; 126 | final float rightSlop = mRightSlopEdge; 127 | final float bottomSlop = mBottomSlopEdge; 128 | 129 | final float x0 = event.getRawX(); 130 | final float y0 = event.getRawY(); 131 | final float x1 = getRawX(event, 1); 132 | final float y1 = getRawY(event, 1); 133 | 134 | boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop 135 | || x0 > rightSlop || y0 > bottomSlop; 136 | boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop 137 | || x1 > rightSlop || y1 > bottomSlop; 138 | 139 | if (p0sloppy && p1sloppy) { 140 | return true; 141 | } else if (p0sloppy) { 142 | return true; 143 | } else if (p1sloppy) { 144 | return true; 145 | } 146 | return false; 147 | } 148 | 149 | /** 150 | * MotionEvent has no getRawX(int) method; simulate it pending future API approval. 151 | * 152 | * @param event 153 | * @param pointerIndex 154 | * @return 155 | */ 156 | protected static float getRawX(MotionEvent event, int pointerIndex) { 157 | float offset = event.getX() - event.getRawX(); 158 | if (pointerIndex < event.getPointerCount()) { 159 | return event.getX(pointerIndex) + offset; 160 | } 161 | return 0f; 162 | } 163 | 164 | /** 165 | * MotionEvent has no getRawY(int) method; simulate it pending future API approval. 166 | * 167 | * @param event 168 | * @param pointerIndex 169 | * @return 170 | */ 171 | protected static float getRawY(MotionEvent event, int pointerIndex) { 172 | float offset = event.getY() - event.getRawY(); 173 | if (pointerIndex < event.getPointerCount()) { 174 | return event.getY(pointerIndex) + offset; 175 | } 176 | return 0f; 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /app/src/main/java/team/uptech/motionviews/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews.ui; 2 | 3 | import android.content.DialogInterface; 4 | import android.content.Intent; 5 | import android.graphics.Bitmap; 6 | import android.graphics.BitmapFactory; 7 | import android.graphics.PointF; 8 | import android.os.Bundle; 9 | import android.support.annotation.NonNull; 10 | import android.support.annotation.Nullable; 11 | import android.support.v7.app.AlertDialog; 12 | import android.support.v7.app.AppCompatActivity; 13 | import android.view.Menu; 14 | import android.view.MenuItem; 15 | import android.view.View; 16 | 17 | import com.flask.colorpicker.ColorPickerView; 18 | import com.flask.colorpicker.builder.ColorPickerClickListener; 19 | import com.flask.colorpicker.builder.ColorPickerDialogBuilder; 20 | 21 | import java.util.List; 22 | 23 | import team.uptech.motionviews.BuildConfig; 24 | import team.uptech.motionviews.R; 25 | import team.uptech.motionviews.ui.adapter.FontsAdapter; 26 | import team.uptech.motionviews.utils.FontProvider; 27 | import team.uptech.motionviews.viewmodel.Font; 28 | import team.uptech.motionviews.viewmodel.Layer; 29 | import team.uptech.motionviews.viewmodel.TextLayer; 30 | import team.uptech.motionviews.widget.MotionView; 31 | import team.uptech.motionviews.widget.entity.ImageEntity; 32 | import team.uptech.motionviews.widget.entity.MotionEntity; 33 | import team.uptech.motionviews.widget.entity.TextEntity; 34 | 35 | public class MainActivity extends AppCompatActivity implements TextEditorDialogFragment.OnTextLayerCallback { 36 | 37 | public static final int SELECT_STICKER_REQUEST_CODE = 123; 38 | 39 | protected MotionView motionView; 40 | protected View textEntityEditPanel; 41 | private final MotionView.MotionViewCallback motionViewCallback = new MotionView.MotionViewCallback() { 42 | @Override 43 | public void onEntitySelected(@Nullable MotionEntity entity) { 44 | if (entity instanceof TextEntity) { 45 | textEntityEditPanel.setVisibility(View.VISIBLE); 46 | } else { 47 | textEntityEditPanel.setVisibility(View.GONE); 48 | } 49 | } 50 | 51 | @Override 52 | public void onEntityDoubleTap(@NonNull MotionEntity entity) { 53 | startTextEntityEditing(); 54 | } 55 | }; 56 | private FontProvider fontProvider; 57 | 58 | @Override 59 | protected void onCreate(Bundle savedInstanceState) { 60 | super.onCreate(savedInstanceState); 61 | setContentView(R.layout.activity_main); 62 | 63 | this.fontProvider = new FontProvider(getResources()); 64 | 65 | motionView = (MotionView) findViewById(R.id.main_motion_view); 66 | textEntityEditPanel = findViewById(R.id.main_motion_text_entity_edit_panel); 67 | motionView.setMotionViewCallback(motionViewCallback); 68 | 69 | addSticker(R.drawable.pikachu_2); 70 | 71 | initTextEntitiesListeners(); 72 | } 73 | 74 | private void addSticker(final int stickerResId) { 75 | motionView.post(new Runnable() { 76 | @Override 77 | public void run() { 78 | Layer layer = new Layer(); 79 | Bitmap pica = BitmapFactory.decodeResource(getResources(), stickerResId); 80 | 81 | ImageEntity entity = new ImageEntity(layer, pica, motionView.getWidth(), motionView.getHeight()); 82 | 83 | motionView.addEntityAndPosition(entity); 84 | } 85 | }); 86 | } 87 | 88 | private void initTextEntitiesListeners() { 89 | findViewById(R.id.text_entity_font_size_increase).setOnClickListener(new View.OnClickListener() { 90 | @Override 91 | public void onClick(View view) { 92 | increaseTextEntitySize(); 93 | } 94 | }); 95 | findViewById(R.id.text_entity_font_size_decrease).setOnClickListener(new View.OnClickListener() { 96 | @Override 97 | public void onClick(View view) { 98 | decreaseTextEntitySize(); 99 | } 100 | }); 101 | findViewById(R.id.text_entity_color_change).setOnClickListener(new View.OnClickListener() { 102 | @Override 103 | public void onClick(View view) { 104 | changeTextEntityColor(); 105 | } 106 | }); 107 | findViewById(R.id.text_entity_font_change).setOnClickListener(new View.OnClickListener() { 108 | @Override 109 | public void onClick(View view) { 110 | changeTextEntityFont(); 111 | } 112 | }); 113 | findViewById(R.id.text_entity_edit).setOnClickListener(new View.OnClickListener() { 114 | @Override 115 | public void onClick(View view) { 116 | startTextEntityEditing(); 117 | } 118 | }); 119 | } 120 | 121 | private void increaseTextEntitySize() { 122 | TextEntity textEntity = currentTextEntity(); 123 | if (textEntity != null) { 124 | textEntity.getLayer().getFont().increaseSize(TextLayer.Limits.FONT_SIZE_STEP); 125 | textEntity.updateEntity(); 126 | motionView.invalidate(); 127 | } 128 | } 129 | 130 | private void decreaseTextEntitySize() { 131 | TextEntity textEntity = currentTextEntity(); 132 | if (textEntity != null) { 133 | textEntity.getLayer().getFont().decreaseSize(TextLayer.Limits.FONT_SIZE_STEP); 134 | textEntity.updateEntity(); 135 | motionView.invalidate(); 136 | } 137 | } 138 | 139 | private void changeTextEntityColor() { 140 | TextEntity textEntity = currentTextEntity(); 141 | if (textEntity == null) { 142 | return; 143 | } 144 | 145 | int initialColor = textEntity.getLayer().getFont().getColor(); 146 | 147 | ColorPickerDialogBuilder 148 | .with(MainActivity.this) 149 | .setTitle(R.string.select_color) 150 | .initialColor(initialColor) 151 | .wheelType(ColorPickerView.WHEEL_TYPE.CIRCLE) 152 | .density(8) // magic number 153 | .setPositiveButton(R.string.ok, new ColorPickerClickListener() { 154 | @Override 155 | public void onClick(DialogInterface dialog, int selectedColor, Integer[] allColors) { 156 | TextEntity textEntity = currentTextEntity(); 157 | if (textEntity != null) { 158 | textEntity.getLayer().getFont().setColor(selectedColor); 159 | textEntity.updateEntity(); 160 | motionView.invalidate(); 161 | } 162 | } 163 | }) 164 | .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { 165 | @Override 166 | public void onClick(DialogInterface dialog, int which) { 167 | } 168 | }) 169 | .build() 170 | .show(); 171 | } 172 | 173 | private void changeTextEntityFont() { 174 | final List fonts = fontProvider.getFontNames(); 175 | FontsAdapter fontsAdapter = new FontsAdapter(this, fonts, fontProvider); 176 | new AlertDialog.Builder(this) 177 | .setTitle(R.string.select_font) 178 | .setAdapter(fontsAdapter, new DialogInterface.OnClickListener() { 179 | @Override 180 | public void onClick(DialogInterface dialogInterface, int which) { 181 | TextEntity textEntity = currentTextEntity(); 182 | if (textEntity != null) { 183 | textEntity.getLayer().getFont().setTypeface(fonts.get(which)); 184 | textEntity.updateEntity(); 185 | motionView.invalidate(); 186 | } 187 | } 188 | }) 189 | .show(); 190 | } 191 | 192 | private void startTextEntityEditing() { 193 | TextEntity textEntity = currentTextEntity(); 194 | if (textEntity != null) { 195 | TextEditorDialogFragment fragment = TextEditorDialogFragment.getInstance(textEntity.getLayer().getText()); 196 | fragment.show(getFragmentManager(), TextEditorDialogFragment.class.getName()); 197 | } 198 | } 199 | 200 | @Nullable 201 | private TextEntity currentTextEntity() { 202 | if (motionView != null && motionView.getSelectedEntity() instanceof TextEntity) { 203 | return ((TextEntity) motionView.getSelectedEntity()); 204 | } else { 205 | return null; 206 | } 207 | } 208 | 209 | @Override 210 | public boolean onCreateOptionsMenu(Menu menu) { 211 | super.onCreateOptionsMenu(menu); 212 | getMenuInflater().inflate(R.menu.main_menu, menu); 213 | return true; 214 | } 215 | 216 | @Override 217 | public boolean onOptionsItemSelected(MenuItem item) { 218 | if (item.getItemId() == R.id.main_add_sticker) { 219 | Intent intent = new Intent(this, StickerSelectActivity.class); 220 | startActivityForResult(intent, SELECT_STICKER_REQUEST_CODE); 221 | return true; 222 | } else if (item.getItemId() == R.id.main_add_text) { 223 | addTextSticker(); 224 | } 225 | return super.onOptionsItemSelected(item); 226 | } 227 | 228 | protected void addTextSticker() { 229 | TextLayer textLayer = createTextLayer(); 230 | TextEntity textEntity = new TextEntity(textLayer, motionView.getWidth(), 231 | motionView.getHeight(), fontProvider); 232 | motionView.addEntityAndPosition(textEntity); 233 | 234 | // move text sticker up so that its not hidden under keyboard 235 | PointF center = textEntity.absoluteCenter(); 236 | center.y = center.y * 0.5F; 237 | textEntity.moveCenterTo(center); 238 | 239 | // redraw 240 | motionView.invalidate(); 241 | 242 | startTextEntityEditing(); 243 | } 244 | 245 | private TextLayer createTextLayer() { 246 | TextLayer textLayer = new TextLayer(); 247 | Font font = new Font(); 248 | 249 | font.setColor(TextLayer.Limits.INITIAL_FONT_COLOR); 250 | font.setSize(TextLayer.Limits.INITIAL_FONT_SIZE); 251 | font.setTypeface(fontProvider.getDefaultFontName()); 252 | 253 | textLayer.setFont(font); 254 | 255 | if (BuildConfig.DEBUG) { 256 | textLayer.setText("Hello, world :))"); 257 | } 258 | 259 | return textLayer; 260 | } 261 | 262 | @Override 263 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 264 | super.onActivityResult(requestCode, resultCode, data); 265 | if (resultCode == RESULT_OK) { 266 | if (requestCode == SELECT_STICKER_REQUEST_CODE) { 267 | if (data != null) { 268 | int stickerId = data.getIntExtra(StickerSelectActivity.EXTRA_STICKER_ID, 0); 269 | if (stickerId != 0) { 270 | addSticker(stickerId); 271 | } 272 | } 273 | } 274 | } 275 | } 276 | 277 | @Override 278 | public void textChanged(@NonNull String text) { 279 | TextEntity textEntity = currentTextEntity(); 280 | if (textEntity != null) { 281 | TextLayer textLayer = textEntity.getLayer(); 282 | if (!text.equals(textLayer.getText())) { 283 | textLayer.setText(text); 284 | textEntity.updateEntity(); 285 | motionView.invalidate(); 286 | } 287 | } 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /app/src/main/java/team/uptech/motionviews/ui/StickerSelectActivity.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews.ui; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.annotation.NonNull; 7 | import android.support.annotation.Nullable; 8 | import android.support.v4.content.ContextCompat; 9 | import android.support.v7.app.AppCompatActivity; 10 | import android.support.v7.widget.GridLayoutManager; 11 | import android.support.v7.widget.RecyclerView; 12 | import android.view.LayoutInflater; 13 | import android.view.MenuItem; 14 | import android.view.View; 15 | import android.view.ViewGroup; 16 | import android.widget.ImageView; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | import team.uptech.motionviews.R; 22 | 23 | /** 24 | * selects sticker 25 | * result - Integer, resource id of the sticker, bundled at key EXTRA_STICKER_ID 26 | *

27 | * Stickers borrowed from : http://www.flaticon.com/packs/pokemon-go 28 | */ 29 | 30 | public class StickerSelectActivity extends AppCompatActivity { 31 | 32 | public static final String EXTRA_STICKER_ID = "extra_sticker_id"; 33 | 34 | private final int[] stickerIds = { 35 | R.drawable.abra, 36 | R.drawable.bellsprout, 37 | R.drawable.bracelet, 38 | R.drawable.bullbasaur, 39 | R.drawable.camera, 40 | R.drawable.candy, 41 | R.drawable.caterpie, 42 | R.drawable.charmander, 43 | R.drawable.mankey, 44 | R.drawable.map, 45 | R.drawable.mega_ball, 46 | R.drawable.meowth, 47 | R.drawable.pawprints, 48 | R.drawable.pidgey, 49 | R.drawable.pikachu, 50 | R.drawable.pikachu_1, 51 | R.drawable.pikachu_2, 52 | R.drawable.player, 53 | R.drawable.pointer, 54 | R.drawable.pokebag, 55 | R.drawable.pokeball, 56 | R.drawable.pokeballs, 57 | R.drawable.pokecoin, 58 | R.drawable.pokedex, 59 | R.drawable.potion, 60 | R.drawable.psyduck, 61 | R.drawable.rattata, 62 | R.drawable.revive, 63 | R.drawable.squirtle, 64 | R.drawable.star, 65 | R.drawable.star_1, 66 | R.drawable.superball, 67 | R.drawable.tornado, 68 | R.drawable.venonat, 69 | R.drawable.weedle, 70 | R.drawable.zubat 71 | }; 72 | 73 | @Override 74 | protected void onCreate(@Nullable Bundle savedInstanceState) { 75 | super.onCreate(savedInstanceState); 76 | setContentView(R.layout.select_sticker_activity); 77 | 78 | //noinspection ConstantConditions 79 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 80 | 81 | RecyclerView recyclerView = (RecyclerView) findViewById(R.id.stickers_recycler_view); 82 | GridLayoutManager glm = new GridLayoutManager(this, 3); 83 | recyclerView.setLayoutManager(glm); 84 | 85 | List stickers = new ArrayList<>(stickerIds.length); 86 | for (Integer id : stickerIds) { 87 | stickers.add(id); 88 | } 89 | 90 | recyclerView.setAdapter(new StickersAdapter(stickers, this)); 91 | } 92 | 93 | @Override 94 | public boolean onOptionsItemSelected(MenuItem item) { 95 | if (item.getItemId() == android.R.id.home) { 96 | onBackPressed(); 97 | return true; 98 | } 99 | return super.onOptionsItemSelected(item); 100 | } 101 | 102 | private void onStickerSelected(int stickerId) { 103 | Intent intent = new Intent(); 104 | intent.putExtra(EXTRA_STICKER_ID, stickerId); 105 | setResult(RESULT_OK, intent); 106 | finish(); 107 | } 108 | 109 | class StickersAdapter extends RecyclerView.Adapter { 110 | 111 | private final List stickerIds; 112 | private final Context context; 113 | private final LayoutInflater layoutInflater; 114 | 115 | StickersAdapter(@NonNull List stickerIds, @NonNull Context context) { 116 | this.stickerIds = stickerIds; 117 | this.context = context; 118 | this.layoutInflater = LayoutInflater.from(context); 119 | } 120 | 121 | @Override 122 | public StickerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 123 | return new StickerViewHolder(layoutInflater.inflate(R.layout.sticker_item, parent, false)); 124 | } 125 | 126 | @Override 127 | public void onBindViewHolder(StickerViewHolder holder, int position) { 128 | holder.image.setImageDrawable(ContextCompat.getDrawable(context, getItem(position))); 129 | } 130 | 131 | @Override 132 | public int getItemCount() { 133 | return stickerIds.size(); 134 | } 135 | 136 | private int getItem(int position) { 137 | return stickerIds.get(position); 138 | } 139 | 140 | class StickerViewHolder extends RecyclerView.ViewHolder { 141 | 142 | ImageView image; 143 | 144 | StickerViewHolder(View itemView) { 145 | super(itemView); 146 | image = (ImageView) itemView.findViewById(R.id.sticker_image); 147 | itemView.setOnClickListener(new View.OnClickListener() { 148 | @Override 149 | public void onClick(View view) { 150 | int pos = getAdapterPosition(); 151 | if (pos >= 0) { // might be NO_POSITION 152 | onStickerSelected(getItem(pos)); 153 | } 154 | } 155 | }); 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /app/src/main/java/team/uptech/motionviews/ui/TextEditorDialogFragment.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews.ui; 2 | 3 | import android.app.Activity; 4 | import android.app.Dialog; 5 | import android.app.DialogFragment; 6 | import android.content.Context; 7 | import android.graphics.Color; 8 | import android.graphics.drawable.ColorDrawable; 9 | import android.os.Bundle; 10 | import android.support.annotation.NonNull; 11 | import android.text.Editable; 12 | import android.text.Selection; 13 | import android.text.TextWatcher; 14 | import android.view.LayoutInflater; 15 | import android.view.View; 16 | import android.view.ViewGroup; 17 | import android.view.Window; 18 | import android.view.WindowManager; 19 | import android.view.inputmethod.InputMethodManager; 20 | import android.widget.EditText; 21 | 22 | import team.uptech.motionviews.R; 23 | 24 | /** 25 | * Transparent Dialog Fragment, with no title and no background 26 | *

27 | * The fragment imitates capturing input from keyboard, but does not display anything 28 | * the result from input from the keyboard is passed through {@link TextEditorDialogFragment.OnTextLayerCallback} 29 | *

30 | * Activity that uses {@link TextEditorDialogFragment} must implement {@link TextEditorDialogFragment.OnTextLayerCallback} 31 | *

32 | * If Activity does not implement {@link TextEditorDialogFragment.OnTextLayerCallback}, exception will be thrown at Runtime 33 | */ 34 | public class TextEditorDialogFragment extends DialogFragment { 35 | 36 | public static final String ARG_TEXT = "editor_text_arg"; 37 | 38 | protected EditText editText; 39 | 40 | private OnTextLayerCallback callback; 41 | 42 | /** 43 | * deprecated 44 | * use {@link TextEditorDialogFragment#getInstance(String)} 45 | */ 46 | @Deprecated 47 | public TextEditorDialogFragment() { 48 | // empty, use getInstance 49 | } 50 | 51 | public static TextEditorDialogFragment getInstance(String textValue) { 52 | @SuppressWarnings("deprecation") 53 | TextEditorDialogFragment fragment = new TextEditorDialogFragment(); 54 | Bundle args = new Bundle(); 55 | args.putString(ARG_TEXT, textValue); 56 | fragment.setArguments(args); 57 | return fragment; 58 | } 59 | 60 | @Override 61 | public void onAttach(Activity activity) { 62 | super.onAttach(activity); 63 | if (activity instanceof OnTextLayerCallback) { 64 | this.callback = (OnTextLayerCallback) activity; 65 | } else { 66 | throw new IllegalStateException(activity.getClass().getName() 67 | + " must implement " + OnTextLayerCallback.class.getName()); 68 | } 69 | } 70 | 71 | @Override 72 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 73 | return inflater.inflate(R.layout.text_editor_layout, container, false); 74 | } 75 | 76 | @Override 77 | public void onViewCreated(View view, Bundle savedInstanceState) { 78 | super.onViewCreated(view, savedInstanceState); 79 | 80 | Bundle args = getArguments(); 81 | String text = ""; 82 | if (args != null) { 83 | text = args.getString(ARG_TEXT); 84 | } 85 | 86 | editText = (EditText) view.findViewById(R.id.edit_text_view); 87 | 88 | initWithTextEntity(text); 89 | 90 | editText.addTextChangedListener(new TextWatcher() { 91 | @Override 92 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 93 | 94 | } 95 | 96 | @Override 97 | public void onTextChanged(CharSequence s, int start, int before, int count) { 98 | 99 | } 100 | 101 | @Override 102 | public void afterTextChanged(Editable s) { 103 | if (callback != null) { 104 | callback.textChanged(s.toString()); 105 | } 106 | } 107 | }); 108 | 109 | view.findViewById(R.id.text_editor_root).setOnClickListener(new View.OnClickListener() { 110 | @Override 111 | public void onClick(View view) { 112 | // exit when clicking on background 113 | dismiss(); 114 | } 115 | }); 116 | } 117 | 118 | private void initWithTextEntity(String text) { 119 | editText.setText(text); 120 | editText.post(new Runnable() { 121 | @Override 122 | public void run() { 123 | if (editText != null) { 124 | Selection.setSelection(editText.getText(), editText.length()); 125 | } 126 | } 127 | }); 128 | } 129 | 130 | @Override 131 | public void dismiss() { 132 | super.dismiss(); 133 | 134 | // clearing memory on exit, cos manipulating with text uses bitmaps extensively 135 | // this does not frees memory immediately, but still can help 136 | System.gc(); 137 | Runtime.getRuntime().gc(); 138 | } 139 | 140 | @Override 141 | public void onDetach() { 142 | // release links 143 | this.callback = null; 144 | super.onDetach(); 145 | } 146 | 147 | @NonNull 148 | @Override 149 | public Dialog onCreateDialog(Bundle savedInstanceState) { 150 | Dialog dialog = super.onCreateDialog(savedInstanceState); 151 | dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); 152 | dialog.requestWindowFeature(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); 153 | return dialog; 154 | } 155 | 156 | @Override 157 | public void onStart() { 158 | super.onStart(); 159 | Dialog dialog = getDialog(); 160 | if (dialog != null) { 161 | Window window = dialog.getWindow(); 162 | if (window != null) { 163 | // remove background 164 | window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 165 | window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT); 166 | 167 | // remove dim 168 | WindowManager.LayoutParams windowParams = window.getAttributes(); 169 | window.setDimAmount(0.0F); 170 | window.setAttributes(windowParams); 171 | } 172 | } 173 | } 174 | 175 | @Override 176 | public void onResume() { 177 | super.onResume(); 178 | editText.post(new Runnable() { 179 | @Override 180 | public void run() { 181 | // force show the keyboard 182 | setEditText(true); 183 | editText.requestFocus(); 184 | InputMethodManager ims = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); 185 | ims.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); 186 | } 187 | }); 188 | } 189 | 190 | private void setEditText(boolean gainFocus) { 191 | if (!gainFocus) { 192 | editText.clearFocus(); 193 | editText.clearComposingText(); 194 | } 195 | editText.setFocusableInTouchMode(gainFocus); 196 | editText.setFocusable(gainFocus); 197 | } 198 | 199 | /** 200 | * Callback that passes all user input through the method 201 | * {@link TextEditorDialogFragment.OnTextLayerCallback#textChanged(String)} 202 | */ 203 | public interface OnTextLayerCallback { 204 | void textChanged(@NonNull String text); 205 | } 206 | } -------------------------------------------------------------------------------- /app/src/main/java/team/uptech/motionviews/ui/adapter/FontsAdapter.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews.ui.adapter; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ArrayAdapter; 9 | import android.widget.TextView; 10 | 11 | import java.util.List; 12 | 13 | import team.uptech.motionviews.utils.FontProvider; 14 | 15 | public class FontsAdapter extends ArrayAdapter { 16 | 17 | private final LayoutInflater inflater; 18 | private final FontProvider fontProvider; 19 | 20 | public FontsAdapter(Context context, List fontNames, FontProvider fontProvider) { 21 | super(context, 0, fontNames); 22 | // save LayoutInflater for later reuse 23 | this.inflater = LayoutInflater.from(context); 24 | this.fontProvider = fontProvider; 25 | } 26 | 27 | @NonNull 28 | @Override 29 | public View getView(int position, View convertView, @NonNull ViewGroup parent) { 30 | 31 | ViewHolder vh; 32 | if (convertView == null) { 33 | convertView = inflater.inflate(android.R.layout.simple_list_item_1, parent, false); 34 | vh = new ViewHolder(convertView); 35 | convertView.setTag(vh); 36 | } else { 37 | vh = (ViewHolder) convertView.getTag(); 38 | } 39 | 40 | String fontName = getItem(position); 41 | 42 | vh.textView.setTypeface(fontProvider.getTypeface(fontName)); 43 | vh.textView.setText(fontName); 44 | 45 | return convertView; 46 | } 47 | 48 | private static class ViewHolder { 49 | 50 | TextView textView; 51 | 52 | ViewHolder(View rootView) { 53 | textView = (TextView) rootView.findViewById(android.R.id.text1); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/team/uptech/motionviews/utils/FontProvider.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews.utils; 2 | 3 | import android.content.res.Resources; 4 | import android.graphics.Typeface; 5 | import android.support.annotation.Nullable; 6 | import android.text.TextUtils; 7 | 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | /** 14 | * extracting Typeface from Assets is a heavy operation, 15 | * we want to make sure that we cache the typefaces for reuse 16 | */ 17 | public class FontProvider { 18 | 19 | private static final String DEFAULT_FONT_NAME = "Helvetica"; 20 | 21 | private final Map typefaces; 22 | private final Map fontNameToTypefaceFile; 23 | private final Resources resources; 24 | private final List fontNames; 25 | 26 | public FontProvider(Resources resources) { 27 | this.resources = resources; 28 | 29 | typefaces = new HashMap<>(); 30 | 31 | // populate fonts 32 | fontNameToTypefaceFile = new HashMap<>(); 33 | fontNameToTypefaceFile.put("Arial", "Arial.ttf"); 34 | fontNameToTypefaceFile.put("Eutemia", "Eutemia.ttf"); 35 | fontNameToTypefaceFile.put("GREENPIL", "GREENPIL.ttf"); 36 | fontNameToTypefaceFile.put("Grinched", "Grinched.ttf"); 37 | fontNameToTypefaceFile.put("Helvetica", "Helvetica.ttf"); 38 | fontNameToTypefaceFile.put("Libertango", "Libertango.ttf"); 39 | fontNameToTypefaceFile.put("Metal Macabre", "MetalMacabre.ttf"); 40 | fontNameToTypefaceFile.put("Parry Hotter", "ParryHotter.ttf"); 41 | fontNameToTypefaceFile.put("SCRIPTIN", "SCRIPTIN.ttf"); 42 | fontNameToTypefaceFile.put("The Godfather v2", "TheGodfather_v2.ttf"); 43 | fontNameToTypefaceFile.put("Aka Dora", "akaDora.ttf"); 44 | fontNameToTypefaceFile.put("Waltograph", "waltograph42.ttf"); 45 | 46 | fontNames = new ArrayList<>(fontNameToTypefaceFile.keySet()); 47 | } 48 | 49 | /** 50 | * @param typefaceName must be one of the font names provided from {@link FontProvider#getFontNames()} 51 | * @return the Typeface associated with {@code typefaceName}, or {@link Typeface#DEFAULT} otherwise 52 | */ 53 | public Typeface getTypeface(@Nullable String typefaceName) { 54 | if (TextUtils.isEmpty(typefaceName)) { 55 | return Typeface.DEFAULT; 56 | } else { 57 | //noinspection Java8CollectionsApi 58 | if (typefaces.get(typefaceName) == null) { 59 | typefaces.put(typefaceName, 60 | Typeface.createFromAsset(resources.getAssets(), "fonts/" + fontNameToTypefaceFile.get(typefaceName))); 61 | } 62 | return typefaces.get(typefaceName); 63 | } 64 | } 65 | 66 | /** 67 | * use {@link FontProvider#getTypeface(String) to get Typeface for the font name} 68 | * 69 | * @return list of available font names 70 | */ 71 | public List getFontNames() { 72 | return fontNames; 73 | } 74 | 75 | /** 76 | * @return Default Font Name - Helvetica 77 | */ 78 | public String getDefaultFontName() { 79 | return DEFAULT_FONT_NAME; 80 | } 81 | } -------------------------------------------------------------------------------- /app/src/main/java/team/uptech/motionviews/utils/MathUtils.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews.utils; 2 | 3 | import android.graphics.PointF; 4 | import android.support.annotation.NonNull; 5 | 6 | public class MathUtils { 7 | 8 | /** 9 | * For more info: 10 | * StackOverflow: How to check point is in rectangle 11 | * 12 | * @param pt point to check 13 | * @param v1 vertex 1 of the triangle 14 | * @param v2 vertex 2 of the triangle 15 | * @param v3 vertex 3 of the triangle 16 | * @return true if point (x, y) is inside the triangle 17 | */ 18 | public static boolean pointInTriangle(@NonNull PointF pt, @NonNull PointF v1, 19 | @NonNull PointF v2, @NonNull PointF v3) { 20 | 21 | boolean b1 = crossProduct(pt, v1, v2) < 0.0f; 22 | boolean b2 = crossProduct(pt, v2, v3) < 0.0f; 23 | boolean b3 = crossProduct(pt, v3, v1) < 0.0f; 24 | 25 | return (b1 == b2) && (b2 == b3); 26 | } 27 | 28 | /** 29 | * calculates cross product of vectors AB and AC 30 | * 31 | * @param a beginning of 2 vectors 32 | * @param b end of vector 1 33 | * @param c enf of vector 2 34 | * @return cross product AB * AC 35 | */ 36 | private static float crossProduct(@NonNull PointF a, @NonNull PointF b, @NonNull PointF c) { 37 | return crossProduct(a.x, a.y, b.x, b.y, c.x, c.y); 38 | } 39 | 40 | /** 41 | * calculates cross product of vectors AB and AC 42 | * 43 | * @param ax X coordinate of point A 44 | * @param ay Y coordinate of point A 45 | * @param bx X coordinate of point B 46 | * @param by Y coordinate of point B 47 | * @param cx X coordinate of point C 48 | * @param cy Y coordinate of point C 49 | * @return cross product AB * AC 50 | */ 51 | private static float crossProduct(float ax, float ay, float bx, float by, float cx, float cy) { 52 | return (ax - cx) * (by - cy) - (bx - cx) * (ay - cy); 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/team/uptech/motionviews/viewmodel/Font.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews.viewmodel; 2 | 3 | public class Font { 4 | 5 | /** 6 | * color value (ex: 0xFF00FF) 7 | */ 8 | private int color; 9 | /** 10 | * name of the font 11 | */ 12 | private String typeface; 13 | /** 14 | * size of the font, relative to parent 15 | */ 16 | private float size; 17 | 18 | public Font() { 19 | } 20 | 21 | public void increaseSize(float diff) { 22 | size = size + diff; 23 | } 24 | 25 | public void decreaseSize(float diff) { 26 | if (size - diff >= Limits.MIN_FONT_SIZE) { 27 | size = size - diff; 28 | } 29 | } 30 | 31 | public int getColor() { 32 | return color; 33 | } 34 | 35 | public void setColor(int color) { 36 | this.color = color; 37 | } 38 | 39 | public String getTypeface() { 40 | return typeface; 41 | } 42 | 43 | public void setTypeface(String typeface) { 44 | this.typeface = typeface; 45 | } 46 | 47 | public float getSize() { 48 | return size; 49 | } 50 | 51 | public void setSize(float size) { 52 | this.size = size; 53 | } 54 | 55 | private interface Limits { 56 | float MIN_FONT_SIZE = 0.01F; 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/team/uptech/motionviews/viewmodel/Layer.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews.viewmodel; 2 | 3 | import android.support.annotation.FloatRange; 4 | 5 | public class Layer { 6 | 7 | /** 8 | * rotation relative to the layer center, in degrees 9 | */ 10 | @FloatRange(from = 0.0F, to = 360.0F) 11 | private float rotationInDegrees; 12 | 13 | private float scale; 14 | /** 15 | * top left X coordinate, relative to parent canvas 16 | */ 17 | private float x; 18 | /** 19 | * top left Y coordinate, relative to parent canvas 20 | */ 21 | private float y; 22 | /** 23 | * is layer flipped horizontally (by X-coordinate) 24 | */ 25 | private boolean isFlipped; 26 | 27 | public Layer() { 28 | reset(); 29 | } 30 | 31 | protected void reset() { 32 | this.rotationInDegrees = 0.0F; 33 | this.scale = 1.0F; 34 | this.isFlipped = false; 35 | this.x = 0.0F; 36 | this.y = 0.0F; 37 | } 38 | 39 | public void postScale(float scaleDiff) { 40 | float newVal = scale + scaleDiff; 41 | if (newVal >= getMinScale() && newVal <= getMaxScale()) { 42 | scale = newVal; 43 | } 44 | } 45 | 46 | protected float getMaxScale() { 47 | return Limits.MAX_SCALE; 48 | } 49 | 50 | protected float getMinScale() { 51 | return Limits.MIN_SCALE; 52 | } 53 | 54 | public void postRotate(float rotationInDegreesDiff) { 55 | this.rotationInDegrees += rotationInDegreesDiff; 56 | this.rotationInDegrees %= 360.0F; 57 | } 58 | 59 | public void postTranslate(float dx, float dy) { 60 | this.x += dx; 61 | this.y += dy; 62 | } 63 | 64 | public void flip() { 65 | this.isFlipped = !isFlipped; 66 | } 67 | 68 | public float initialScale() { 69 | return Limits.INITIAL_ENTITY_SCALE; 70 | } 71 | 72 | public float getRotationInDegrees() { 73 | return rotationInDegrees; 74 | } 75 | 76 | public void setRotationInDegrees(@FloatRange(from = 0.0, to = 360.0) float rotationInDegrees) { 77 | this.rotationInDegrees = rotationInDegrees; 78 | } 79 | 80 | public float getScale() { 81 | return scale; 82 | } 83 | 84 | public void setScale(float scale) { 85 | this.scale = scale; 86 | } 87 | 88 | public float getX() { 89 | return x; 90 | } 91 | 92 | public void setX(float x) { 93 | this.x = x; 94 | } 95 | 96 | public float getY() { 97 | return y; 98 | } 99 | 100 | public void setY(float y) { 101 | this.y = y; 102 | } 103 | 104 | public boolean isFlipped() { 105 | return isFlipped; 106 | } 107 | 108 | public void setFlipped(boolean flipped) { 109 | isFlipped = flipped; 110 | } 111 | 112 | interface Limits { 113 | float MIN_SCALE = 0.06F; 114 | float MAX_SCALE = 4.0F; 115 | float INITIAL_ENTITY_SCALE = 0.4F; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/team/uptech/motionviews/viewmodel/TextLayer.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews.viewmodel; 2 | 3 | public class TextLayer extends Layer { 4 | 5 | private String text; 6 | private Font font; 7 | 8 | public TextLayer() { 9 | } 10 | 11 | @Override 12 | protected void reset() { 13 | super.reset(); 14 | this.text = ""; 15 | this.font = new Font(); 16 | } 17 | 18 | @Override 19 | protected float getMaxScale() { 20 | return Limits.MAX_SCALE; 21 | } 22 | 23 | @Override 24 | protected float getMinScale() { 25 | return Limits.MIN_SCALE; 26 | } 27 | 28 | @Override 29 | public float initialScale() { 30 | return Limits.INITIAL_SCALE; 31 | } 32 | 33 | public String getText() { 34 | return text; 35 | } 36 | 37 | public void setText(String text) { 38 | this.text = text; 39 | } 40 | 41 | public Font getFont() { 42 | return font; 43 | } 44 | 45 | public void setFont(Font font) { 46 | this.font = font; 47 | } 48 | 49 | public interface Limits { 50 | /** 51 | * limit text size to view bounds 52 | * so that users don't put small font size and scale it 100+ times 53 | */ 54 | float MAX_SCALE = 1.0F; 55 | float MIN_SCALE = 0.2F; 56 | 57 | float MIN_BITMAP_HEIGHT = 0.13F; 58 | 59 | float FONT_SIZE_STEP = 0.008F; 60 | 61 | float INITIAL_FONT_SIZE = 0.075F; 62 | int INITIAL_FONT_COLOR = 0xff000000; 63 | 64 | float INITIAL_SCALE = 0.8F; // set the same to avoid text scaling 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/team/uptech/motionviews/widget/MotionView.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews.widget; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.graphics.PointF; 10 | import android.os.Build; 11 | import android.support.annotation.NonNull; 12 | import android.support.annotation.Nullable; 13 | import android.support.v4.content.ContextCompat; 14 | import android.support.v4.view.GestureDetectorCompat; 15 | import android.util.AttributeSet; 16 | import android.view.GestureDetector; 17 | import android.view.MotionEvent; 18 | import android.view.ScaleGestureDetector; 19 | import android.view.View; 20 | import android.widget.FrameLayout; 21 | 22 | import com.almeros.android.multitouch.MoveGestureDetector; 23 | import com.almeros.android.multitouch.RotateGestureDetector; 24 | 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | 28 | import team.uptech.motionviews.R; 29 | import team.uptech.motionviews.widget.entity.MotionEntity; 30 | 31 | /** 32 | * Created on 9/29/16. 33 | */ 34 | 35 | public class MotionView extends FrameLayout { 36 | 37 | private static final String TAG = MotionView.class.getSimpleName(); 38 | 39 | public interface Constants { 40 | float SELECTED_LAYER_ALPHA = 0.15F; 41 | } 42 | 43 | public interface MotionViewCallback { 44 | void onEntitySelected(@Nullable MotionEntity entity); 45 | void onEntityDoubleTap(@NonNull MotionEntity entity); 46 | } 47 | 48 | // layers 49 | private final List entities = new ArrayList<>(); 50 | @Nullable 51 | private MotionEntity selectedEntity; 52 | 53 | private Paint selectedLayerPaint; 54 | 55 | // callback 56 | @Nullable 57 | private MotionViewCallback motionViewCallback; 58 | 59 | // gesture detection 60 | private ScaleGestureDetector scaleGestureDetector; 61 | private RotateGestureDetector rotateGestureDetector; 62 | private MoveGestureDetector moveGestureDetector; 63 | private GestureDetectorCompat gestureDetectorCompat; 64 | 65 | // constructors 66 | public MotionView(Context context) { 67 | super(context); 68 | init(context); 69 | } 70 | 71 | public MotionView(Context context, AttributeSet attrs) { 72 | super(context, attrs); 73 | init(context); 74 | } 75 | 76 | public MotionView(Context context, AttributeSet attrs, int defStyleAttr) { 77 | super(context, attrs, defStyleAttr); 78 | init(context); 79 | } 80 | 81 | @SuppressWarnings("unused") 82 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 83 | public MotionView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 84 | super(context, attrs, defStyleAttr, defStyleRes); 85 | init(context); 86 | } 87 | 88 | private void init(@NonNull Context context) { 89 | // I fucking love Android 90 | setWillNotDraw(false); 91 | 92 | selectedLayerPaint = new Paint(); 93 | selectedLayerPaint.setAlpha((int) (255 * Constants.SELECTED_LAYER_ALPHA)); 94 | selectedLayerPaint.setAntiAlias(true); 95 | 96 | // init listeners 97 | this.scaleGestureDetector = new ScaleGestureDetector(context, new ScaleListener()); 98 | this.rotateGestureDetector = new RotateGestureDetector(context, new RotateListener()); 99 | this.moveGestureDetector = new MoveGestureDetector(context, new MoveListener()); 100 | this.gestureDetectorCompat = new GestureDetectorCompat(context, new TapsListener()); 101 | 102 | setOnTouchListener(onTouchListener); 103 | 104 | updateUI(); 105 | } 106 | 107 | public MotionEntity getSelectedEntity() { 108 | return selectedEntity; 109 | } 110 | 111 | public List getEntities() { 112 | return entities; 113 | } 114 | 115 | public void setMotionViewCallback(@Nullable MotionViewCallback callback) { 116 | this.motionViewCallback = callback; 117 | } 118 | 119 | public void addEntity(@Nullable MotionEntity entity) { 120 | if (entity != null) { 121 | entities.add(entity); 122 | selectEntity(entity, false); 123 | } 124 | } 125 | 126 | public void addEntityAndPosition(@Nullable MotionEntity entity) { 127 | if (entity != null) { 128 | initEntityBorder(entity); 129 | initialTranslateAndScale(entity); 130 | entities.add(entity); 131 | selectEntity(entity, true); 132 | } 133 | } 134 | 135 | private void initEntityBorder(@NonNull MotionEntity entity ) { 136 | // init stroke 137 | int strokeSize = getResources().getDimensionPixelSize(R.dimen.stroke_size); 138 | Paint borderPaint = new Paint(); 139 | borderPaint.setStrokeWidth(strokeSize); 140 | borderPaint.setAntiAlias(true); 141 | borderPaint.setColor(ContextCompat.getColor(getContext(), R.color.stroke_color)); 142 | 143 | entity.setBorderPaint(borderPaint); 144 | } 145 | 146 | @Override 147 | protected void dispatchDraw(Canvas canvas) { 148 | super.dispatchDraw(canvas); 149 | 150 | // dispatch draw is called after child views is drawn. 151 | // the idea that is we draw background stickers, than child views (if any), and than selected item 152 | // to draw on top of child views - do it in dispatchDraw(Canvas) 153 | // to draw below that - do it in onDraw(Canvas) 154 | if (selectedEntity != null) { 155 | selectedEntity.draw(canvas, selectedLayerPaint); 156 | } 157 | } 158 | 159 | @Override 160 | protected void onDraw(Canvas canvas) { 161 | drawAllEntities(canvas); 162 | super.onDraw(canvas); 163 | } 164 | 165 | /** 166 | * draws all entities on the canvas 167 | * @param canvas Canvas where to draw all entities 168 | */ 169 | private void drawAllEntities(Canvas canvas) { 170 | for (int i = 0; i < entities.size(); i++) { 171 | entities.get(i).draw(canvas, null); 172 | } 173 | } 174 | 175 | /** 176 | * as a side effect - the method deselects Entity (if any selected) 177 | * @return bitmap with all the Entities at their current positions 178 | */ 179 | public Bitmap getThumbnailImage() { 180 | selectEntity(null, false); 181 | 182 | Bitmap bmp = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); 183 | // IMPORTANT: always create white background, cos if the image is saved in JPEG format, 184 | // which doesn't have transparent pixels, the background will be black 185 | bmp.eraseColor(Color.WHITE); 186 | Canvas canvas = new Canvas(bmp); 187 | drawAllEntities(canvas); 188 | 189 | return bmp; 190 | } 191 | 192 | private void updateUI() { 193 | invalidate(); 194 | } 195 | 196 | private void handleTranslate(PointF delta) { 197 | if (selectedEntity != null) { 198 | float newCenterX = selectedEntity.absoluteCenterX() + delta.x; 199 | float newCenterY = selectedEntity.absoluteCenterY() + delta.y; 200 | // limit entity center to screen bounds 201 | boolean needUpdateUI = false; 202 | if (newCenterX >= 0 && newCenterX <= getWidth()) { 203 | selectedEntity.getLayer().postTranslate(delta.x / getWidth(), 0.0F); 204 | needUpdateUI = true; 205 | } 206 | if (newCenterY >= 0 && newCenterY <= getHeight()) { 207 | selectedEntity.getLayer().postTranslate(0.0F, delta.y / getHeight()); 208 | needUpdateUI = true; 209 | } 210 | if (needUpdateUI) { 211 | updateUI(); 212 | } 213 | } 214 | } 215 | 216 | private void initialTranslateAndScale(@NonNull MotionEntity entity) { 217 | entity.moveToCanvasCenter(); 218 | entity.getLayer().setScale(entity.getLayer().initialScale()); 219 | } 220 | 221 | private void selectEntity(@Nullable MotionEntity entity, boolean updateCallback) { 222 | if (selectedEntity != null) { 223 | selectedEntity.setIsSelected(false); 224 | } 225 | if (entity != null) { 226 | entity.setIsSelected(true); 227 | } 228 | selectedEntity = entity; 229 | invalidate(); 230 | if (updateCallback && motionViewCallback != null) { 231 | motionViewCallback.onEntitySelected(entity); 232 | } 233 | } 234 | 235 | public void unselectEntity() { 236 | if (selectedEntity != null) { 237 | selectEntity(null, true); 238 | } 239 | } 240 | 241 | @Nullable 242 | private MotionEntity findEntityAtPoint(float x, float y) { 243 | MotionEntity selected = null; 244 | PointF p = new PointF(x, y); 245 | for (int i = entities.size() - 1; i >= 0; i--) { 246 | if (entities.get(i).pointInLayerRect(p)) { 247 | selected = entities.get(i); 248 | break; 249 | } 250 | } 251 | return selected; 252 | } 253 | 254 | private void updateSelectionOnTap(MotionEvent e) { 255 | MotionEntity entity = findEntityAtPoint(e.getX(), e.getY()); 256 | selectEntity(entity, true); 257 | } 258 | 259 | private void updateOnLongPress(MotionEvent e) { 260 | // if layer is currently selected and point inside layer - move it to front 261 | if (selectedEntity != null) { 262 | PointF p = new PointF(e.getX(), e.getY()); 263 | if (selectedEntity.pointInLayerRect(p)) { 264 | bringLayerToFront(selectedEntity); 265 | } 266 | } 267 | } 268 | 269 | private void bringLayerToFront(@NonNull MotionEntity entity) { 270 | // removing and adding brings layer to front 271 | if (entities.remove(entity)) { 272 | entities.add(entity); 273 | invalidate(); 274 | } 275 | } 276 | 277 | private void moveEntityToBack(@Nullable MotionEntity entity) { 278 | if (entity == null) { 279 | return; 280 | } 281 | if (entities.remove(entity)) { 282 | entities.add(0, entity); 283 | invalidate(); 284 | } 285 | } 286 | 287 | public void flipSelectedEntity() { 288 | if (selectedEntity == null) { 289 | return; 290 | } 291 | selectedEntity.getLayer().flip(); 292 | invalidate(); 293 | } 294 | 295 | public void moveSelectedBack() { 296 | moveEntityToBack(selectedEntity); 297 | } 298 | 299 | public void deletedSelectedEntity() { 300 | if (selectedEntity == null) { 301 | return; 302 | } 303 | if (entities.remove(selectedEntity)) { 304 | selectedEntity.release(); 305 | selectedEntity = null; 306 | invalidate(); 307 | } 308 | } 309 | 310 | // memory 311 | public void release() { 312 | for (MotionEntity entity : entities) { 313 | entity.release(); 314 | } 315 | } 316 | 317 | // gesture detectors 318 | 319 | private final View.OnTouchListener onTouchListener = new View.OnTouchListener() { 320 | 321 | @Override 322 | public boolean onTouch(View v, MotionEvent event) { 323 | if (scaleGestureDetector != null) { 324 | scaleGestureDetector.onTouchEvent(event); 325 | rotateGestureDetector.onTouchEvent(event); 326 | moveGestureDetector.onTouchEvent(event); 327 | gestureDetectorCompat.onTouchEvent(event); 328 | } 329 | return true; 330 | } 331 | }; 332 | 333 | private class TapsListener extends GestureDetector.SimpleOnGestureListener { 334 | @Override 335 | public boolean onDoubleTap(MotionEvent e) { 336 | if (motionViewCallback != null && selectedEntity != null) { 337 | motionViewCallback.onEntityDoubleTap(selectedEntity); 338 | } 339 | return true; 340 | } 341 | 342 | @Override 343 | public void onLongPress(MotionEvent e) { 344 | updateOnLongPress(e); 345 | } 346 | 347 | @Override 348 | public boolean onSingleTapUp(MotionEvent e) { 349 | updateSelectionOnTap(e); 350 | return true; 351 | } 352 | } 353 | 354 | private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { 355 | @Override 356 | public boolean onScale(ScaleGestureDetector detector) { 357 | if (selectedEntity != null) { 358 | float scaleFactorDiff = detector.getScaleFactor(); 359 | selectedEntity.getLayer().postScale(scaleFactorDiff - 1.0F); 360 | updateUI(); 361 | } 362 | return true; 363 | } 364 | } 365 | 366 | private class RotateListener extends RotateGestureDetector.SimpleOnRotateGestureListener { 367 | @Override 368 | public boolean onRotate(RotateGestureDetector detector) { 369 | if (selectedEntity != null) { 370 | selectedEntity.getLayer().postRotate(-detector.getRotationDegreesDelta()); 371 | updateUI(); 372 | } 373 | return true; 374 | } 375 | } 376 | 377 | private class MoveListener extends MoveGestureDetector.SimpleOnMoveGestureListener { 378 | @Override 379 | public boolean onMove(MoveGestureDetector detector) { 380 | handleTranslate(detector.getFocusDelta()); 381 | return true; 382 | } 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /app/src/main/java/team/uptech/motionviews/widget/entity/ImageEntity.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews.widget.entity; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | import android.support.annotation.IntRange; 7 | import android.support.annotation.NonNull; 8 | import android.support.annotation.Nullable; 9 | 10 | import team.uptech.motionviews.viewmodel.Layer; 11 | 12 | public class ImageEntity extends MotionEntity { 13 | 14 | @NonNull 15 | private final Bitmap bitmap; 16 | 17 | public ImageEntity(@NonNull Layer layer, 18 | @NonNull Bitmap bitmap, 19 | @IntRange(from = 1) int canvasWidth, 20 | @IntRange(from = 1) int canvasHeight) { 21 | super(layer, canvasWidth, canvasHeight); 22 | 23 | this.bitmap = bitmap; 24 | float width = bitmap.getWidth(); 25 | float height = bitmap.getHeight(); 26 | 27 | float widthAspect = 1.0F * canvasWidth / width; 28 | float heightAspect = 1.0F * canvasHeight / height; 29 | // fit the smallest size 30 | holyScale = Math.min(widthAspect, heightAspect); 31 | 32 | // initial position of the entity 33 | srcPoints[0] = 0; srcPoints[1] = 0; 34 | srcPoints[2] = width; srcPoints[3] = 0; 35 | srcPoints[4] = width; srcPoints[5] = height; 36 | srcPoints[6] = 0; srcPoints[7] = height; 37 | srcPoints[8] = 0; srcPoints[8] = 0; 38 | } 39 | 40 | @Override 41 | public void drawContent(@NonNull Canvas canvas, @Nullable Paint drawingPaint) { 42 | canvas.drawBitmap(bitmap, matrix, drawingPaint); 43 | } 44 | 45 | @Override 46 | public int getWidth() { 47 | return bitmap.getWidth(); 48 | } 49 | 50 | @Override 51 | public int getHeight() { 52 | return bitmap.getHeight(); 53 | } 54 | 55 | @Override 56 | public void release() { 57 | if (!bitmap.isRecycled()) { 58 | bitmap.recycle(); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/team/uptech/motionviews/widget/entity/MotionEntity.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews.widget.entity; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Matrix; 5 | import android.graphics.Paint; 6 | import android.graphics.PointF; 7 | import android.support.annotation.IntRange; 8 | import android.support.annotation.NonNull; 9 | import android.support.annotation.Nullable; 10 | 11 | import team.uptech.motionviews.utils.MathUtils; 12 | import team.uptech.motionviews.viewmodel.Layer; 13 | 14 | @SuppressWarnings({"WeakerAccess"}) 15 | public abstract class MotionEntity { 16 | 17 | /** 18 | * data 19 | */ 20 | @NonNull 21 | protected final Layer layer; 22 | 23 | /** 24 | * transformation matrix for the entity 25 | */ 26 | protected final Matrix matrix = new Matrix(); 27 | /** 28 | * true - entity is selected and need to draw it's border 29 | * false - not selected, no need to draw it's border 30 | */ 31 | private boolean isSelected; 32 | 33 | /** 34 | * maximum scale of the initial image, so that 35 | * the entity still fits within the parent canvas 36 | */ 37 | protected float holyScale; 38 | 39 | /** 40 | * width of canvas the entity is drawn in 41 | */ 42 | @IntRange(from = 0) 43 | protected int canvasWidth; 44 | /** 45 | * height of canvas the entity is drawn in 46 | */ 47 | @IntRange(from = 0) 48 | protected int canvasHeight; 49 | 50 | /** 51 | * Destination points of the entity 52 | * 5 points. Size of array - 10; Starting upper left corner, clockwise 53 | * last point is the same as first to close the circle 54 | * NOTE: saved as a field variable in order to avoid creating array in draw()-like methods 55 | */ 56 | private final float[] destPoints = new float[10]; // x0, y0, x1, y1, x2, y2, x3, y3, x0, y0 57 | /** 58 | * Initial points of the entity 59 | * @see #destPoints 60 | */ 61 | protected final float[] srcPoints = new float[10]; // x0, y0, x1, y1, x2, y2, x3, y3, x0, y0 62 | 63 | @NonNull 64 | private Paint borderPaint = new Paint(); 65 | 66 | public MotionEntity(@NonNull Layer layer, 67 | @IntRange(from = 1) int canvasWidth, 68 | @IntRange(from = 1) int canvasHeight) { 69 | this.layer = layer; 70 | this.canvasWidth = canvasWidth; 71 | this.canvasHeight = canvasHeight; 72 | } 73 | 74 | private boolean isSelected() { 75 | return isSelected; 76 | } 77 | 78 | public void setIsSelected(boolean isSelected) { 79 | this.isSelected = isSelected; 80 | } 81 | 82 | /** 83 | * S - scale matrix, R - rotate matrix, T - translate matrix, 84 | * L - result transformation matrix 85 | *

86 | * The correct order of applying transformations is : L = S * R * T 87 | *

88 | * See more info: Game Dev: Transform Matrix multiplication order 89 | *

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

93 | * NOTE: postconcat (postScale, etc.) works the other way : M` = S * M, in order to use it 94 | * we'd need to reverse the order of applying 95 | * transformations : post holy scale -> postTranslate -> postRotate -> postScale 96 | */ 97 | protected void updateMatrix() { 98 | // init matrix to E - identity matrix 99 | matrix.reset(); 100 | 101 | float topLeftX = layer.getX() * canvasWidth; 102 | float topLeftY = layer.getY() * canvasHeight; 103 | 104 | float centerX = topLeftX + getWidth() * holyScale * 0.5F; 105 | float centerY = topLeftY + getHeight() * holyScale * 0.5F; 106 | 107 | // calculate params 108 | float rotationInDegree = layer.getRotationInDegrees(); 109 | float scaleX = layer.getScale(); 110 | float scaleY = layer.getScale(); 111 | if (layer.isFlipped()) { 112 | // flip (by X-coordinate) if needed 113 | rotationInDegree *= -1.0F; 114 | scaleX *= -1.0F; 115 | } 116 | 117 | // applying transformations : L = S * R * T 118 | 119 | // scale 120 | matrix.preScale(scaleX, scaleY, centerX, centerY); 121 | 122 | // rotate 123 | matrix.preRotate(rotationInDegree, centerX, centerY); 124 | 125 | // translate 126 | matrix.preTranslate(topLeftX, topLeftY); 127 | 128 | // applying holy scale - S`, the result will be : L = S * R * T * S` 129 | matrix.preScale(holyScale, holyScale); 130 | } 131 | 132 | public float absoluteCenterX() { 133 | float topLeftX = layer.getX() * canvasWidth; 134 | return topLeftX + getWidth() * holyScale * 0.5F; 135 | } 136 | 137 | public float absoluteCenterY() { 138 | float topLeftY = layer.getY() * canvasHeight; 139 | 140 | return topLeftY + getHeight() * holyScale * 0.5F; 141 | } 142 | 143 | public PointF absoluteCenter() { 144 | float topLeftX = layer.getX() * canvasWidth; 145 | float topLeftY = layer.getY() * canvasHeight; 146 | 147 | float centerX = topLeftX + getWidth() * holyScale * 0.5F; 148 | float centerY = topLeftY + getHeight() * holyScale * 0.5F; 149 | 150 | return new PointF(centerX, centerY); 151 | } 152 | 153 | public void moveToCanvasCenter() { 154 | moveCenterTo(new PointF(canvasWidth * 0.5F, canvasHeight * 0.5F)); 155 | } 156 | 157 | public void moveCenterTo(PointF moveToCenter) { 158 | PointF currentCenter = absoluteCenter(); 159 | layer.postTranslate(1.0F * (moveToCenter.x - currentCenter.x) / canvasWidth, 160 | 1.0F * (moveToCenter.y - currentCenter.y) / canvasHeight); 161 | } 162 | 163 | private final PointF pA = new PointF(); 164 | private final PointF pB = new PointF(); 165 | private final PointF pC = new PointF(); 166 | private final PointF pD = new PointF(); 167 | 168 | /** 169 | * For more info: 170 | * StackOverflow: How to check point is in rectangle 171 | *

NOTE: it's easier to apply the same transformation matrix (calculated before) to the original source points, rather than 172 | * calculate the result points ourselves 173 | * @param point point 174 | * @return true if point (x, y) is inside the triangle 175 | */ 176 | public boolean pointInLayerRect(PointF point) { 177 | 178 | updateMatrix(); 179 | // map rect vertices 180 | matrix.mapPoints(destPoints, srcPoints); 181 | 182 | pA.x = destPoints[0]; 183 | pA.y = destPoints[1]; 184 | pB.x = destPoints[2]; 185 | pB.y = destPoints[3]; 186 | pC.x = destPoints[4]; 187 | pC.y = destPoints[5]; 188 | pD.x = destPoints[6]; 189 | pD.y = destPoints[7]; 190 | 191 | return MathUtils.pointInTriangle(point, pA, pB, pC) || MathUtils.pointInTriangle(point, pA, pD, pC); 192 | } 193 | 194 | /** 195 | * http://judepereira.com/blog/calculate-the-real-scale-factor-and-the-angle-of-rotation-from-an-android-matrix/ 196 | * 197 | * @param canvas Canvas to draw 198 | * @param drawingPaint Paint to use during drawing 199 | */ 200 | public final void draw(@NonNull Canvas canvas, @Nullable Paint drawingPaint) { 201 | 202 | updateMatrix(); 203 | 204 | canvas.save(); 205 | 206 | drawContent(canvas, drawingPaint); 207 | 208 | if (isSelected()) { 209 | // get alpha from drawingPaint 210 | int storedAlpha = borderPaint.getAlpha(); 211 | if (drawingPaint != null) { 212 | borderPaint.setAlpha(drawingPaint.getAlpha()); 213 | } 214 | drawSelectedBg(canvas); 215 | // restore border alpha 216 | borderPaint.setAlpha(storedAlpha); 217 | } 218 | 219 | canvas.restore(); 220 | } 221 | 222 | private void drawSelectedBg(Canvas canvas) { 223 | matrix.mapPoints(destPoints, srcPoints); 224 | //noinspection Range 225 | canvas.drawLines(destPoints, 0, 8, borderPaint); 226 | //noinspection Range 227 | canvas.drawLines(destPoints, 2, 8, borderPaint); 228 | } 229 | 230 | @NonNull 231 | public Layer getLayer() { 232 | return layer; 233 | } 234 | 235 | public void setBorderPaint(@NonNull Paint borderPaint) { 236 | this.borderPaint = borderPaint; 237 | } 238 | 239 | protected abstract void drawContent(@NonNull Canvas canvas, @Nullable Paint drawingPaint); 240 | 241 | public abstract int getWidth(); 242 | 243 | public abstract int getHeight(); 244 | 245 | public void release() { 246 | // free resources here 247 | } 248 | 249 | @Override 250 | protected void finalize() throws Throwable { 251 | try { 252 | release(); 253 | } finally { 254 | //noinspection ThrowFromFinallyBlock 255 | super.finalize(); 256 | } 257 | } 258 | } -------------------------------------------------------------------------------- /app/src/main/java/team/uptech/motionviews/widget/entity/TextEntity.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews.widget.entity; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Paint; 7 | import android.graphics.PointF; 8 | import android.support.annotation.IntRange; 9 | import android.support.annotation.NonNull; 10 | import android.support.annotation.Nullable; 11 | import android.text.Layout; 12 | import android.text.StaticLayout; 13 | import android.text.TextPaint; 14 | 15 | import team.uptech.motionviews.utils.FontProvider; 16 | import team.uptech.motionviews.viewmodel.TextLayer; 17 | 18 | public class TextEntity extends MotionEntity { 19 | 20 | private final TextPaint textPaint; 21 | private final FontProvider fontProvider; 22 | @Nullable 23 | private Bitmap bitmap; 24 | 25 | public TextEntity(@NonNull TextLayer textLayer, 26 | @IntRange(from = 1) int canvasWidth, 27 | @IntRange(from = 1) int canvasHeight, 28 | @NonNull FontProvider fontProvider) { 29 | super(textLayer, canvasWidth, canvasHeight); 30 | this.fontProvider = fontProvider; 31 | 32 | this.textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 33 | 34 | updateEntity(false); 35 | } 36 | 37 | private void updateEntity(boolean moveToPreviousCenter) { 38 | 39 | // save previous center 40 | PointF oldCenter = absoluteCenter(); 41 | 42 | Bitmap newBmp = createBitmap(getLayer(), bitmap); 43 | 44 | // recycle previous bitmap (if not reused) as soon as possible 45 | if (bitmap != null && bitmap != newBmp && !bitmap.isRecycled()) { 46 | bitmap.recycle(); 47 | } 48 | 49 | this.bitmap = newBmp; 50 | 51 | float width = bitmap.getWidth(); 52 | float height = bitmap.getHeight(); 53 | 54 | @SuppressWarnings("UnnecessaryLocalVariable") 55 | float widthAspect = 1.0F * canvasWidth / width; 56 | 57 | // for text we always match text width with parent width 58 | this.holyScale = widthAspect; 59 | 60 | // initial position of the entity 61 | srcPoints[0] = 0; 62 | srcPoints[1] = 0; 63 | srcPoints[2] = width; 64 | srcPoints[3] = 0; 65 | srcPoints[4] = width; 66 | srcPoints[5] = height; 67 | srcPoints[6] = 0; 68 | srcPoints[7] = height; 69 | srcPoints[8] = 0; 70 | srcPoints[8] = 0; 71 | 72 | if (moveToPreviousCenter) { 73 | // move to previous center 74 | moveCenterTo(oldCenter); 75 | } 76 | } 77 | 78 | /** 79 | * If reuseBmp is not null, and size of the new bitmap matches the size of the reuseBmp, 80 | * new bitmap won't be created, reuseBmp it will be reused instead 81 | * 82 | * @param textLayer text to draw 83 | * @param reuseBmp the bitmap that will be reused 84 | * @return bitmap with the text 85 | */ 86 | @NonNull 87 | private Bitmap createBitmap(@NonNull TextLayer textLayer, @Nullable Bitmap reuseBmp) { 88 | 89 | int boundsWidth = canvasWidth; 90 | 91 | // init params - size, color, typeface 92 | textPaint.setStyle(Paint.Style.FILL); 93 | textPaint.setTextSize(textLayer.getFont().getSize() * canvasWidth); 94 | textPaint.setColor(textLayer.getFont().getColor()); 95 | textPaint.setTypeface(fontProvider.getTypeface(textLayer.getFont().getTypeface())); 96 | 97 | // drawing text guide : http://ivankocijan.xyz/android-drawing-multiline-text-on-canvas/ 98 | // Static layout which will be drawn on canvas 99 | StaticLayout sl = new StaticLayout( 100 | textLayer.getText(), // - text which will be drawn 101 | textPaint, 102 | boundsWidth, // - width of the layout 103 | Layout.Alignment.ALIGN_CENTER, // - layout alignment 104 | 1, // 1 - text spacing multiply 105 | 1, // 1 - text spacing add 106 | true); // true - include padding 107 | 108 | // calculate height for the entity, min - Limits.MIN_BITMAP_HEIGHT 109 | int boundsHeight = sl.getHeight(); 110 | 111 | // create bitmap not smaller than TextLayer.Limits.MIN_BITMAP_HEIGHT 112 | int bmpHeight = (int) (canvasHeight * Math.max(TextLayer.Limits.MIN_BITMAP_HEIGHT, 113 | 1.0F * boundsHeight / canvasHeight)); 114 | 115 | // create bitmap where text will be drawn 116 | Bitmap bmp; 117 | if (reuseBmp != null && reuseBmp.getWidth() == boundsWidth 118 | && reuseBmp.getHeight() == bmpHeight) { 119 | // if previous bitmap exists, and it's width/height is the same - reuse it 120 | bmp = reuseBmp; 121 | bmp.eraseColor(Color.TRANSPARENT); // erase color when reusing 122 | } else { 123 | bmp = Bitmap.createBitmap(boundsWidth, bmpHeight, Bitmap.Config.ARGB_8888); 124 | } 125 | 126 | Canvas canvas = new Canvas(bmp); 127 | canvas.save(); 128 | 129 | // move text to center if bitmap is bigger that text 130 | if (boundsHeight < bmpHeight) { 131 | //calculate Y coordinate - In this case we want to draw the text in the 132 | //center of the canvas so we move Y coordinate to center. 133 | float textYCoordinate = (bmpHeight - boundsHeight) / 2; 134 | canvas.translate(0, textYCoordinate); 135 | } 136 | 137 | //draws static layout on canvas 138 | sl.draw(canvas); 139 | canvas.restore(); 140 | 141 | return bmp; 142 | } 143 | 144 | @Override 145 | @NonNull 146 | public TextLayer getLayer() { 147 | return (TextLayer) layer; 148 | } 149 | 150 | @Override 151 | protected void drawContent(@NonNull Canvas canvas, @Nullable Paint drawingPaint) { 152 | if (bitmap != null) { 153 | canvas.drawBitmap(bitmap, matrix, drawingPaint); 154 | } 155 | } 156 | 157 | @Override 158 | public int getWidth() { 159 | return bitmap != null ? bitmap.getWidth() : 0; 160 | } 161 | 162 | @Override 163 | public int getHeight() { 164 | return bitmap != null ? bitmap.getHeight() : 0; 165 | } 166 | 167 | public void updateEntity() { 168 | updateEntity(true); 169 | } 170 | 171 | @Override 172 | public void release() { 173 | if (bitmap != null && !bitmap.isRecycled()) { 174 | bitmap.recycle(); 175 | } 176 | } 177 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi/ic_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi/ic_add_text.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi/ic_format_color_text.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi/ic_mode_edit.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi/ic_neg_1.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi/ic_plus_1.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi/ic_text_fields.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/abra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/abra.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/bellsprout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/bellsprout.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/bracelet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/bracelet.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/bullbasaur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/bullbasaur.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/camera.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/candy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/candy.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/caterpie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/caterpie.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/charmander.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/charmander.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/mankey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/mankey.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/map.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/mega_ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/mega_ball.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/meowth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/meowth.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/pawprints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/pawprints.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/pidgey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/pidgey.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/pikachu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/pikachu.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/pikachu_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/pikachu_1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/pikachu_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/pikachu_2.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/player.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/pointer.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/pokebag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/pokebag.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/pokeball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/pokeball.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/pokeballs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/pokeballs.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/pokecoin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/pokecoin.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/pokedex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/pokedex.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/potion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/potion.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/psyduck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/psyduck.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/rattata.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/rattata.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/revive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/revive.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/squirtle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/squirtle.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/star.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/star_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/star_1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/superball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/superball.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/tornado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/tornado.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/venonat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/venonat.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/weedle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/weedle.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/zubat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/drawable-nodpi/zubat.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 15 | 16 | 21 | 22 | 29 | 30 | 37 | 38 | 45 | 46 | 53 | 54 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/res/layout/select_sticker_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/sticker_item.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/text_editor_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main_menu.xml: -------------------------------------------------------------------------------- 1 | 2 |

4 | 5 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uptechteam/MotionViews-Android/5d0968f96af36076faf10b5bd06179e6b11ff11d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #2b231d 4 | #181411 5 | #fed136 6 | 7 | 8 | #8cf437 9 | 10 | #00FFFFFF 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 3dp 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Motion Views 3 | Add Sticker 4 | Add Text 5 | 6 | Select color 7 | OK 8 | Cancel 9 | Select font 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/team/uptech/motionviews/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package team.uptech.motionviews; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.3' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | maven { url "https://jitpack.io" } 19 | } 20 | } 21 | 22 | task clean(type: Delete) { 23 | delete rootProject.buildDir 24 | } 25 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------