├── .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 | ![Tilting to pan images.](/sample/sample_in_action.gif) 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 |

5 | 6 | 10 | 11 | 16 | 17 | 22 | 23 | 28 | 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 | --------------------------------------------------------------------------------