├── .gitignore
├── LICENSE.txt
├── README.md
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── library
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── jmedeisis
│ │ └── windowview
│ │ ├── WindowView.java
│ │ └── sensor
│ │ ├── ExponentialSmoothingFilter.java
│ │ ├── Filter.java
│ │ └── TiltSensor.java
│ └── res
│ └── values
│ └── attrs.xml
├── sample-debug
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── example
│ │ └── windowviewdebug
│ │ ├── DebugActivity.java
│ │ └── DebugWindowView.java
│ └── res
│ ├── drawable-hdpi
│ └── ic_launcher.png
│ ├── drawable-mdpi
│ └── ic_launcher.png
│ ├── drawable-nodpi
│ ├── device_frame.png
│ ├── device_z.png
│ ├── london_wide.jpg
│ └── singapore_tall.jpg
│ ├── drawable-xhdpi
│ └── ic_launcher.png
│ ├── drawable-xxhdpi
│ └── ic_launcher.png
│ ├── layout-land
│ └── activity_debug.xml
│ ├── layout
│ ├── activity_debug.xml
│ └── device_compass.xml
│ ├── menu
│ └── menu_debug.xml
│ └── values
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
├── sample
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── sample_in_action.gif
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── example
│ │ └── windowview
│ │ └── DemoActivity.java
│ └── res
│ ├── drawable-hdpi
│ └── ic_launcher.png
│ ├── drawable-mdpi
│ └── ic_launcher.png
│ ├── drawable-nodpi
│ ├── london_wide.jpg
│ └── singapore_tall.jpg
│ ├── drawable-xhdpi
│ └── ic_launcher.png
│ ├── drawable-xxhdpi
│ └── ic_launcher.png
│ ├── layout-land
│ └── activity_demo.xml
│ ├── layout
│ └── activity_demo.xml
│ └── values
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | /.idea
4 | *.iml
5 | .DS_Store
6 | /build
7 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Justas Medeisis
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 | WindowView
2 | ==========
3 |
4 | *Window as in windowsill.*
5 |
6 | 
7 |
8 | An Android `ImageView` that can be panned around by tilting your device, as if you were looking
9 | through a window.
10 |
11 | Usage
12 | -----
13 | Add it to your project using Gradle:
14 |
15 | ```groovy
16 | compile 'com.jmedeisis:windowview:0.2.0'
17 | ```
18 |
19 | Use in place of an `ImageView`. Example XML layout file:
20 |
21 | ```xml
22 |
26 | ```
27 |
28 | Please refer to the included [sample application project](sample/) for a complete example.
29 |
30 | Configuration
31 | -------------
32 | You will typically want to configure the following attributes for the `WindowView` class:
33 |
34 | - `wwv_max_pitch` - maximum angle (in degrees) from origin for vertical device tilts.
35 | *Default - 30°*
36 |
37 | - `wwv_max_roll` - maximum angle (in degrees) from origin for horizontal device tilts.
38 | *Default - 30°*
39 |
40 | - `wwv_vertical_origin` - (in degrees) when device pitch equals this value, the image is centered
41 | vertically. *Default - 0°*
42 |
43 | - `wwv_horizontal_origin` - (in degrees) when device roll equals this value, the image is centered
44 | horizontally. *Default - 0°*
45 |
46 | You may also want to configure more advanced attributes:
47 |
48 | - `wwv_orientation_mode` - `Absolute` or `Relative` (default). Specifies whether device tilt should
49 | be tracked with respect to `Absolute` world coordinates (i.e. pitch, roll w.r.t. ground plane) or
50 | with respect to the device orientation when `WindowView` is created, which `WindowView` refers to as
51 | the 'orientation origin'. If using the latter, i.e. `Relative`, you may use
52 | `WindowView#resetOrientationOrigin(boolean)` to set the orientation origin to that of the device
53 | when the method is called.
54 |
55 | - `wwv_translate_mode` - `Constant` or `Proportional` (default). Specifies how much the image is
56 | translated in response to device tilt. If `Proportional`, the image moves within the full range
57 | defined by `max_pitch` / `max_roll`, with the extremities of the image visible when device pitch /
58 | roll is at those angles. If `Constant`, the image moves a constant amount per unit of tilt which is
59 | defined by `max_constant_translation`, achieved when pitch / roll are at `max_pitch` / `max_roll`.
60 |
61 | - `wwv_max_constant_translation` - see above. *Default - 150dp*
62 |
63 | - `wwv_sensor_sampling_period` - the desired rate of sensor events. In microseconds or one of
64 | `fast`, `normal` (default) or `slow`. If using microsecond values, higher values result in slower
65 | sensor updates. Directly related to the rate at which `WindowView` updates in response to device
66 | tilt.
67 |
68 | - `wwv_tilt_sensor_mode` - `Manual` or `Automatic` (default). Specifies whether `WindowView` is
69 | responsible for when tilt motion tracking starts and stops. If `Automatic`, `WindowView` works out
70 | of the box and requires no extra configuration. If `Manual`, you must explicitly start and stop tilt
71 | motion tracking. You have two options:
72 | * Use `WindowView#startTiltTracking()` and `WindowView#stopTiltTracking()`, e.g. in your
73 | `Activity`'s `onResume()` and `onPause()`, respectively.
74 |
75 | * Use `WindowView#attachTiltTracking(TiltSensor)` and
76 | `WindowView#detachTiltTracking(TiltSensor)`. This approach is recommended when using multiple
77 | `WindowView`s in a single logical layout. The externally managed `TiltSensor` should be started
78 | and stopped using `TiltSensor#startTracking(int)` and `TiltSensor#stopTracking()` as appropriate.
79 |
80 | Example configuration:
81 |
82 | ```xml
83 |
96 | ```
97 |
98 | Limitations
99 | -----------
100 | - Only supports the CENTER_CROP scale type.
101 | - Works for API levels 9+.
102 |
103 | Development
104 | -----------
105 | Pull requests are welcome and encouraged for bugfixes and features such as:
106 |
107 | - adaptive smoothing filters tuned for different sensor accuracy and rates
108 | - bi-directional image panning
109 |
110 | License
111 | -------
112 | WindowView is licensed under the terms of the [MIT License](LICENSE.txt).
--------------------------------------------------------------------------------
/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.1.0'
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 | }
19 | }
20 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Settings specified in this file will override any Gradle settings
5 | # configured through the IDE.
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 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Jun 18 20:09:31 EEST 2016
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.10-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 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'bintray-release'
3 |
4 | android {
5 | compileSdkVersion 24
6 | buildToolsVersion '24.0.0'
7 |
8 | defaultConfig {
9 | minSdkVersion 9
10 | versionCode 3
11 | versionName "0.2.0"
12 | }
13 | buildTypes {
14 | release {
15 | minifyEnabled false
16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
17 | }
18 | }
19 |
20 | resourcePrefix 'wwv_'
21 | }
22 |
23 | dependencies {
24 | compile fileTree(dir: 'libs', include: ['*.jar'])
25 | compile 'com.android.support:support-annotations:24.0.0'
26 | }
27 |
28 | buildscript {
29 | repositories {
30 | jcenter()
31 | }
32 | dependencies {
33 | classpath 'com.novoda:bintray-release:0.3.4'
34 | }
35 | }
36 |
37 | // see https://github.com/novoda/bintray-release/wiki/Configuration-of-the-publish-closure
38 | publish {
39 | userOrg = 'justasm'
40 | groupId = 'com.jmedeisis'
41 | artifactId = 'windowview'
42 | version = "0.2.0"
43 | licences = ['MIT']
44 | desc = "An Android ImageView you pan by tilting your device."
45 | website = 'https://github.com/justasm/WindowView'
46 | }
--------------------------------------------------------------------------------
/library/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 C:/Users/Justas/Desktop/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 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/library/src/main/java/com/jmedeisis/windowview/WindowView.java:
--------------------------------------------------------------------------------
1 | package com.jmedeisis.windowview;
2 |
3 | import android.annotation.TargetApi;
4 | import android.content.Context;
5 | import android.content.res.TypedArray;
6 | import android.graphics.Canvas;
7 | import android.graphics.drawable.Drawable;
8 | import android.hardware.Sensor;
9 | import android.hardware.SensorEventListener;
10 | import android.hardware.SensorManager;
11 | import android.os.Build;
12 | import android.support.annotation.NonNull;
13 | import android.util.AttributeSet;
14 | import android.widget.ImageView;
15 |
16 | import com.jmedeisis.windowview.sensor.TiltSensor;
17 |
18 | /**
19 | * An ImageView that automatically pans in response to device tilt.
20 | * Currently only supports {@link android.widget.ImageView.ScaleType#CENTER_CROP}.
21 | */
22 | public class WindowView extends ImageView implements TiltSensor.TiltListener {
23 |
24 | private static final float DEFAULT_MAX_PITCH_DEGREES = 30;
25 | private static final float DEFAULT_MAX_ROLL_DEGREES = 30;
26 | private static final float DEFAULT_HORIZONTAL_ORIGIN_DEGREES = 0;
27 | private static final float DEFAULT_VERTICAL_ORIGIN_DEGREES = 0;
28 | private float latestPitch;
29 | private float latestRoll;
30 | private float maxPitchDeg;
31 | private float maxRollDeg;
32 | private float horizontalOriginDeg;
33 | private float verticalOriginDeg;
34 |
35 | private static final int DEFAULT_SENSOR_SAMPLING_PERIOD_US = SensorManager.SENSOR_DELAY_GAME;
36 | private int sensorSamplingPeriod;
37 |
38 | /**
39 | * Determines the basis in which device orientation is measured.
40 | */
41 | public enum OrientationMode {
42 | /**
43 | * Measures absolute yaw / pitch / roll (i.e. relative to the world).
44 | */
45 | ABSOLUTE,
46 | /**
47 | * Measures yaw / pitch / roll relative to the starting orientation.
48 | * The starting orientation is determined upon receiving the first sensor data,
49 | * but can be manually reset at any time using {@link #resetOrientationOrigin(boolean)}.
50 | */
51 | RELATIVE
52 | }
53 |
54 | private static final OrientationMode DEFAULT_ORIENTATION_MODE = OrientationMode.RELATIVE;
55 | private OrientationMode orientationMode;
56 |
57 | private static final float DEFAULT_MAX_CONSTANT_TRANSLATION_DP = 150;
58 | private float maxConstantTranslation;
59 |
60 | /**
61 | * Determines the relationship between change in device tilt and change in image translation.
62 | */
63 | public enum TranslateMode {
64 | /**
65 | * The image is translated by a constant amount per unit of device tilt.
66 | * Generally preferable when viewing multiple adjacent WindowViews that have different
67 | * contents but should move in tandem.
68 | *
69 | * Same amount of tilt will result in the same translation for two images of differing size.
70 | */
71 | CONSTANT,
72 | /**
73 | * The image is translated proportional to its off-view size. Generally preferable when
74 | * viewing a single WindowView, this mode ensures that the full image can be 'explored'
75 | * within a fixed tilt amount range.
76 | *
77 | * Same amount of tilt will result in different translation for two images of differing size.
78 | */
79 | PROPORTIONAL
80 | }
81 |
82 | private static final TranslateMode DEFAULT_TRANSLATE_MODE = TranslateMode.PROPORTIONAL;
83 | private TranslateMode translateMode;
84 |
85 | /**
86 | * Determines when and how tilt motion tracking starts and stops.
87 | */
88 | public enum TiltSensorMode {
89 | /**
90 | * Tilt motion tracking is completely automated and requires no explicit intervention.
91 | * WindowView (un)registers for hardware motion sensor events during View lifecycle events
92 | * such as {@link #onAttachedToWindow()}, {@link #onDetachedFromWindow()} and
93 | * {@link #onWindowFocusChanged(boolean)}.
94 | *
95 | * Note that in this mode, each WindowView tracks motion events independently.
96 | */
97 | AUTOMATIC,
98 | /**
99 | * Tilt motion tracking must be manually initiated and stopped. There are two options:
100 | *
101 | * - Use {@link #startTiltTracking()} and {@link #stopTiltTracking()}.
102 | * Good candidate opportunities to do this are the container Activity's / Fragment's
103 | * onResume() and onPause() lifecycle events.
104 | * - Use {@link #attachTiltTracking(TiltSensor)} and
105 | * {@link #detachTiltTracking(TiltSensor)}. This mode is recommended when using multiple
106 | * WindowViews in a single logical layout. The externally managed {@link TiltSensor}
107 | * should be started and stopped using {@link TiltSensor#startTracking(int)} and
108 | * {@link TiltSensor#stopTracking()} as appropriate. Good candidate opportunities to do
109 | * this are the container Activity's / Fragment's onResume() and onPause() lifecycle
110 | * events.
111 | *
112 | *
113 | * Note that in this mode, care must be taken to stop motion tracking at the appropriate
114 | * lifecycle events to ensure that hardware sensors are detached and do not cause
115 | * unnecessary battery drain.
116 | */
117 | MANUAL
118 | }
119 |
120 | private static final TiltSensorMode DEFAULT_TILT_SENSOR_MODE = TiltSensorMode.AUTOMATIC;
121 | private TiltSensorMode tiltSensorMode;
122 |
123 | protected TiltSensor sensor;
124 |
125 | // layout
126 | protected boolean heightMatches;
127 | protected float widthDifference;
128 | protected float heightDifference;
129 |
130 | public WindowView(Context context) {
131 | super(context);
132 | init(context, null);
133 | }
134 |
135 | public WindowView(Context context, AttributeSet attrs) {
136 | super(context, attrs);
137 | init(context, attrs);
138 | }
139 |
140 | public WindowView(Context context, AttributeSet attrs, int defStyleAttr) {
141 | super(context, attrs, defStyleAttr);
142 | init(context, attrs);
143 | }
144 |
145 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
146 | public WindowView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
147 | super(context, attrs, defStyleAttr, defStyleRes);
148 | init(context, attrs);
149 | }
150 |
151 | protected void init(Context context, AttributeSet attrs) {
152 | sensorSamplingPeriod = DEFAULT_SENSOR_SAMPLING_PERIOD_US;
153 | maxPitchDeg = DEFAULT_MAX_PITCH_DEGREES;
154 | maxRollDeg = DEFAULT_MAX_ROLL_DEGREES;
155 | verticalOriginDeg = DEFAULT_VERTICAL_ORIGIN_DEGREES;
156 | horizontalOriginDeg = DEFAULT_HORIZONTAL_ORIGIN_DEGREES;
157 | tiltSensorMode = DEFAULT_TILT_SENSOR_MODE;
158 | orientationMode = DEFAULT_ORIENTATION_MODE;
159 | translateMode = DEFAULT_TRANSLATE_MODE;
160 | maxConstantTranslation = DEFAULT_MAX_CONSTANT_TRANSLATION_DP *
161 | getResources().getDisplayMetrics().density;
162 |
163 | if (null != attrs) {
164 | final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.wwv_WindowView);
165 | sensorSamplingPeriod = a.getInt(R.styleable.wwv_WindowView_wwv_sensor_sampling_period,
166 | sensorSamplingPeriod);
167 | maxPitchDeg = a.getFloat(R.styleable.wwv_WindowView_wwv_max_pitch, maxPitchDeg);
168 | maxRollDeg = a.getFloat(R.styleable.wwv_WindowView_wwv_max_roll, maxRollDeg);
169 | verticalOriginDeg = a.getFloat(R.styleable.wwv_WindowView_wwv_vertical_origin,
170 | verticalOriginDeg);
171 | horizontalOriginDeg = a.getFloat(R.styleable.wwv_WindowView_wwv_horizontal_origin,
172 | horizontalOriginDeg);
173 |
174 | int tiltSensorModeIndex = a.getInt(R.styleable.wwv_WindowView_wwv_tilt_sensor_mode, -1);
175 | if (tiltSensorModeIndex >= 0) {
176 | tiltSensorMode = TiltSensorMode.values()[tiltSensorModeIndex];
177 | }
178 | int orientationModeIndex = a.getInt(R.styleable.wwv_WindowView_wwv_orientation_mode, -1);
179 | if (orientationModeIndex >= 0) {
180 | orientationMode = OrientationMode.values()[orientationModeIndex];
181 | }
182 | int translateModeIndex = a.getInt(R.styleable.wwv_WindowView_wwv_translate_mode, -1);
183 | if (translateModeIndex >= 0) {
184 | translateMode = TranslateMode.values()[translateModeIndex];
185 | }
186 |
187 | maxConstantTranslation = a.getDimension(
188 | R.styleable.wwv_WindowView_wwv_max_constant_translation,
189 | maxConstantTranslation);
190 | a.recycle();
191 | }
192 |
193 | if (!isInEditMode() && TiltSensorMode.AUTOMATIC == tiltSensorMode) {
194 | initSensor();
195 | }
196 |
197 | setScaleType(ScaleType.CENTER_CROP);
198 | }
199 |
200 | /*
201 | * LIFE-CYCLE
202 | * Registering for sensor events should be tied to Activity / Fragment lifecycle events.
203 | * However, this would mean that WindowView cannot be independent. We tie into a few
204 | * lifecycle-esque View events that allow us to make WindowView completely independent.
205 | *
206 | * Un-registering from sensor events is done aggressively to minimise battery drain and
207 | * performance impact.
208 | * ---------------------------------------------------------------------------------------------
209 | */
210 | @Override
211 | public void onWindowFocusChanged(boolean hasWindowFocus) {
212 | super.onWindowFocusChanged(hasWindowFocus);
213 | if (null != sensor && TiltSensorMode.AUTOMATIC == tiltSensorMode) {
214 | if (hasWindowFocus) {
215 | sensor.startTracking(sensorSamplingPeriod);
216 | } else {
217 | sensor.stopTracking();
218 | }
219 | }
220 | }
221 |
222 | @Override
223 | protected void onAttachedToWindow() {
224 | super.onAttachedToWindow();
225 | if (!isInEditMode() && null != sensor && TiltSensorMode.AUTOMATIC == tiltSensorMode) {
226 | sensor.startTracking(sensorSamplingPeriod);
227 | }
228 | }
229 |
230 | @Override
231 | protected void onDetachedFromWindow() {
232 | super.onDetachedFromWindow();
233 | if (null != sensor && TiltSensorMode.AUTOMATIC == tiltSensorMode) {
234 | sensor.stopTracking();
235 | }
236 | }
237 |
238 | /*
239 | * DRAWING & LAYOUT
240 | * ---------------------------------------------------------------------------------------------
241 | */
242 | @Override
243 | protected void onDraw(@NonNull Canvas canvas) {
244 | // -1 -> 1
245 | float xOffset = 0f;
246 | float yOffset = 0f;
247 | if (heightMatches) {
248 | // only let user tilt horizontally
249 | xOffset = (-horizontalOriginDeg +
250 | clampAbsoluteFloating(horizontalOriginDeg, latestRoll, maxRollDeg)) / maxRollDeg;
251 | } else {
252 | // only let user tilt vertically
253 | yOffset = (verticalOriginDeg -
254 | clampAbsoluteFloating(verticalOriginDeg, latestPitch, maxPitchDeg)) / maxPitchDeg;
255 | }
256 | canvas.save();
257 | switch (translateMode) {
258 | case CONSTANT:
259 | canvas.translate(
260 | clampAbsoluteFloating(0, maxConstantTranslation * xOffset, widthDifference / 2),
261 | clampAbsoluteFloating(0, maxConstantTranslation * yOffset, heightDifference / 2));
262 | break;
263 | case PROPORTIONAL:
264 | canvas.translate(Math.round((widthDifference / 2) * xOffset),
265 | Math.round((heightDifference / 2) * yOffset));
266 | break;
267 | }
268 | super.onDraw(canvas);
269 | canvas.restore();
270 | }
271 |
272 | protected float clampAbsoluteFloating(float origin, float value, float maxAbsolute) {
273 | return value < origin ?
274 | Math.max(value, origin - maxAbsolute) : Math.min(value, origin + maxAbsolute);
275 | }
276 |
277 | /**
278 | * See {@link TranslateMode}.
279 | */
280 | public void setTranslateMode(TranslateMode translateMode) {
281 | this.translateMode = translateMode;
282 | }
283 |
284 | public TranslateMode getTranslateMode() {
285 | return translateMode;
286 | }
287 |
288 | /**
289 | * Maximum image translation from center when using {@link TranslateMode#CONSTANT}.
290 | */
291 | public void setMaxConstantTranslation(float maxConstantTranslation) {
292 | this.maxConstantTranslation = maxConstantTranslation;
293 | }
294 |
295 | public float getMaxConstantTranslation() {
296 | return maxConstantTranslation;
297 | }
298 |
299 | /**
300 | * Maximum angle (in degrees) from origin for vertical tilts.
301 | */
302 | public void setMaxPitch(float maxPitch) {
303 | this.maxPitchDeg = maxPitch;
304 | }
305 |
306 | public float getMaxPitch() {
307 | return maxPitchDeg;
308 | }
309 |
310 | /**
311 | * Maximum angle (in degrees) from origin for horizontal tilts.
312 | */
313 | public void setMaxRoll(float maxRoll) {
314 | this.maxRollDeg = maxRoll;
315 | }
316 |
317 | public float getMaxRoll() {
318 | return maxRollDeg;
319 | }
320 |
321 | /**
322 | * Horizontal origin (in degrees). When {@link #latestRoll} equals this value, the image
323 | * is centered horizontally.
324 | */
325 | public void setHorizontalOrigin(float horizontalOrigin) {
326 | this.horizontalOriginDeg = horizontalOrigin;
327 | }
328 |
329 | public float getHorizontalOrigin() {
330 | return horizontalOriginDeg;
331 | }
332 |
333 | /**
334 | * Vertical origin (in degrees). When {@link #latestPitch} equals this value, the image
335 | * is centered vertically.
336 | */
337 | public void setVerticalOrigin(float verticalOrigin) {
338 | this.verticalOriginDeg = verticalOrigin;
339 | }
340 |
341 | public float getVerticalOrigin() {
342 | return verticalOriginDeg;
343 | }
344 |
345 | @Override
346 | public void setImageDrawable(Drawable drawable) {
347 | super.setImageDrawable(drawable);
348 | recalculateImageDimensions();
349 | }
350 |
351 | @Override
352 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
353 | super.onSizeChanged(w, h, oldw, oldh);
354 | recalculateImageDimensions();
355 | }
356 |
357 | private void recalculateImageDimensions() {
358 | Drawable drawable = getDrawable();
359 | if (null == drawable) return;
360 |
361 | ScaleType scaleType = getScaleType();
362 | float width = getWidth();
363 | float height = getHeight();
364 | float imageWidth = drawable.getIntrinsicWidth();
365 | float imageHeight = drawable.getIntrinsicHeight();
366 |
367 | heightMatches = !widthRatioGreater(width, height, imageWidth, imageHeight);
368 |
369 | switch (scaleType) {
370 | case CENTER_CROP:
371 | if (heightMatches) {
372 | imageWidth *= height / imageHeight;
373 | imageHeight = height;
374 | } else {
375 | imageWidth = width;
376 | imageHeight *= width / imageWidth;
377 | }
378 | widthDifference = imageWidth - width;
379 | heightDifference = imageHeight - height;
380 | break;
381 | default:
382 | widthDifference = 0;
383 | heightDifference = 0;
384 | break;
385 | }
386 | }
387 |
388 | private static boolean widthRatioGreater(float width, float height,
389 | float otherWidth, float otherHeight) {
390 | return height / otherHeight < width / otherWidth;
391 | }
392 |
393 | @Override
394 | public void setScaleType(ScaleType scaleType) {
395 | if (ScaleType.CENTER_CROP != scaleType)
396 | throw new IllegalArgumentException("Image scale type " + scaleType +
397 | " is not supported by WindowView. Use CENTER_CROP instead.");
398 | super.setScaleType(scaleType);
399 | }
400 |
401 | /*
402 | * SENSOR DATA
403 | * ---------------------------------------------------------------------------------------------
404 | */
405 | public TiltSensorMode getTiltSensorMode() {
406 | return tiltSensorMode;
407 | }
408 |
409 | private void initSensor() {
410 | sensor = new TiltSensor(getContext(), orientationMode == OrientationMode.RELATIVE);
411 | sensor.addListener(this);
412 | }
413 |
414 | /**
415 | * If tilt motion tracking is not in progress, start it.
416 | */
417 | public void startTiltTracking() {
418 | if (null == sensor) {
419 | // this will be the case if tiltSensorMode == TiltSensorMode.MANUAL
420 | initSensor();
421 | } else if (sensor.isTracking()) {
422 | return;
423 | }
424 | sensor.startTracking(sensorSamplingPeriod);
425 | }
426 |
427 | /**
428 | * Stop tilt motion tracking.
429 | *
430 | * @throws IllegalStateException if {@link #getTiltSensorMode()} is {@link TiltSensorMode#MANUAL}
431 | * and {@link #startTiltTracking()} was not called prior.
432 | */
433 | public void stopTiltTracking() {
434 | if (null == sensor) {
435 | throw new IllegalStateException(
436 | "WindowView does not have its own tilt sensor, cannot stop tracking.");
437 | }
438 | sensor.stopTracking();
439 | }
440 |
441 | /**
442 | * Connect this WindowView to a separately managed TiltSensor. Alternative to calling
443 | * {@link #startTiltTracking()}. Calling {@link TiltSensor#startTracking(int)} is not the
444 | * responsibility of this view.
445 | *
446 | * @param externalSensor an externally managed {@link TiltSensor}.
447 | * @throws IllegalStateException if {@link #getTiltSensorMode()} is not
448 | * {@link TiltSensorMode#MANUAL}.
449 | */
450 | public void attachTiltTracking(TiltSensor externalSensor) {
451 | if (TiltSensorMode.MANUAL != tiltSensorMode) {
452 | // WindowView has its own tilt sensor, cannot attach external one.
453 | throw new IllegalStateException(
454 | "External tilt sensor can only be attached if tilt sensor mode is set to MANUAL.");
455 | }
456 | if (null != sensor) {
457 | // will be the case if #startTiltTracking() was called previously
458 | if (sensor.isTracking()) sensor.stopTracking();
459 | }
460 | externalSensor.addListener(this);
461 | }
462 |
463 | public void detachTiltTracking(TiltSensor externalSensor) {
464 | externalSensor.removeListener(this);
465 | }
466 |
467 | @Override
468 | public void onTiltUpdate(float yaw, float pitch, float roll) {
469 | this.latestPitch = pitch;
470 | this.latestRoll = roll;
471 | invalidate();
472 | }
473 |
474 | public void addTiltListener(TiltSensor.TiltListener listener) {
475 | if (null == sensor) {
476 | throw new IllegalStateException(
477 | "WindowView does not have its own tilt sensor, cannot add listener.");
478 | }
479 | sensor.addListener(listener);
480 | }
481 |
482 | public void removeTiltListener(TiltSensor.TiltListener listener) {
483 | if (null == sensor) {
484 | throw new IllegalStateException(
485 | "WindowView does not have its own tilt sensor, cannot remove listener.");
486 | }
487 | sensor.removeListener(listener);
488 | }
489 |
490 | /**
491 | * Manually resets the orientation origin. Has no effect unless {@link #getOrientationMode()}
492 | * is {@link OrientationMode#RELATIVE}.
493 | *
494 | * @param immediate if false, the sensor values smoothly interpolate to the new origin.
495 | */
496 | public void resetOrientationOrigin(boolean immediate) {
497 | if (null == sensor) {
498 | throw new IllegalStateException(
499 | "WindowView does not have its own tilt sensor, cannot reset orientation origin.");
500 | }
501 | sensor.resetOrigin(immediate);
502 | }
503 |
504 | /**
505 | * Determines the mapping of orientation to image offset.
506 | * See {@link OrientationMode}.
507 | */
508 | public void setOrientationMode(OrientationMode orientationMode) {
509 | this.orientationMode = orientationMode;
510 | if (null != sensor) {
511 | sensor.setTrackRelativeOrientation(orientationMode == OrientationMode.RELATIVE);
512 | sensor.resetOrigin(true);
513 | }
514 | }
515 |
516 | public OrientationMode getOrientationMode() {
517 | return orientationMode;
518 | }
519 |
520 | /**
521 | * @param samplingPeriodUs see {@link SensorManager#registerListener(SensorEventListener, Sensor, int)}
522 | */
523 | public void setSensorSamplingPeriod(int samplingPeriodUs) {
524 | this.sensorSamplingPeriod = samplingPeriodUs;
525 | if (null != sensor && sensor.isTracking()) {
526 | sensor.stopTracking();
527 | sensor.startTracking(this.sensorSamplingPeriod);
528 | }
529 | }
530 |
531 | /**
532 | * @return sensor sampling period (in microseconds).
533 | */
534 | public int getSensorSamplingPeriod() {
535 | return sensorSamplingPeriod;
536 | }
537 | }
538 |
--------------------------------------------------------------------------------
/library/src/main/java/com/jmedeisis/windowview/sensor/ExponentialSmoothingFilter.java:
--------------------------------------------------------------------------------
1 | package com.jmedeisis.windowview.sensor;
2 |
3 | /**
4 | * Performs exponential smoothing with an exponentially-weighted moving average.
5 | * Analogous to an infinite-impulse-response, single-pole low-pass filter.
6 | */
7 | public class ExponentialSmoothingFilter implements Filter {
8 |
9 | private float lastValue;
10 | /**
11 | * 0-1. See {@link #setSmoothingFactor(float)}.
12 | */
13 | private float factor;
14 |
15 | public ExponentialSmoothingFilter(float smoothingFactor, float initialValue) {
16 | this.factor = smoothingFactor;
17 | reset(initialValue);
18 | }
19 |
20 | /**
21 | * @param factor 0-1. Calculated as dt / (t + dt), where t is the system's time constant and dt
22 | * is the sampling period, i.e. the rate that new values are delivered via
23 | * {@link #push(float)}.
24 | * The closer to 0, the greater the inertia, i.e. the filter responds more slowly
25 | * to new input values.
26 | */
27 | public void setSmoothingFactor(float factor) {
28 | this.factor = factor;
29 | }
30 |
31 | @Override
32 | public void reset(float value) {
33 | lastValue = value;
34 | }
35 |
36 | /**
37 | * Pushes new sample to filter.
38 | *
39 | * @return new smoothed value.
40 | */
41 | @Override
42 | public float push(float value) {
43 | // do low-pass
44 | lastValue = lastValue + factor * (value - lastValue);
45 | return get();
46 | }
47 |
48 | /**
49 | * @return smoothed value.
50 | */
51 | @Override
52 | public float get() {
53 | return lastValue;
54 | }
55 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/jmedeisis/windowview/sensor/Filter.java:
--------------------------------------------------------------------------------
1 | package com.jmedeisis.windowview.sensor;
2 |
3 | /**
4 | * A discrete-time filter for raw sensor values.
5 | */
6 | public interface Filter {
7 | /**
8 | * Update filter with the latest value.
9 | *
10 | * @return latest filtered value.
11 | */
12 | float push(float value);
13 |
14 | /**
15 | * Reset filter to the given value.
16 | */
17 | void reset(float value);
18 |
19 | /**
20 | * @return latest filtered value.
21 | */
22 | float get();
23 | }
24 |
--------------------------------------------------------------------------------
/library/src/main/java/com/jmedeisis/windowview/sensor/TiltSensor.java:
--------------------------------------------------------------------------------
1 | package com.jmedeisis.windowview.sensor;
2 |
3 | import android.content.Context;
4 | import android.hardware.Sensor;
5 | import android.hardware.SensorEvent;
6 | import android.hardware.SensorEventListener;
7 | import android.hardware.SensorManager;
8 | import android.view.Display;
9 | import android.view.Surface;
10 | import android.view.WindowManager;
11 |
12 | import java.util.ArrayList;
13 | import java.util.List;
14 |
15 | /**
16 | * Interprets sensor data to calculate device tilt in terms of yaw, pitch and roll.
17 | * Requires one of the following sensor combinations to be accessible via {@link SensorManager}:
18 | *
19 | * - TYPE_ROTATION_VECTOR
20 | * - TYPE_MAGNETIC_FIELD + TYPE_GRAVITY
21 | * - TYPE_MAGNETIC_FIELD + TYPE_ACCELEROMETER
22 | *
23 | */
24 | public class TiltSensor implements SensorEventListener {
25 | // 1 radian = 180 / PI = 57.2957795 degrees
26 | private static final float DEGREES_PER_RADIAN = 57.2957795f;
27 |
28 | private final SensorManager sensorManager;
29 |
30 | private boolean tracking;
31 |
32 | /**
33 | * @see {@link Display#getRotation()}.
34 | */
35 | private final int screenRotation;
36 |
37 | private boolean relativeTilt;
38 |
39 | /**
40 | * Interface for callback to be invoked when new orientation values are available.
41 | */
42 | public interface TiltListener {
43 | /**
44 | * Euler angles defined as per {@link SensorManager#getOrientation(float[], float[])}.
45 | *
46 | * All three are in radians and positive in the counter-clockwise
47 | * direction.
48 | *
49 | * @param yaw rotation around -Z axis. -PI to PI.
50 | * @param pitch rotation around -X axis. -PI/2 to PI/2.
51 | * @param roll rotation around Y axis. -PI to PI.
52 | */
53 | void onTiltUpdate(float yaw, float pitch, float roll);
54 | }
55 |
56 | private List listeners;
57 |
58 | private final float[] rotationMatrix = new float[9];
59 | private final float[] rotationMatrixTemp = new float[9];
60 | private final float[] rotationMatrixOrigin = new float[9];
61 | /**
62 | * [w, x, y, z]
63 | */
64 | private final float[] latestQuaternion = new float[4];
65 | /**
66 | * [w, x, y, z]
67 | */
68 | private final float[] invQuaternionOrigin = new float[4];
69 | /**
70 | * [w, x, y, z]
71 | */
72 | private final float[] rotationQuaternion = new float[4];
73 | private final float[] latestAccelerations = new float[3];
74 | private final float[] latestMagFields = new float[3];
75 | private final float[] orientation = new float[3];
76 | private boolean haveGravData = false;
77 | private boolean haveAccelData = false;
78 | private boolean haveMagData = false;
79 | private boolean haveRotOrigin = false;
80 | private boolean haveQuatOrigin = false;
81 | private boolean haveRotVecData = false;
82 |
83 | private Filter yawFilter;
84 | private Filter pitchFilter;
85 | private Filter rollFilter;
86 |
87 | /**
88 | * See {@link ExponentialSmoothingFilter#setSmoothingFactor(float)}.
89 | */
90 | private static final float SMOOTHING_FACTOR_HIGH_ACC = 0.8f;
91 | private static final float SMOOTHING_FACTOR_LOW_ACC = 0.05f;
92 |
93 | public TiltSensor(Context context, boolean trackRelativeOrientation) {
94 | listeners = new ArrayList<>();
95 |
96 | initialiseDefaultFilters(SMOOTHING_FACTOR_LOW_ACC);
97 |
98 | sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
99 | tracking = false;
100 |
101 | screenRotation = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
102 | .getDefaultDisplay().getRotation();
103 |
104 | this.relativeTilt = trackRelativeOrientation;
105 | }
106 |
107 | /**
108 | * Registers for motion sensor events.
109 | * Do this to begin receiving {@link TiltListener#onTiltUpdate(float, float, float)} callbacks.
110 | *
111 | * You must call {@link #stopTracking()} to unregister when tilt updates are no longer
112 | * needed.
113 | *
114 | * @param samplingPeriodUs see {@link SensorManager#registerListener(SensorEventListener, Sensor, int)}
115 | */
116 | public void startTracking(int samplingPeriodUs) {
117 | sensorManager.registerListener(this,
118 | sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR), samplingPeriodUs);
119 | sensorManager.registerListener(this,
120 | sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD), samplingPeriodUs);
121 | sensorManager.registerListener(this,
122 | sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY), samplingPeriodUs);
123 | sensorManager.registerListener(this,
124 | sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), samplingPeriodUs);
125 | tracking = true;
126 | }
127 |
128 | public boolean isTracking() {
129 | return tracking;
130 | }
131 |
132 | /**
133 | * Unregisters from motion sensor events.
134 | */
135 | public void stopTracking() {
136 | sensorManager.unregisterListener(this);
137 | if (null != yawFilter) yawFilter.reset(0);
138 | if (null != pitchFilter) pitchFilter.reset(0);
139 | if (null != rollFilter) rollFilter.reset(0);
140 | tracking = false;
141 | }
142 |
143 | public void addListener(TiltListener listener) {
144 | listeners.add(listener);
145 | }
146 |
147 | public void removeListener(TiltListener listener) {
148 | listeners.remove(listener);
149 | }
150 |
151 | public void setTrackRelativeOrientation(boolean trackRelative) {
152 | this.relativeTilt = trackRelative;
153 | }
154 |
155 | /**
156 | * @see Display#getRotation()
157 | */
158 | public int getScreenRotation() {
159 | return screenRotation;
160 | }
161 |
162 | /**
163 | * @param factor see {@link ExponentialSmoothingFilter#setSmoothingFactor(float)}
164 | */
165 | private void initialiseDefaultFilters(float factor) {
166 | yawFilter = new ExponentialSmoothingFilter(factor, null == yawFilter ? 0 : yawFilter.get());
167 | pitchFilter = new ExponentialSmoothingFilter(factor, null == pitchFilter ? 0 : pitchFilter.get());
168 | rollFilter = new ExponentialSmoothingFilter(factor, null == rollFilter ? 0 : rollFilter.get());
169 | }
170 |
171 | @Override
172 | public void onSensorChanged(SensorEvent event) {
173 | switch (event.sensor.getType()) {
174 | case Sensor.TYPE_ROTATION_VECTOR:
175 | SensorManager.getQuaternionFromVector(latestQuaternion, event.values);
176 | if (!haveRotVecData) {
177 | initialiseDefaultFilters(SMOOTHING_FACTOR_HIGH_ACC);
178 | }
179 | haveRotVecData = true;
180 | break;
181 | case Sensor.TYPE_GRAVITY:
182 | if (haveRotVecData) {
183 | // rotation vector sensor data is better
184 | sensorManager.unregisterListener(this,
185 | sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY));
186 | break;
187 | }
188 | System.arraycopy(event.values, 0, latestAccelerations, 0, 3);
189 | haveGravData = true;
190 | break;
191 | case Sensor.TYPE_ACCELEROMETER:
192 | if (haveGravData || haveRotVecData) {
193 | // rotation vector / gravity sensor data is better!
194 | // let's not listen to the accelerometer anymore
195 | sensorManager.unregisterListener(this,
196 | sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER));
197 | break;
198 | }
199 | System.arraycopy(event.values, 0, latestAccelerations, 0, 3);
200 | haveAccelData = true;
201 | break;
202 | case Sensor.TYPE_MAGNETIC_FIELD:
203 | if (haveRotVecData) {
204 | // rotation vector sensor data is better
205 | sensorManager.unregisterListener(this,
206 | sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD));
207 | break;
208 | }
209 | System.arraycopy(event.values, 0, latestMagFields, 0, 3);
210 | haveMagData = true;
211 | break;
212 | }
213 |
214 | if (haveDataNecessaryToComputeOrientation()) {
215 | computeOrientation();
216 | }
217 | }
218 |
219 | /**
220 | * After {@link #startTracking(int)} has been called and sensor data has been received,
221 | * this method returns the sensor type chosen for orientation calculations.
222 | *
223 | * @return one of {@link Sensor#TYPE_ROTATION_VECTOR}, {@link Sensor#TYPE_GRAVITY},
224 | * {@link Sensor#TYPE_ACCELEROMETER} or 0 if none of the previous are available or
225 | * {@link #startTracking(int)} has not yet been called.
226 | */
227 | public int getChosenSensorType() {
228 | if (haveRotVecData) return Sensor.TYPE_ROTATION_VECTOR;
229 | if (haveGravData) return Sensor.TYPE_GRAVITY;
230 | if (haveAccelData) return Sensor.TYPE_ACCELEROMETER;
231 | return 0;
232 | }
233 |
234 | /**
235 | * @return true if both {@link #latestAccelerations} and {@link #latestMagFields} have valid values.
236 | */
237 | private boolean haveDataNecessaryToComputeOrientation() {
238 | return haveRotVecData || ((haveGravData || haveAccelData) && haveMagData);
239 | }
240 |
241 | /**
242 | * Computes the latest rotation, remaps it according to the current {@link #screenRotation},
243 | * and stores it in {@link #rotationMatrix}.
244 | *
245 | * Should only be called if {@link #haveDataNecessaryToComputeOrientation()} returns true and
246 | * {@link #haveRotVecData} is false, else result may be undefined.
247 | *
248 | * @return true if rotation was retrieved and recalculated, false otherwise.
249 | */
250 | private boolean computeRotationMatrix() {
251 | if (SensorManager.getRotationMatrix(rotationMatrixTemp, null, latestAccelerations, latestMagFields)) {
252 | switch (screenRotation) {
253 | case Surface.ROTATION_0:
254 | SensorManager.remapCoordinateSystem(rotationMatrixTemp,
255 | SensorManager.AXIS_X, SensorManager.AXIS_Y, rotationMatrix);
256 | break;
257 | case Surface.ROTATION_90:
258 | //noinspection SuspiciousNameCombination
259 | SensorManager.remapCoordinateSystem(rotationMatrixTemp,
260 | SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X, rotationMatrix);
261 | break;
262 | case Surface.ROTATION_180:
263 | SensorManager.remapCoordinateSystem(rotationMatrixTemp,
264 | SensorManager.AXIS_MINUS_X, SensorManager.AXIS_MINUS_Y, rotationMatrix);
265 | break;
266 | case Surface.ROTATION_270:
267 | //noinspection SuspiciousNameCombination
268 | SensorManager.remapCoordinateSystem(rotationMatrixTemp,
269 | SensorManager.AXIS_MINUS_Y, SensorManager.AXIS_X, rotationMatrix);
270 | break;
271 | }
272 | return true;
273 | }
274 | return false;
275 | }
276 |
277 | /**
278 | * Computes the latest orientation and notifies any {@link TiltListener}s.
279 | */
280 | private void computeOrientation() {
281 | boolean updated = false;
282 | float yaw = 0;
283 | float pitch = 0;
284 | float roll = 0;
285 |
286 | if (haveRotVecData) {
287 | remapQuaternionToScreenRotation(latestQuaternion, screenRotation);
288 | if (relativeTilt) {
289 | if (!haveQuatOrigin) {
290 | System.arraycopy(latestQuaternion, 0, invQuaternionOrigin, 0, 4);
291 | invertQuaternion(invQuaternionOrigin);
292 | haveQuatOrigin = true;
293 | }
294 | multQuaternions(rotationQuaternion, invQuaternionOrigin, latestQuaternion);
295 | } else {
296 | System.arraycopy(latestQuaternion, 0, rotationQuaternion, 0, 4);
297 | }
298 |
299 | // https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles
300 | final float q0 = rotationQuaternion[0]; // w
301 | final float q1 = rotationQuaternion[1]; // x
302 | final float q2 = rotationQuaternion[2]; // y
303 | final float q3 = rotationQuaternion[3]; // z
304 |
305 | float rotXRad = (float) Math.atan2(2 * (q0 * q1 + q2 * q3), 1 - 2 * (q1 * q1 + q2 * q2));
306 | float rotYRad = (float) Math.asin(2 * (q0 * q2 - q3 * q1));
307 | float rotZRad = (float) Math.atan2(2 * (q0 * q3 + q1 * q2), 1 - 2 * (q2 * q2 + q3 * q3));
308 |
309 | // constructed to match output of SensorManager#getOrientation
310 | yaw = -rotZRad * DEGREES_PER_RADIAN;
311 | pitch = -rotXRad * DEGREES_PER_RADIAN;
312 | roll = rotYRad * DEGREES_PER_RADIAN;
313 | updated = true;
314 | } else if (computeRotationMatrix()) {
315 | if (relativeTilt) {
316 | if (!haveRotOrigin) {
317 | System.arraycopy(rotationMatrix, 0, rotationMatrixOrigin, 0, 9);
318 | haveRotOrigin = true;
319 | }
320 | // get yaw / pitch / roll relative to original rotation
321 | SensorManager.getAngleChange(orientation, rotationMatrix, rotationMatrixOrigin);
322 | } else {
323 | // get absolute yaw / pitch / roll
324 | SensorManager.getOrientation(rotationMatrix, orientation);
325 | }
326 | /*
327 | * [0] : yaw, rotation around -z axis
328 | * [1] : pitch, rotation around -x axis
329 | * [2] : roll, rotation around y axis
330 | */
331 | yaw = orientation[0] * DEGREES_PER_RADIAN;
332 | pitch = orientation[1] * DEGREES_PER_RADIAN;
333 | roll = orientation[2] * DEGREES_PER_RADIAN;
334 | updated = true;
335 | }
336 |
337 | if (!updated) return;
338 |
339 |
340 | if (null != yawFilter) yaw = yawFilter.push(yaw);
341 | if (null != pitchFilter) pitch = pitchFilter.push(pitch);
342 | if (null != rollFilter) roll = rollFilter.push(roll);
343 |
344 | for (int i = 0; i < listeners.size(); i++) {
345 | listeners.get(i).onTiltUpdate(yaw, pitch, roll);
346 | }
347 | }
348 |
349 | /**
350 | * @param immediate if true, any sensor data filters are reset to new origin immediately.
351 | * If false, values transition smoothly to new origin.
352 | */
353 | public void resetOrigin(boolean immediate) {
354 | haveRotOrigin = false;
355 | haveQuatOrigin = false;
356 | if (immediate) {
357 | if (null != yawFilter) yawFilter.reset(0);
358 | if (null != pitchFilter) pitchFilter.reset(0);
359 | if (null != rollFilter) rollFilter.reset(0);
360 | }
361 | }
362 |
363 | @Override
364 | public void onAccuracyChanged(Sensor sensor, int accuracy) {
365 |
366 | }
367 |
368 | /**
369 | * Please drop me a PM if you know of a more elegant way to accomplish this - Justas
370 | *
371 | * @param q [w, x, y, z]
372 | * @param screenRotation see {@link Display#getRotation()}
373 | */
374 | private static void remapQuaternionToScreenRotation(float[] q, int screenRotation) {
375 | final float x = q[1];
376 | final float y = q[2];
377 | switch (screenRotation) {
378 | case Surface.ROTATION_0:
379 | break;
380 | case Surface.ROTATION_90:
381 | q[1] = -y;
382 | q[2] = x;
383 | break;
384 | case Surface.ROTATION_180:
385 | q[1] = -x;
386 | q[2] = -y;
387 | break;
388 | case Surface.ROTATION_270:
389 | q[1] = y;
390 | q[2] = -x;
391 | break;
392 | }
393 | }
394 |
395 | /**
396 | * @param qOut [w, x, y, z] result.
397 | * @param q1 [w, x, y, z] left.
398 | * @param q2 [w, x, y, z] right.
399 | */
400 | private static void multQuaternions(float[] qOut, float[] q1, float[] q2) {
401 | // multiply quaternions
402 | final float a = q1[0];
403 | final float b = q1[1];
404 | final float c = q1[2];
405 | final float d = q1[3];
406 |
407 | final float e = q2[0];
408 | final float f = q2[1];
409 | final float g = q2[2];
410 | final float h = q2[3];
411 |
412 | qOut[0] = a * e - b * f - c * g - d * h;
413 | qOut[1] = b * e + a * f + c * h - d * g;
414 | qOut[2] = a * g - b * h + c * e + d * f;
415 | qOut[3] = a * h + b * g - c * f + d * e;
416 | }
417 |
418 | /**
419 | * @param q [w, x, y, z]
420 | */
421 | private static void invertQuaternion(float[] q) {
422 | for (int i = 1; i < 4; i++) {
423 | q[i] = -q[i]; // invert quaternion
424 | }
425 | }
426 | }
--------------------------------------------------------------------------------
/library/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/sample-debug/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/sample-debug/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 24
5 | buildToolsVersion "24.0.0"
6 |
7 | defaultConfig {
8 | applicationId "com.example.windowviewdebug"
9 | minSdkVersion 11
10 | targetSdkVersion 24
11 | versionCode 1
12 | versionName "1.0"
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 project(":library")
24 | compile fileTree(dir: 'libs', include: ['*.jar'])
25 | compile 'com.android.support:appcompat-v7:24.0.0'
26 | }
27 |
--------------------------------------------------------------------------------
/sample-debug/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 C:\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 |
--------------------------------------------------------------------------------
/sample-debug/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/sample-debug/src/main/java/com/example/windowviewdebug/DebugActivity.java:
--------------------------------------------------------------------------------
1 | package com.example.windowviewdebug;
2 |
3 | import android.annotation.TargetApi;
4 | import android.content.pm.ActivityInfo;
5 | import android.hardware.SensorManager;
6 | import android.os.Build;
7 | import android.os.Bundle;
8 | import android.support.v4.view.GravityCompat;
9 | import android.support.v7.app.AppCompatActivity;
10 | import android.view.Gravity;
11 | import android.view.Menu;
12 | import android.view.MenuItem;
13 | import android.view.View;
14 | import android.widget.Toast;
15 |
16 | import com.jmedeisis.windowview.sensor.TiltSensor;
17 |
18 | public class DebugActivity extends AppCompatActivity {
19 |
20 | private static final String ORIENTATION = "orientation";
21 | private static final String DEBUG_TILT = "debugTilt";
22 | private static final String DEBUG_IMAGE = "debugImage";
23 | private boolean debugTilt;
24 | private boolean debugImage;
25 | private TiltSensor tiltSensor;
26 | private DebugWindowView windowView1;
27 | private DebugWindowView windowView2;
28 |
29 | @Override
30 | protected void onCreate(Bundle savedInstanceState) {
31 | super.onCreate(savedInstanceState);
32 | setContentView(R.layout.activity_debug);
33 |
34 | tiltSensor = new TiltSensor(this, true);
35 |
36 | windowView1 = (DebugWindowView) findViewById(R.id.windowView1);
37 | windowView2 = (DebugWindowView) findViewById(R.id.windowView2);
38 |
39 | // use one TiltSensor to drive both WindowViews
40 | windowView1.attachTiltTracking(tiltSensor);
41 | windowView2.attachTiltTracking(tiltSensor);
42 |
43 | View.OnClickListener onClickListener = new View.OnClickListener() {
44 | @Override
45 | public void onClick(View v) {
46 | resetTiltSensorOrientationOrigin();
47 | }
48 | };
49 | windowView1.setOnClickListener(onClickListener);
50 | windowView2.setOnClickListener(onClickListener);
51 |
52 | if (null != savedInstanceState && savedInstanceState.containsKey(ORIENTATION)
53 | && savedInstanceState.containsKey(DEBUG_TILT)
54 | && savedInstanceState.containsKey(DEBUG_IMAGE)) {
55 | //noinspection ResourceType
56 | setRequestedOrientation(savedInstanceState.getInt(ORIENTATION));
57 | debugTilt = savedInstanceState.getBoolean(DEBUG_TILT);
58 | debugImage = savedInstanceState.getBoolean(DEBUG_IMAGE);
59 |
60 | windowView1.setDebugEnabled(debugTilt, debugImage);
61 | windowView2.setDebugEnabled(debugTilt, debugImage);
62 | } else {
63 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); // default
64 | debugTilt = false;
65 | debugImage = false;
66 | }
67 | }
68 |
69 | @Override
70 | public void onResume() {
71 | super.onResume();
72 | tiltSensor.startTracking(SensorManager.SENSOR_DELAY_FASTEST);
73 | }
74 |
75 | @Override
76 | public void onPause() {
77 | super.onPause();
78 | tiltSensor.stopTracking();
79 | }
80 |
81 | @Override
82 | public void onSaveInstanceState(Bundle outState) {
83 | super.onSaveInstanceState(outState);
84 | outState.putInt(ORIENTATION, getRequestedOrientation());
85 | outState.putBoolean(DEBUG_TILT, debugTilt);
86 | outState.putBoolean(DEBUG_IMAGE, debugImage);
87 | }
88 |
89 | @TargetApi(Build.VERSION_CODES.HONEYCOMB)
90 | @Override
91 | public boolean onCreateOptionsMenu(Menu menu) {
92 | getMenuInflater().inflate(R.menu.menu_debug, menu);
93 |
94 | menu.findItem(R.id.action_lock_portrait)
95 | .setChecked(getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
96 | menu.findItem(R.id.action_debug_tilt).setChecked(debugTilt);
97 | menu.findItem(R.id.action_debug_image).setChecked(debugImage);
98 |
99 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
100 | // display 3D-ish icon representation of absolute device orientation
101 | final View actionView = View.inflate(this, R.layout.device_compass, null);
102 | final View xy = actionView.findViewById(R.id.compass_xy);
103 | final View z = actionView.findViewById(R.id.compass_z);
104 | tiltSensor.addListener(new TiltSensor.TiltListener() {
105 | @Override
106 | public void onTiltUpdate(float yaw, float pitch, float roll) {
107 | xy.setRotation(yaw);
108 | xy.setRotationX(pitch);
109 | xy.setRotationY(roll);
110 |
111 | z.setRotation(yaw);
112 | z.setRotationX(pitch);
113 | z.setRotationY(roll - 90);
114 | }
115 | });
116 | actionView.setOnClickListener(new View.OnClickListener() {
117 | @Override
118 | public void onClick(View v) {
119 | resetTiltSensorOrientationOrigin();
120 | }
121 | });
122 | actionView.setOnLongClickListener(new View.OnLongClickListener() {
123 | @Override
124 | public boolean onLongClick(View v) {
125 | Toast t = Toast.makeText(DebugActivity.this,
126 | R.string.action_reset_orientation, Toast.LENGTH_SHORT);
127 | int[] pos = new int[2];
128 | actionView.getLocationInWindow(pos);
129 | t.setGravity(Gravity.TOP | GravityCompat.START,
130 | pos[0], pos[1] + actionView.getHeight() / 2);
131 | t.show();
132 | return true;
133 | }
134 | });
135 |
136 |
137 | menu.findItem(R.id.action_reset_orientation).setActionView(actionView);
138 | }
139 |
140 | return true;
141 | }
142 |
143 | private void resetTiltSensorOrientationOrigin() {
144 | tiltSensor.resetOrigin(false);
145 | Toast.makeText(DebugActivity.this, R.string.hint_orientation_reset, Toast.LENGTH_SHORT).show();
146 | }
147 |
148 | @Override
149 | public boolean onOptionsItemSelected(MenuItem item) {
150 | switch (item.getItemId()) {
151 | case R.id.action_lock_portrait:
152 | item.setChecked(!item.isChecked());
153 | if (item.isChecked()) {
154 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
155 | } else {
156 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
157 | }
158 | return true;
159 | case R.id.action_debug_tilt:
160 | item.setChecked(!item.isChecked());
161 | debugTilt = item.isChecked();
162 | windowView1.setDebugEnabled(debugTilt, debugImage);
163 | windowView2.setDebugEnabled(debugTilt, debugImage);
164 | return true;
165 | case R.id.action_debug_image:
166 | item.setChecked(!item.isChecked());
167 | debugImage = item.isChecked();
168 | windowView1.setDebugEnabled(debugTilt, debugImage);
169 | windowView2.setDebugEnabled(debugTilt, debugImage);
170 | return true;
171 | }
172 | return super.onOptionsItemSelected(item);
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/sample-debug/src/main/java/com/example/windowviewdebug/DebugWindowView.java:
--------------------------------------------------------------------------------
1 | package com.example.windowviewdebug;
2 |
3 | import android.annotation.TargetApi;
4 | import android.content.Context;
5 | import android.graphics.Canvas;
6 | import android.graphics.Color;
7 | import android.graphics.Paint;
8 | import android.graphics.Typeface;
9 | import android.hardware.Sensor;
10 | import android.hardware.SensorManager;
11 | import android.os.Build;
12 | import android.support.annotation.NonNull;
13 | import android.util.AttributeSet;
14 | import android.util.Log;
15 | import android.view.Surface;
16 |
17 | import com.jmedeisis.windowview.WindowView;
18 |
19 | /**
20 | * WindowView that exposes many internal properties through overlay debug text.
21 | */
22 | public class DebugWindowView extends WindowView {
23 | private static final String LOG_TAG = DebugWindowView.class.getSimpleName();
24 |
25 | private boolean debugTilt = false;
26 | private boolean debugImage = false;
27 | private static final boolean DEBUG_LIFECYCLE = false;
28 | private final static int DEBUG_TEXT_SIZE = 32;
29 | private Paint debugTextPaint;
30 |
31 | private float latestYaw;
32 | private float latestPitch;
33 | private float latestRoll;
34 |
35 | public DebugWindowView(Context context) {
36 | super(context);
37 | }
38 |
39 | public DebugWindowView(Context context, AttributeSet attrs) {
40 | super(context, attrs);
41 | }
42 |
43 | public DebugWindowView(Context context, AttributeSet attrs, int defStyleAttr) {
44 | super(context, attrs, defStyleAttr);
45 | }
46 |
47 | @SuppressWarnings("unused")
48 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
49 | public DebugWindowView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
50 | super(context, attrs, defStyleAttr, defStyleRes);
51 | }
52 |
53 | @Override
54 | protected void init(Context context, AttributeSet attrs) {
55 | super.init(context, attrs);
56 |
57 | debugTextPaint = new Paint();
58 | debugTextPaint.setColor(Color.MAGENTA);
59 | debugTextPaint.setTextSize(DEBUG_TEXT_SIZE);
60 | debugTextPaint.setTypeface(Typeface.MONOSPACE);
61 | }
62 |
63 | @Override
64 | public void onTiltUpdate(float yaw, float pitch, float roll) {
65 | super.onTiltUpdate(yaw, pitch, roll);
66 | this.latestYaw = yaw;
67 | this.latestPitch = pitch;
68 | this.latestRoll = roll;
69 | }
70 |
71 | /**
72 | * Enables/disables on-screen debug information.
73 | *
74 | * @param debugTilt if true, displays on-screen information about the current tilt values and limits.
75 | * @param debugImage if true, displays on-screen information about the source image and dimensions.
76 | */
77 | public void setDebugEnabled(boolean debugTilt, boolean debugImage) {
78 | this.debugTilt = debugTilt;
79 | this.debugImage = debugImage;
80 | invalidate();
81 | }
82 |
83 | @Override
84 | public void onWindowFocusChanged(boolean hasWindowFocus) {
85 | super.onWindowFocusChanged(hasWindowFocus);
86 | if (DEBUG_LIFECYCLE)
87 | Log.d(LOG_TAG, "onWindowFocusChanged(), hasWindowFocus: " + hasWindowFocus);
88 | }
89 |
90 | @Override
91 | protected void onAttachedToWindow() {
92 | super.onAttachedToWindow();
93 | if (DEBUG_LIFECYCLE) Log.d(LOG_TAG, "onAttachedToWindow()");
94 | }
95 |
96 | @Override
97 | protected void onDetachedFromWindow() {
98 | super.onDetachedFromWindow();
99 | if (DEBUG_LIFECYCLE) Log.d(LOG_TAG, "onDetachedFromWindow()");
100 | }
101 |
102 | @SuppressWarnings("UnusedAssignment")
103 | @Override
104 | protected void onDraw(@NonNull Canvas canvas) {
105 | super.onDraw(canvas);
106 |
107 | int i = 0;
108 | if (debugImage) {
109 | debugText(canvas, i++, "width " + getWidth());
110 | debugText(canvas, i++, "height " + getHeight());
111 | debugText(canvas, i++, "img width " + (getWidth() + widthDifference));
112 | debugText(canvas, i++, "img height " + (getHeight() + heightDifference));
113 |
114 | debugText(canvas, i++, getTranslateMode() + " translateMode");
115 |
116 | float translateX = 0;
117 | float translateY = 0;
118 | if (heightMatches) {
119 | translateX = (-getHorizontalOrigin() +
120 | clampAbsoluteFloating(getHorizontalOrigin(), latestRoll, getMaxRoll())) / getMaxRoll();
121 | } else {
122 | translateY = (getVerticalOrigin() -
123 | clampAbsoluteFloating(getVerticalOrigin(), latestPitch, getMaxPitch())) / getMaxPitch();
124 | }
125 | debugText(canvas, i++, "tx " + translateX);
126 | debugText(canvas, i++, "ty " + translateY);
127 | debugText(canvas, i++, "tx abs " + Math.round((widthDifference / 2) * translateX));
128 | debugText(canvas, i++, "ty abs " + Math.round((heightDifference / 2) * translateY));
129 | debugText(canvas, i++, "height matches " + heightMatches);
130 | }
131 |
132 | if (debugTilt) {
133 | if (null == sensor) {
134 | debugText(canvas, i++, "EXTERNAL TILT SENSOR");
135 | } else {
136 | switch (sensor.getChosenSensorType()) {
137 | case 0:
138 | debugText(canvas, i++, "NO AVAILABLE SENSOR");
139 | break;
140 | case Sensor.TYPE_ROTATION_VECTOR:
141 | debugText(canvas, i++, "ROTATION_VECTOR");
142 | break;
143 | case Sensor.TYPE_GRAVITY:
144 | debugText(canvas, i++, "MAG + GRAVITY");
145 | break;
146 | case Sensor.TYPE_ACCELEROMETER:
147 | debugText(canvas, i++, "MAG + ACCELEROMETER");
148 | break;
149 | }
150 | switch (getSensorSamplingPeriod()) {
151 | case SensorManager.SENSOR_DELAY_FASTEST:
152 | debugText(canvas, i++, "SENSOR_DELAY_FASTEST");
153 | break;
154 | case SensorManager.SENSOR_DELAY_GAME:
155 | debugText(canvas, i++, "SENSOR_DELAY_GAME");
156 | break;
157 | case SensorManager.SENSOR_DELAY_UI:
158 | debugText(canvas, i++, "SENSOR_DELAY_UI");
159 | break;
160 | case SensorManager.SENSOR_DELAY_NORMAL:
161 | debugText(canvas, i++, "SENSOR_DELAY_NORMAL");
162 | break;
163 | default:
164 | debugText(canvas, i++, "Sensor delay " + getSensorSamplingPeriod() + "us");
165 | break;
166 | }
167 | debugText(canvas, i++, getOrientationMode() + " orientationMode");
168 | }
169 | debugText(canvas, i++, getTiltSensorMode() + " tiltSensorMode");
170 |
171 | /*if(haveOrigin){
172 | SensorManager.getOrientation(rotationMatrixOrigin, orientationOrigin);
173 | debugText(canvas, i++, "org yaw " + orientationOrigin[0]*DEGREES_PER_RADIAN);
174 | debugText(canvas, i++, "org pitch " + orientationOrigin[1]*DEGREES_PER_RADIAN);
175 | debugText(canvas, i++, "org roll " + orientationOrigin[2]*DEGREES_PER_RADIAN);
176 | }*/
177 |
178 | debugText(canvas, i++, "yaw " + latestYaw);
179 | debugText(canvas, i++, "pitch " + latestPitch);
180 | debugText(canvas, i++, "roll " + latestRoll);
181 |
182 | debugText(canvas, i++, "MAX_PITCH " + getMaxPitch());
183 | debugText(canvas, i++, "MAX_ROLL " + getMaxRoll());
184 |
185 | debugText(canvas, i++, "HOR ORIGIN " + getHorizontalOrigin());
186 | debugText(canvas, i++, "VER ORIGIN " + getVerticalOrigin());
187 |
188 | if (null != sensor) {
189 | switch (sensor.getScreenRotation()) {
190 | case Surface.ROTATION_0:
191 | debugText(canvas, i++, "ROTATION_0");
192 | break;
193 | case Surface.ROTATION_90:
194 | debugText(canvas, i++, "ROTATION_90");
195 | break;
196 | case Surface.ROTATION_180:
197 | debugText(canvas, i++, "ROTATION_180");
198 | break;
199 | case Surface.ROTATION_270:
200 | debugText(canvas, i++, "ROTATION_270");
201 | break;
202 | }
203 | }
204 | }
205 | }
206 |
207 | private void debugText(Canvas canvas, int i, String text) {
208 | canvas.drawText(text, DEBUG_TEXT_SIZE, (2 + i) * DEBUG_TEXT_SIZE, debugTextPaint);
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/sample-debug/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample-debug/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample-debug/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample-debug/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample-debug/src/main/res/drawable-nodpi/device_frame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample-debug/src/main/res/drawable-nodpi/device_frame.png
--------------------------------------------------------------------------------
/sample-debug/src/main/res/drawable-nodpi/device_z.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample-debug/src/main/res/drawable-nodpi/device_z.png
--------------------------------------------------------------------------------
/sample-debug/src/main/res/drawable-nodpi/london_wide.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample-debug/src/main/res/drawable-nodpi/london_wide.jpg
--------------------------------------------------------------------------------
/sample-debug/src/main/res/drawable-nodpi/singapore_tall.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample-debug/src/main/res/drawable-nodpi/singapore_tall.jpg
--------------------------------------------------------------------------------
/sample-debug/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample-debug/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample-debug/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample-debug/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample-debug/src/main/res/layout-land/activity_debug.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
17 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/sample-debug/src/main/res/layout/activity_debug.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
16 |
17 |
23 |
24 |
--------------------------------------------------------------------------------
/sample-debug/src/main/res/layout/device_compass.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
22 |
23 |
--------------------------------------------------------------------------------
/sample-debug/src/main/res/menu/menu_debug.xml:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/sample-debug/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 |
4 |
--------------------------------------------------------------------------------
/sample-debug/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | WV Debug
3 |
4 | Window image
5 | Lock Screen Portrait
6 | Debug Tilt
7 | Debug Image
8 | Reset Orientation
9 | Start orientation reset.
10 |
11 |
--------------------------------------------------------------------------------
/sample-debug/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
13 |
14 |
17 |
18 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 24
5 | buildToolsVersion '24.0.0'
6 |
7 | defaultConfig {
8 | applicationId "com.example.windowview"
9 | minSdkVersion 9
10 | targetSdkVersion 24
11 | versionCode 1
12 | versionName "1.0"
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 project(":library")
24 | compile fileTree(dir: 'libs', include: ['*.jar'])
25 | compile 'com.android.support:appcompat-v7:24.0.0'
26 | }
27 |
--------------------------------------------------------------------------------
/sample/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 C:/Users/Justas/Desktop/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 |
--------------------------------------------------------------------------------
/sample/sample_in_action.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample/sample_in_action.gif
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/example/windowview/DemoActivity.java:
--------------------------------------------------------------------------------
1 | package com.example.windowview;
2 |
3 | import android.os.Bundle;
4 | import android.support.v7.app.AppCompatActivity;
5 | import android.view.View;
6 |
7 | import com.jmedeisis.windowview.WindowView;
8 |
9 | public class DemoActivity extends AppCompatActivity {
10 |
11 | @Override
12 | protected void onCreate(Bundle savedInstanceState) {
13 | super.onCreate(savedInstanceState);
14 | setContentView(R.layout.activity_demo);
15 |
16 | // re-center of WindowView tilt sensors on tap
17 | final WindowView windowView1 = (WindowView) findViewById(R.id.windowView1);
18 | windowView1.setOnClickListener(new View.OnClickListener() {
19 | @Override
20 | public void onClick(View v) {
21 | windowView1.resetOrientationOrigin(false);
22 | }
23 | });
24 |
25 | final WindowView windowView2 = (WindowView) findViewById(R.id.windowView2);
26 | windowView2.setOnClickListener(new View.OnClickListener() {
27 | @Override
28 | public void onClick(View v) {
29 | windowView2.resetOrientationOrigin(false);
30 | }
31 | });
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-nodpi/london_wide.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample/src/main/res/drawable-nodpi/london_wide.jpg
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-nodpi/singapore_tall.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample/src/main/res/drawable-nodpi/singapore_tall.jpg
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justasm/WindowView/733d5df6d3c4fb0e87d62b8d314e05e1026bb40f/sample/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/layout-land/activity_demo.xml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
19 |
20 |
28 |
29 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_demo.xml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
18 |
19 |
27 |
28 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 |
4 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WindowView Demo
5 | Window image
6 |
7 |
8 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':sample', ':library', ':sample-debug'
2 |
--------------------------------------------------------------------------------