├── .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 | 
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 |
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
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
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
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 |