Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

First commit.

  • Loading branch information...
commit 69c4d05834bc1ce1a148c1b2394268ec7338fddc 0 parents
nadavfima authored
Showing with 2,648 additions and 0 deletions.
  1. +14 −0 README.md
  2. BIN  example.png
  3. +8 −0 example/.classpath
  4. +33 −0 example/.project
  5. +29 −0 example/AndroidManifest.xml
  6. +20 −0 example/proguard-project.txt
  7. +15 −0 example/project.properties
  8. BIN  example/res/drawable-hdpi/ic_launcher.png
  9. BIN  example/res/drawable-ldpi/ic_launcher.png
  10. BIN  example/res/drawable-mdpi/ic_launcher.png
  11. BIN  example/res/drawable-xhdpi/ic_launcher.png
  12. BIN  example/res/drawable-xhdpi/ic_lockscreen_camera_activated.png
  13. BIN  example/res/drawable-xhdpi/ic_lockscreen_camera_normal.png
  14. BIN  example/res/drawable-xhdpi/ic_lockscreen_google_activated.png
  15. BIN  example/res/drawable-xhdpi/ic_lockscreen_google_focused.png
  16. BIN  example/res/drawable-xhdpi/ic_lockscreen_google_normal.png
  17. +26 −0 example/res/drawable/ic_alarm_alert_outerring.xml
  18. +32 −0 example/res/drawable/ic_alarm_alert_touch_handle.xml
  19. +21 −0 example/res/drawable/ic_item_camera.xml
  20. +21 −0 example/res/drawable/ic_item_google.xml
  21. +31 −0 example/res/layout/activity_main.xml
  22. +9 −0 example/res/menu/activity_main.xml
  23. +11 −0 example/res/values-v11/styles.xml
  24. +12 −0 example/res/values-v14/styles.xml
  25. +23 −0 example/res/values/arrays.xml
  26. +8 −0 example/res/values/strings.xml
  27. +20 −0 example/res/values/styles.xml
  28. +68 −0 example/src/com/example/glowpadexample/MainActivity.java
  29. +8 −0 library/.classpath
  30. +33 −0 library/.project
  31. +14 −0 library/AndroidManifest.xml
  32. +15 −0 library/project.properties
  33. BIN  library/res/drawable-xhdpi/ic_launcher.png
  34. BIN  library/res/drawable-xhdpi/ic_lockscreen_glowdot.png
  35. BIN  library/res/drawable-xhdpi/ic_lockscreen_handle_normal.png
  36. BIN  library/res/drawable-xhdpi/ic_lockscreen_handle_pressed.png
  37. +76 −0 library/res/values/attrs.xml
  38. +21 −0 library/res/values/dimens.xml
  39. +6 −0 library/res/values/strings.xml
  40. +133 −0 library/src/com/fima/glowpadview/Ease.java
  41. +1,259 −0 library/src/com/fima/glowpadview/GlowPadView.java
  42. +236 −0 library/src/com/fima/glowpadview/PointCloud.java
  43. +267 −0 library/src/com/fima/glowpadview/TargetDrawable.java
  44. +179 −0 library/src/com/fima/glowpadview/Tweener.java
14 README.md
@@ -0,0 +1,14 @@
+GlowPadView Library
+====
+
+The GlowPadView library is designed to be used as a mimic to Android's current way of screen-unlocking and alarm dismissing.
+
+The library is based on the "GlowPadView" view found in the DeskClock on Jelly Bean, but extended.
+
+![Example image](https://raw.github.com/nadavfima/GlowPadView/master/example.png)
+
+
+Copyright and Licensing
+----
+
+Copyright Nadav Fima © 2012. All rights reserved.
BIN  example.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 example/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="src" path="gen"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry kind="output" path="bin/classes"/>
+</classpath>
33 example/.project
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>GlowPadExample</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ApkBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
29 example/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.glowpadexample"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-permission android:name="android.permission.VIBRATE"/>
+
+ <uses-sdk
+ android:minSdkVersion="8"
+ android:targetSdkVersion="17" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme" >
+ <activity
+ android:name="com.example.glowpadexample.MainActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
20 example/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
15 example/project.properties
@@ -0,0 +1,15 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-17
+android.library.reference.1=../GlowPadView
BIN  example/res/drawable-hdpi/ic_launcher.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  example/res/drawable-ldpi/ic_launcher.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  example/res/drawable-mdpi/ic_launcher.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  example/res/drawable-xhdpi/ic_launcher.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  example/res/drawable-xhdpi/ic_lockscreen_camera_activated.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  example/res/drawable-xhdpi/ic_lockscreen_camera_normal.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  example/res/drawable-xhdpi/ic_lockscreen_google_activated.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  example/res/drawable-xhdpi/ic_lockscreen_google_focused.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  example/res/drawable-xhdpi/ic_lockscreen_google_normal.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 example/res/drawable/ic_alarm_alert_outerring.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval"
+ >
+ <size android:height="@dimen/glowpadview_outerring_diameter"
+ android:width="@dimen/glowpadview_outerring_diameter"/>
+ <solid android:color="#00000000"/>
+ <stroke android:color="#1affffff" android:width="2dp"/>
+</shape>
32 example/res/drawable/ic_alarm_alert_touch_handle.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_enabled="true"
+ android:state_active="false"
+ android:state_focused="false"
+ android:drawable="@drawable/ic_lockscreen_handle_normal"/>
+
+ <!-- "Pressed" state uses the same simple "ring" image as on the lockscreen -->
+ <item
+ android:state_enabled="true"
+ android:state_active="true"
+ android:state_focused="false"
+ android:drawable="@drawable/ic_lockscreen_handle_pressed"/>
+</selector>
21 example/res/drawable/ic_item_camera.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_enabled="true"
+ android:state_active="false"
+ android:state_focused="false"
+ android:drawable="@drawable/ic_lockscreen_camera_normal"/>
+
+ <item
+ android:state_enabled="true"
+ android:state_active="true"
+ android:state_focused="false"
+ android:drawable="@drawable/ic_lockscreen_camera_activated"/>
+
+ <item
+ android:state_enabled="true"
+ android:state_active="false"
+ android:state_focused="true"
+ android:drawable="@drawable/ic_lockscreen_camera_activated"/>
+</selector>
21 example/res/drawable/ic_item_google.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_enabled="true"
+ android:state_active="false"
+ android:state_focused="false"
+ android:drawable="@drawable/ic_lockscreen_google_normal"/>
+
+ <item
+ android:state_enabled="true"
+ android:state_active="true"
+ android:state_focused="false"
+ android:drawable="@drawable/ic_lockscreen_google_activated"/>
+
+ <item
+ android:state_enabled="true"
+ android:state_active="false"
+ android:state_focused="true"
+ android:drawable="@drawable/ic_lockscreen_google_focused"/>
+</selector>
31 example/res/layout/activity_main.xml
@@ -0,0 +1,31 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:glowpad="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".MainActivity" >
+
+ <com.fima.glowpadview.GlowPadView
+ android:id="@+id/glow_pad_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:layout_gravity="center_vertical"
+ android:layout_marginBottom="@dimen/glowpadview_margin_bottom"
+ android:focusable="true"
+ android:gravity="center"
+ glowpad:directionDescriptions="@array/snooze_dismiss_direction_descriptions"
+ glowpad:feedbackCount="1"
+ glowpad:glowRadius="@dimen/glowpadview_glow_radius"
+ glowpad:handleDrawable="@drawable/ic_alarm_alert_touch_handle"
+ glowpad:innerRadius="@dimen/glowpadview_inner_radius"
+ glowpad:outerRadius="@dimen/glowpadview_target_placement_radius"
+ glowpad:outerRingDrawable="@drawable/ic_alarm_alert_outerring"
+ glowpad:pointDrawable="@drawable/ic_lockscreen_glowdot"
+ glowpad:snapMargin="@dimen/glowpadview_snap_margin"
+ glowpad:targetDescriptions="@array/snooze_dismiss_descriptions"
+ glowpad:targetDrawables="@array/snooze_dismiss_drawables"
+ glowpad:vibrationDuration="20" />
+
+</RelativeLayout>
9 example/res/menu/activity_main.xml
@@ -0,0 +1,9 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_settings"
+ android:orderInCategory="100"
+ android:showAsAction="never"
+ android:title="@string/menu_settings"/>
+
+</menu>
11 example/res/values-v11/styles.xml
@@ -0,0 +1,11 @@
+<resources>
+
+ <!--
+ Base application theme for API 11+. This theme completely replaces
+ AppBaseTheme from res/values/styles.xml on API 11+ devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Holo">
+ <!-- API 11 theme customizations can go here. -->
+ </style>
+
+</resources>
12 example/res/values-v14/styles.xml
@@ -0,0 +1,12 @@
+<resources>
+
+ <!--
+ Base application theme for API 14+. This theme completely replaces
+ AppBaseTheme from BOTH res/values/styles.xml and
+ res/values-v11/styles.xml on API 14+ devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Holo">
+ <!-- API 14 theme customizations can go here. -->
+ </style>
+
+</resources>
23 example/res/values/arrays.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <array name="snooze_dismiss_drawables">
+ <item>@drawable/ic_item_google</item>
+ <item>@null</item>
+ <item>@drawable/ic_item_camera</item>
+ <item>@null</item>
+ </array>
+ <array name="snooze_dismiss_descriptions">
+ <item>dismiss</item>
+ <item>@null</item>
+ <item>snooze</item>
+ <item>@null</item>
+ </array>
+ <array name="snooze_dismiss_direction_descriptions">
+ <item>right</item>
+ <item>@null</item>
+ <item>left</item>
+ <item>@null</item>
+ </array>
+
+</resources>
8 example/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">GlowPadExample</string>
+ <string name="hello_world">Hello world!</string>
+ <string name="menu_settings">Settings</string>
+
+</resources>
20 example/res/values/styles.xml
@@ -0,0 +1,20 @@
+<resources>
+
+ <!--
+ Base application theme, dependent on API level. This theme is replaced
+ by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Light">
+ <!--
+ Theme customizations available in newer API levels can go in
+ res/values-vXX/styles.xml, while customizations related to
+ backward-compatibility can go here.
+ -->
+ </style>
+
+ <!-- Application theme. -->
+ <style name="AppTheme" parent="AppBaseTheme">
+ <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+ </style>
+
+</resources>
68 example/src/com/example/glowpadexample/MainActivity.java
@@ -0,0 +1,68 @@
+package com.example.glowpadexample;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Toast;
+
+import com.fima.glowpadview.GlowPadView;
+import com.fima.glowpadview.GlowPadView.OnTriggerListener;
+
+public class MainActivity extends Activity implements OnTriggerListener {
+
+ private GlowPadView mGlowPadView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ mGlowPadView = (GlowPadView) findViewById(R.id.glow_pad_view);
+
+ // mGlowPadView.setVibrateEnabled(true);
+ mGlowPadView.setOnTriggerListener(this);
+ }
+
+ @Override
+ public void onGrabbed(View v, int handle) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onReleased(View v, int handle) {
+ mGlowPadView.ping();
+
+ }
+
+ @Override
+ public void onTrigger(View v, int target) {
+ final int resId = mGlowPadView.getResourceIdForTarget(target);
+ switch (resId) {
+ case R.drawable.ic_item_camera:
+ Toast.makeText(this, "Camera selected", Toast.LENGTH_SHORT).show();
+ break;
+
+ case R.drawable.ic_item_google:
+ Toast.makeText(this, "Google selected", Toast.LENGTH_SHORT).show();
+
+ break;
+ default:
+ // Code should never reach here.
+ }
+
+ }
+
+ @Override
+ public void onGrabbedStateChange(View v, int handle) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onFinishFinalAnimation() {
+ // TODO Auto-generated method stub
+
+ }
+
+}
8 library/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="src" path="gen"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry kind="output" path="bin/classes"/>
+</classpath>
33 library/.project
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>GlowPadView</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ApkBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
14 library/AndroidManifest.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.fima.glowpadview"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-permission android:name="android.permission.VIBRATE" />
+
+ <uses-sdk
+ android:minSdkVersion="16"
+ android:targetSdkVersion="17" />
+
+
+</manifest>
15 library/project.properties
@@ -0,0 +1,15 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-17
+android.library=true
BIN  library/res/drawable-xhdpi/ic_launcher.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  library/res/drawable-xhdpi/ic_lockscreen_glowdot.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  library/res/drawable-xhdpi/ic_lockscreen_handle_normal.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  library/res/drawable-xhdpi/ic_lockscreen_handle_pressed.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
76 library/res/values/attrs.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<!-- =============================== -->
+<!-- GlowPadView class attributes -->
+<!-- =============================== -->
+<resources>
+ <declare-styleable name="GlowPadView">
+ <attr name="android:gravity"/>
+
+ <!-- Reference to an array resource that be shown as targets around a circle. -->
+ <attr name="targetDrawables" format="reference"/>
+
+ <!-- Reference to an array resource that be used as description for the targets around the circle. -->
+ <attr name="targetDescriptions" format="reference"/>
+
+ <!-- Reference to an array resource that be used to announce the directions with targets around the circle. -->
+ <attr name="directionDescriptions" format="reference"/>
+
+ <!-- Sets a drawable as the center. -->
+ <attr name="handleDrawable" format="reference"/>
+
+ <!-- Drawable to use for wave ripple animation. -->
+ <attr name="outerRingDrawable" format="reference"/>
+
+ <!-- Drawble used for drawing points -->
+ <attr name="pointDrawable" format="reference"/>
+
+ <!-- Inner radius of glow area. -->
+ <attr name="innerRadius" format="dimension"/>
+
+ <!-- Outer radius of glow area. Target icons will be drawn on this circle. -->
+ <attr name="outerRadius" format="dimension"/>
+
+ <!-- Size of target radius. Points within this distance of target center is a "hit". -->
+ <!--
+ <attr name="hitRadius" format="dimension"/>
+ -->
+
+ <!-- Radius of glow under finger. -->
+ <attr name="glowRadius" format="dimension"/>
+
+ <!-- Tactile feedback duration for actions. Set to '0' for no vibration. -->
+ <attr name="vibrationDuration" format="integer"/>
+
+ <!-- How close we need to be before snapping to a target. -->
+ <attr name="snapMargin" format="dimension"/>
+
+ <!-- Number of waves/chevrons to show in animation. -->
+ <attr name="feedbackCount" format="integer"/>
+
+ <!-- Used when the handle shouldn't wait to be hit before following the finger -->
+ <attr name="alwaysTrackFinger" format="boolean"/>
+ </declare-styleable>
+
+ <declare-styleable name="AnalogClock">
+ <attr name="jewelRadius" format="dimension"/>
+ <attr name="jewelOffset" format="dimension"/>
+ <attr name="jewelColor" format="color"/>
+ </declare-styleable>
+
+</resources>
21 library/res/values/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Size of alarm alert outer ring. -->
+ <dimen name="glowpadview_outerring_diameter">270dip</dimen>
+
+ <!-- Default target placement radius for GlowPadView. Should be 1/2 of outerring diameter. -->
+ <dimen name="glowpadview_target_placement_radius">135dip</dimen>
+
+ <!-- Default glow radius for GlowPadView -->
+ <dimen name="glowpadview_glow_radius">75dip</dimen>
+
+ <!-- Default distance beyond which GlowPadView snaps to the matching target -->
+ <dimen name="glowpadview_snap_margin">40dip</dimen>
+
+ <!-- Default distance from each snap target that GlowPadView considers a "hit" -->
+ <dimen name="glowpadview_inner_radius">15dip</dimen>
+ <dimen name="glowpadview_margin_bottom">-64dip</dimen>
+ <dimen name="glowpadview_margin_right">0dip</dimen>
+
+</resources>
6 library/res/values/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">GlowPadView</string>
+
+</resources>
133 library/src/com/fima/glowpadview/Ease.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.fima.glowpadview;
+
+import android.animation.TimeInterpolator;
+
+
+class Ease {
+ private static final float DOMAIN = 1.0f;
+ private static final float DURATION = 1.0f;
+ private static final float START = 0.0f;
+
+ static class Linear {
+ public static final TimeInterpolator easeNone = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return input;
+ }
+ };
+ }
+
+ static class Cubic {
+ public static final TimeInterpolator easeIn = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return DOMAIN*(input/=DURATION)*input*input + START;
+ }
+ };
+ public static final TimeInterpolator easeOut = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return DOMAIN*((input=input/DURATION-1)*input*input + 1) + START;
+ }
+ };
+ public static final TimeInterpolator easeInOut = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return ((input/=DURATION/2) < 1.0f) ?
+ (DOMAIN/2*input*input*input + START)
+ : (DOMAIN/2*((input-=2)*input*input + 2) + START);
+ }
+ };
+ }
+
+ static class Quad {
+ public static final TimeInterpolator easeIn = new TimeInterpolator() {
+ public float getInterpolation (float input) {
+ return DOMAIN*(input/=DURATION)*input + START;
+ }
+ };
+ public static final TimeInterpolator easeOut = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return -DOMAIN *(input/=DURATION)*(input-2) + START;
+ }
+ };
+ public static final TimeInterpolator easeInOut = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return ((input/=DURATION/2) < 1) ?
+ (DOMAIN/2*input*input + START)
+ : (-DOMAIN/2 * ((--input)*(input-2) - 1) + START);
+ }
+ };
+ }
+
+ static class Quart {
+ public static final TimeInterpolator easeIn = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return DOMAIN*(input/=DURATION)*input*input*input + START;
+ }
+ };
+ public static final TimeInterpolator easeOut = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return -DOMAIN * ((input=input/DURATION-1)*input*input*input - 1) + START;
+ }
+ };
+ public static final TimeInterpolator easeInOut = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return ((input/=DURATION/2) < 1) ?
+ (DOMAIN/2*input*input*input*input + START)
+ : (-DOMAIN/2 * ((input-=2)*input*input*input - 2) + START);
+ }
+ };
+ }
+
+ static class Quint {
+ public static final TimeInterpolator easeIn = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return DOMAIN*(input/=DURATION)*input*input*input*input + START;
+ }
+ };
+ public static final TimeInterpolator easeOut = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return DOMAIN*((input=input/DURATION-1)*input*input*input*input + 1) + START;
+ }
+ };
+ public static final TimeInterpolator easeInOut = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return ((input/=DURATION/2) < 1) ?
+ (DOMAIN/2*input*input*input*input*input + START)
+ : (DOMAIN/2*((input-=2)*input*input*input*input + 2) + START);
+ }
+ };
+ }
+
+ static class Sine {
+ public static final TimeInterpolator easeIn = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return -DOMAIN * (float) Math.cos(input/DURATION * (Math.PI/2)) + DOMAIN + START;
+ }
+ };
+ public static final TimeInterpolator easeOut = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return DOMAIN * (float) Math.sin(input/DURATION * (Math.PI/2)) + START;
+ }
+ };
+ public static final TimeInterpolator easeInOut = new TimeInterpolator() {
+ public float getInterpolation(float input) {
+ return -DOMAIN/2 * ((float)Math.cos(Math.PI*input/DURATION) - 1.0f) + START;
+ }
+ };
+ }
+
+}
1,259 library/src/com/fima/glowpadview/GlowPadView.java
@@ -0,0 +1,1259 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.fima.glowpadview;
+
+import java.util.ArrayList;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Vibrator;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+
+/**
+ * This is a copy of com.android.internal.widget.multiwaveview.GlowPadView with minor changes
+ * to remove dependencies on private api's.
+ *
+ * Contains changes up to If296b60af2421bfa1a9a082e608ba77b2392a218
+ *
+ * A re-usable widget containing a center, outer ring and wave animation.
+ */
+public class GlowPadView extends View {
+ private static final String TAG = "GlowPadView";
+ private static final boolean DEBUG = false;
+
+ // Wave state machine
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_START = 1;
+ private static final int STATE_FIRST_TOUCH = 2;
+ private static final int STATE_TRACKING = 3;
+ private static final int STATE_SNAP = 4;
+ private static final int STATE_FINISH = 5;
+
+ // Animation properties.
+ private static final float SNAP_MARGIN_DEFAULT = 20.0f; // distance to ring before we snap to it
+
+ public interface OnTriggerListener {
+ int NO_HANDLE = 0;
+ int CENTER_HANDLE = 1;
+ public void onGrabbed(View v, int handle);
+ public void onReleased(View v, int handle);
+ public void onTrigger(View v, int target);
+ public void onGrabbedStateChange(View v, int handle);
+ public void onFinishFinalAnimation();
+ }
+
+ // Tuneable parameters for animation
+ private static final int WAVE_ANIMATION_DURATION = 1350;
+ private static final int RETURN_TO_HOME_DELAY = 1200;
+ private static final int RETURN_TO_HOME_DURATION = 200;
+ private static final int HIDE_ANIMATION_DELAY = 200;
+ private static final int HIDE_ANIMATION_DURATION = 200;
+ private static final int SHOW_ANIMATION_DURATION = 200;
+ private static final int SHOW_ANIMATION_DELAY = 50;
+ private static final int INITIAL_SHOW_HANDLE_DURATION = 200;
+ private static final int REVEAL_GLOW_DELAY = 0;
+ private static final int REVEAL_GLOW_DURATION = 0;
+
+ private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f;
+ private static final float TARGET_SCALE_EXPANDED = 1.0f;
+ private static final float TARGET_SCALE_COLLAPSED = 0.8f;
+ private static final float RING_SCALE_EXPANDED = 1.0f;
+ private static final float RING_SCALE_COLLAPSED = 0.5f;
+
+ private ArrayList<TargetDrawable> mTargetDrawables = new ArrayList<TargetDrawable>();
+ private AnimationBundle mWaveAnimations = new AnimationBundle();
+ private AnimationBundle mTargetAnimations = new AnimationBundle();
+ private AnimationBundle mGlowAnimations = new AnimationBundle();
+ private ArrayList<String> mTargetDescriptions;
+ private ArrayList<String> mDirectionDescriptions;
+ private OnTriggerListener mOnTriggerListener;
+ private TargetDrawable mHandleDrawable;
+ private TargetDrawable mOuterRing;
+ private Vibrator mVibrator;
+
+ private int mFeedbackCount = 3;
+ private int mVibrationDuration = 0;
+ private int mGrabbedState;
+ private int mActiveTarget = -1;
+ private float mGlowRadius;
+ private float mWaveCenterX;
+ private float mWaveCenterY;
+ private int mMaxTargetHeight;
+ private int mMaxTargetWidth;
+
+ private float mOuterRadius = 0.0f;
+ private float mSnapMargin = 0.0f;
+ private boolean mDragging;
+ private int mNewTargetResources;
+
+ private class AnimationBundle extends ArrayList<Tweener> {
+ private static final long serialVersionUID = 0xA84D78726F127468L;
+ private boolean mSuspended;
+
+ public void start() {
+ if (mSuspended) return; // ignore attempts to start animations
+ final int count = size();
+ for (int i = 0; i < count; i++) {
+ Tweener anim = get(i);
+ anim.animator.start();
+ }
+ }
+
+ public void cancel() {
+ final int count = size();
+ for (int i = 0; i < count; i++) {
+ Tweener anim = get(i);
+ anim.animator.cancel();
+ }
+ clear();
+ }
+
+ public void stop() {
+ final int count = size();
+ for (int i = 0; i < count; i++) {
+ Tweener anim = get(i);
+ anim.animator.end();
+ }
+ clear();
+ }
+
+ public void setSuspended(boolean suspend) {
+ mSuspended = suspend;
+ }
+ };
+
+ private AnimatorListener mResetListener = new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator animator) {
+ switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY);
+ dispatchOnFinishFinalAnimation();
+ }
+ };
+
+ private AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator animator) {
+ ping();
+ switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY);
+ dispatchOnFinishFinalAnimation();
+ }
+ };
+
+ private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() {
+ public void onAnimationUpdate(ValueAnimator animation) {
+ invalidate();
+ }
+ };
+
+ private boolean mAnimatingTargets;
+ private AnimatorListener mTargetUpdateListener = new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator animator) {
+ if (mNewTargetResources != 0) {
+ internalSetTargetResources(mNewTargetResources);
+ mNewTargetResources = 0;
+ hideTargets(false, false);
+ }
+ mAnimatingTargets = false;
+ }
+ };
+ private int mTargetResourceId;
+ private int mTargetDescriptionsResourceId;
+ private int mDirectionDescriptionsResourceId;
+ private boolean mAlwaysTrackFinger;
+ private int mHorizontalInset;
+ private int mVerticalInset;
+ private int mGravity = Gravity.TOP;
+ private boolean mInitialLayout = true;
+ private Tweener mBackgroundAnimator;
+ private PointCloud mPointCloud;
+ private float mInnerRadius;
+ private int mPointerId;
+
+ public GlowPadView(Context context) {
+ this(context, null);
+ }
+
+ public GlowPadView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ Resources res = context.getResources();
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GlowPadView);
+ mInnerRadius = a.getDimension(R.styleable.GlowPadView_innerRadius, mInnerRadius);
+ mOuterRadius = a.getDimension(R.styleable.GlowPadView_outerRadius, mOuterRadius);
+ mSnapMargin = a.getDimension(R.styleable.GlowPadView_snapMargin, mSnapMargin);
+ mVibrationDuration = a.getInt(R.styleable.GlowPadView_vibrationDuration,
+ mVibrationDuration);
+ mFeedbackCount = a.getInt(R.styleable.GlowPadView_feedbackCount,
+ mFeedbackCount);
+ TypedValue handle = a.peekValue(R.styleable.GlowPadView_handleDrawable);
+ mHandleDrawable = new TargetDrawable(res, handle != null ? handle.resourceId : 0, 2);
+ mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
+ mOuterRing = new TargetDrawable(res,
+ getResourceId(a, R.styleable.GlowPadView_outerRingDrawable), 1);
+
+ mAlwaysTrackFinger = a.getBoolean(R.styleable.GlowPadView_alwaysTrackFinger, false);
+
+ int pointId = getResourceId(a, R.styleable.GlowPadView_pointDrawable);
+ Drawable pointDrawable = pointId != 0 ? res.getDrawable(pointId) : null;
+ mGlowRadius = a.getDimension(R.styleable.GlowPadView_glowRadius, 0.0f);
+
+ TypedValue outValue = new TypedValue();
+
+ // Read array of target drawables
+ if (a.getValue(R.styleable.GlowPadView_targetDrawables, outValue)) {
+ internalSetTargetResources(outValue.resourceId);
+ }
+ if (mTargetDrawables == null || mTargetDrawables.size() == 0) {
+ throw new IllegalStateException("Must specify at least one target drawable");
+ }
+
+ // Read array of target descriptions
+ if (a.getValue(R.styleable.GlowPadView_targetDescriptions, outValue)) {
+ final int resourceId = outValue.resourceId;
+ if (resourceId == 0) {
+ throw new IllegalStateException("Must specify target descriptions");
+ }
+ setTargetDescriptionsResourceId(resourceId);
+ }
+
+ // Read array of direction descriptions
+ if (a.getValue(R.styleable.GlowPadView_directionDescriptions, outValue)) {
+ final int resourceId = outValue.resourceId;
+ if (resourceId == 0) {
+ throw new IllegalStateException("Must specify direction descriptions");
+ }
+ setDirectionDescriptionsResourceId(resourceId);
+ }
+
+ a.recycle();
+
+ // Use gravity attribute from LinearLayout
+ //a = context.obtainStyledAttributes(attrs, R.styleable.LinearLayout);
+ mGravity = a.getInt(R.styleable.GlowPadView_android_gravity, Gravity.TOP);
+ a.recycle();
+
+
+ setVibrateEnabled(mVibrationDuration > 0);
+
+ assignDefaultsIfNeeded();
+
+ mPointCloud = new PointCloud(pointDrawable);
+ mPointCloud.makePointCloud(mInnerRadius, mOuterRadius);
+ mPointCloud.glowManager.setRadius(mGlowRadius);
+ }
+
+ private int getResourceId(TypedArray a, int id) {
+ TypedValue tv = a.peekValue(id);
+ return tv == null ? 0 : tv.resourceId;
+ }
+
+ private void dump() {
+ Log.v(TAG, "Outer Radius = " + mOuterRadius);
+ Log.v(TAG, "SnapMargin = " + mSnapMargin);
+ Log.v(TAG, "FeedbackCount = " + mFeedbackCount);
+ Log.v(TAG, "VibrationDuration = " + mVibrationDuration);
+ Log.v(TAG, "GlowRadius = " + mGlowRadius);
+ Log.v(TAG, "WaveCenterX = " + mWaveCenterX);
+ Log.v(TAG, "WaveCenterY = " + mWaveCenterY);
+ }
+
+ public void suspendAnimations() {
+ mWaveAnimations.setSuspended(true);
+ mTargetAnimations.setSuspended(true);
+ mGlowAnimations.setSuspended(true);
+ }
+
+ public void resumeAnimations() {
+ mWaveAnimations.setSuspended(false);
+ mTargetAnimations.setSuspended(false);
+ mGlowAnimations.setSuspended(false);
+ mWaveAnimations.start();
+ mTargetAnimations.start();
+ mGlowAnimations.start();
+ }
+
+ @Override
+ protected int getSuggestedMinimumWidth() {
+ // View should be large enough to contain the background + handle and
+ // target drawable on either edge.
+ return (int) (Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + mMaxTargetWidth);
+ }
+
+ @Override
+ protected int getSuggestedMinimumHeight() {
+ // View should be large enough to contain the unlock ring + target and
+ // target drawable on either edge
+ return (int) (Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + mMaxTargetHeight);
+ }
+
+ private int resolveMeasured(int measureSpec, int desired)
+ {
+ int result = 0;
+ int specSize = MeasureSpec.getSize(measureSpec);
+ switch (MeasureSpec.getMode(measureSpec)) {
+ case MeasureSpec.UNSPECIFIED:
+ result = desired;
+ break;
+ case MeasureSpec.AT_MOST:
+ result = Math.min(specSize, desired);
+ break;
+ case MeasureSpec.EXACTLY:
+ default:
+ result = specSize;
+ }
+ return result;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int minimumWidth = getSuggestedMinimumWidth();
+ final int minimumHeight = getSuggestedMinimumHeight();
+ int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
+ int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
+ computeInsets((computedWidth - minimumWidth), (computedHeight - minimumHeight));
+ setMeasuredDimension(computedWidth, computedHeight);
+ }
+
+ private void switchToState(int state, float x, float y) {
+ switch (state) {
+ case STATE_IDLE:
+ deactivateTargets();
+ hideGlow(0, 0, 0.0f, null);
+ startBackgroundAnimation(0, 0.0f);
+ mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
+ mHandleDrawable.setAlpha(1.0f);
+ break;
+
+ case STATE_START:
+ startBackgroundAnimation(0, 0.0f);
+ break;
+
+ case STATE_FIRST_TOUCH:
+ mHandleDrawable.setAlpha(0.0f);
+ deactivateTargets();
+ showTargets(true);
+ startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f);
+ setGrabbedState(OnTriggerListener.CENTER_HANDLE);
+
+ final AccessibilityManager accessibilityManager =
+ (AccessibilityManager) getContext().getSystemService(
+ Context.ACCESSIBILITY_SERVICE);
+ if (accessibilityManager.isEnabled()) {
+ announceTargets();
+ }
+ break;
+
+ case STATE_TRACKING:
+ mHandleDrawable.setAlpha(0.0f);
+ showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 1.0f, null);
+ break;
+
+ case STATE_SNAP:
+ // TODO: Add transition states (see list_selector_background_transition.xml)
+ mHandleDrawable.setAlpha(0.0f);
+ showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 0.0f, null);
+ break;
+
+ case STATE_FINISH:
+ doFinish();
+ break;
+ }
+ }
+
+ private void showGlow(int duration, int delay, float finalAlpha,
+ AnimatorListener finishListener) {
+ mGlowAnimations.cancel();
+ mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration,
+ "ease", Ease.Cubic.easeIn,
+ "delay", delay,
+ "alpha", finalAlpha,
+ "onUpdate", mUpdateListener,
+ "onComplete", finishListener));
+ mGlowAnimations.start();
+ }
+
+ private void hideGlow(int duration, int delay, float finalAlpha,
+ AnimatorListener finishListener) {
+ mGlowAnimations.cancel();
+ mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration,
+ "ease", Ease.Quart.easeOut,
+ "delay", delay,
+ "alpha", finalAlpha,
+ "x", 0.0f,
+ "y", 0.0f,
+ "onUpdate", mUpdateListener,
+ "onComplete", finishListener));
+ mGlowAnimations.start();
+ }
+
+ private void deactivateTargets() {
+ final int count = mTargetDrawables.size();
+ for (int i = 0; i < count; i++) {
+ TargetDrawable target = mTargetDrawables.get(i);
+ target.setState(TargetDrawable.STATE_INACTIVE);
+ }
+ mActiveTarget = -1;
+ }
+
+ /**
+ * Dispatches a trigger event to listener. Ignored if a listener is not set.
+ * @param whichTarget the target that was triggered.
+ */
+ private void dispatchTriggerEvent(int whichTarget) {
+ vibrate();
+ if (mOnTriggerListener != null) {
+ mOnTriggerListener.onTrigger(this, whichTarget);
+ }
+ }
+
+ private void dispatchOnFinishFinalAnimation() {
+ if (mOnTriggerListener != null) {
+ mOnTriggerListener.onFinishFinalAnimation();
+ }
+ }
+
+ private void doFinish() {
+ final int activeTarget = mActiveTarget;
+ final boolean targetHit = activeTarget != -1;
+
+ if (targetHit) {
+ if (DEBUG) Log.v(TAG, "Finish with target hit = " + targetHit);
+
+ highlightSelected(activeTarget);
+
+ // Inform listener of any active targets. Typically only one will be active.
+ hideGlow(RETURN_TO_HOME_DURATION, RETURN_TO_HOME_DELAY, 0.0f, mResetListener);
+ dispatchTriggerEvent(activeTarget);
+ if (!mAlwaysTrackFinger) {
+ // Force ring and targets to finish animation to final expanded state
+ mTargetAnimations.stop();
+ }
+ } else {
+ // Animate handle back to the center based on current state.
+ hideGlow(HIDE_ANIMATION_DURATION, 0, 0.0f, mResetListenerWithPing);
+ hideTargets(true, false);
+ }
+
+ setGrabbedState(OnTriggerListener.NO_HANDLE);
+ }
+
+ private void highlightSelected(int activeTarget) {
+ // Highlight the given target and fade others
+ mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE);
+ hideUnselected(activeTarget);
+ }
+
+ private void hideUnselected(int active) {
+ for (int i = 0; i < mTargetDrawables.size(); i++) {
+ if (i != active) {
+ mTargetDrawables.get(i).setAlpha(0.0f);
+ }
+ }
+ }
+
+ private void hideTargets(boolean animate, boolean expanded) {
+ mTargetAnimations.cancel();
+ // Note: these animations should complete at the same time so that we can swap out
+ // the target assets asynchronously from the setTargetResources() call.
+ mAnimatingTargets = animate;
+ final int duration = animate ? HIDE_ANIMATION_DURATION : 0;
+ final int delay = animate ? HIDE_ANIMATION_DELAY : 0;
+
+ final float targetScale = expanded ?
+ TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED;
+ final int length = mTargetDrawables.size();
+ final TimeInterpolator interpolator = Ease.Cubic.easeOut;
+ for (int i = 0; i < length; i++) {
+ TargetDrawable target = mTargetDrawables.get(i);
+ target.setState(TargetDrawable.STATE_INACTIVE);
+ mTargetAnimations.add(Tweener.to(target, duration,
+ "ease", interpolator,
+ "alpha", 0.0f,
+ "scaleX", targetScale,
+ "scaleY", targetScale,
+ "delay", delay,
+ "onUpdate", mUpdateListener));
+ }
+
+ final float ringScaleTarget = expanded ?
+ RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED;
+ mTargetAnimations.add(Tweener.to(mOuterRing, duration,
+ "ease", interpolator,
+ "alpha", 0.0f,
+ "scaleX", ringScaleTarget,
+ "scaleY", ringScaleTarget,
+ "delay", delay,
+ "onUpdate", mUpdateListener,
+ "onComplete", mTargetUpdateListener));
+
+ mTargetAnimations.start();
+ }
+
+ private void showTargets(boolean animate) {
+ mTargetAnimations.stop();
+ mAnimatingTargets = animate;
+ final int delay = animate ? SHOW_ANIMATION_DELAY : 0;
+ final int duration = animate ? SHOW_ANIMATION_DURATION : 0;
+ final int length = mTargetDrawables.size();
+ for (int i = 0; i < length; i++) {
+ TargetDrawable target = mTargetDrawables.get(i);
+ target.setState(TargetDrawable.STATE_INACTIVE);
+ mTargetAnimations.add(Tweener.to(target, duration,
+ "ease", Ease.Cubic.easeOut,
+ "alpha", 1.0f,
+ "scaleX", 1.0f,
+ "scaleY", 1.0f,
+ "delay", delay,
+ "onUpdate", mUpdateListener));
+ }
+ mTargetAnimations.add(Tweener.to(mOuterRing, duration,
+ "ease", Ease.Cubic.easeOut,
+ "alpha", 1.0f,
+ "scaleX", 1.0f,
+ "scaleY", 1.0f,
+ "delay", delay,
+ "onUpdate", mUpdateListener,
+ "onComplete", mTargetUpdateListener));
+
+ mTargetAnimations.start();
+ }
+
+ private void vibrate() {
+ if (mVibrator != null) {
+ mVibrator.vibrate(mVibrationDuration);
+ }
+ }
+
+ private ArrayList<TargetDrawable> loadDrawableArray(int resourceId) {
+ Resources res = getContext().getResources();
+ TypedArray array = res.obtainTypedArray(resourceId);
+ final int count = array.length();
+ ArrayList<TargetDrawable> drawables = new ArrayList<TargetDrawable>(count);
+ for (int i = 0; i < count; i++) {
+ TypedValue value = array.peekValue(i);
+ TargetDrawable target = new TargetDrawable(res, value != null ? value.resourceId : 0, 3);
+ drawables.add(target);
+ }
+ array.recycle();
+ return drawables;
+ }
+
+ private void internalSetTargetResources(int resourceId) {
+ final ArrayList<TargetDrawable> targets = loadDrawableArray(resourceId);
+ mTargetDrawables = targets;
+ mTargetResourceId = resourceId;
+
+ int maxWidth = mHandleDrawable.getWidth();
+ int maxHeight = mHandleDrawable.getHeight();
+ final int count = targets.size();
+ for (int i = 0; i < count; i++) {
+ TargetDrawable target = targets.get(i);
+ maxWidth = Math.max(maxWidth, target.getWidth());
+ maxHeight = Math.max(maxHeight, target.getHeight());
+ }
+ if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) {
+ mMaxTargetWidth = maxWidth;
+ mMaxTargetHeight = maxHeight;
+ requestLayout(); // required to resize layout and call updateTargetPositions()
+ } else {
+ updateTargetPositions(mWaveCenterX, mWaveCenterY);
+ updatePointCloudPosition(mWaveCenterX, mWaveCenterY);
+ }
+ }
+
+ /**
+ * Loads an array of drawables from the given resourceId.
+ *
+ * @param resourceId
+ */
+ public void setTargetResources(int resourceId) {
+ if (mAnimatingTargets) {
+ // postpone this change until we return to the initial state
+ mNewTargetResources = resourceId;
+ } else {
+ internalSetTargetResources(resourceId);
+ }
+ }
+
+ public int getTargetResourceId() {
+ return mTargetResourceId;
+ }
+
+ /**
+ * Sets the resource id specifying the target descriptions for accessibility.
+ *
+ * @param resourceId The resource id.
+ */
+ public void setTargetDescriptionsResourceId(int resourceId) {
+ mTargetDescriptionsResourceId = resourceId;
+ if (mTargetDescriptions != null) {
+ mTargetDescriptions.clear();
+ }
+ }
+
+ /**
+ * Gets the resource id specifying the target descriptions for accessibility.
+ *
+ * @return The resource id.
+ */
+ public int getTargetDescriptionsResourceId() {
+ return mTargetDescriptionsResourceId;
+ }
+
+ /**
+ * Sets the resource id specifying the target direction descriptions for accessibility.
+ *
+ * @param resourceId The resource id.
+ */
+ public void setDirectionDescriptionsResourceId(int resourceId) {
+ mDirectionDescriptionsResourceId = resourceId;
+ if (mDirectionDescriptions != null) {
+ mDirectionDescriptions.clear();
+ }
+ }
+
+ /**
+ * Gets the resource id specifying the target direction descriptions.
+ *
+ * @return The resource id.
+ */
+ public int getDirectionDescriptionsResourceId() {
+ return mDirectionDescriptionsResourceId;
+ }
+
+ /**
+ * Enable or disable vibrate on touch.
+ *
+ * @param enabled
+ */
+ public void setVibrateEnabled(boolean enabled) {
+ if (enabled && mVibrator == null) {
+ mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
+ } else {
+ mVibrator = null;
+ }
+ }
+
+ /**
+ * Starts wave animation.
+ *
+ */
+ public void ping() {
+ if (mFeedbackCount > 0) {
+ boolean doWaveAnimation = true;
+ final AnimationBundle waveAnimations = mWaveAnimations;
+
+ // Don't do a wave if there's already one in progress
+ if (waveAnimations.size() > 0 && waveAnimations.get(0).animator.isRunning()) {
+ long t = waveAnimations.get(0).animator.getCurrentPlayTime();
+ if (t < WAVE_ANIMATION_DURATION/2) {
+ doWaveAnimation = false;
+ }
+ }
+
+ if (doWaveAnimation) {
+ startWaveAnimation();
+ }
+ }
+ }
+
+ private void stopAndHideWaveAnimation() {
+ mWaveAnimations.cancel();
+ mPointCloud.waveManager.setAlpha(0.0f);
+ }
+
+ private void startWaveAnimation() {
+ mWaveAnimations.cancel();
+ mPointCloud.waveManager.setAlpha(1.0f);
+ mPointCloud.waveManager.setRadius(mHandleDrawable.getWidth()/2.0f);
+ mWaveAnimations.add(Tweener.to(mPointCloud.waveManager, WAVE_ANIMATION_DURATION,
+ "ease", Ease.Quad.easeOut,
+ "delay", 0,
+ "radius", 2.0f * mOuterRadius,
+ "onUpdate", mUpdateListener,
+ "onComplete",
+ new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator animator) {
+ mPointCloud.waveManager.setRadius(0.0f);
+ mPointCloud.waveManager.setAlpha(0.0f);
+ }
+ }));
+ mWaveAnimations.start();
+ }
+
+ /**
+ * Resets the widget to default state and cancels all animation. If animate is 'true', will
+ * animate objects into place. Otherwise, objects will snap back to place.
+ *
+ * @param animate
+ */
+ public void reset(boolean animate) {
+ mGlowAnimations.stop();
+ mTargetAnimations.stop();
+ startBackgroundAnimation(0, 0.0f);
+ stopAndHideWaveAnimation();
+ hideTargets(animate, false);
+ hideGlow(0, 0, 0.0f, null);
+ Tweener.reset();
+ }
+
+ private void startBackgroundAnimation(int duration, float alpha) {
+ final Drawable background = getBackground();
+ if (mAlwaysTrackFinger && background != null) {
+ if (mBackgroundAnimator != null) {
+ mBackgroundAnimator.animator.cancel();
+ }
+ mBackgroundAnimator = Tweener.to(background, duration,
+ "ease", Ease.Cubic.easeIn,
+ "alpha", (int)(255.0f * alpha),
+ "delay", SHOW_ANIMATION_DELAY);
+ mBackgroundAnimator.animator.start();
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ final int action = event.getActionMasked();
+ boolean handled = false;
+ switch (action) {
+ case MotionEvent.ACTION_POINTER_DOWN:
+ case MotionEvent.ACTION_DOWN:
+ if (DEBUG) Log.v(TAG, "*** DOWN ***");
+ handleDown(event);
+ handleMove(event);
+ handled = true;
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (DEBUG) Log.v(TAG, "*** MOVE ***");
+ handleMove(event);
+ handled = true;
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ case MotionEvent.ACTION_UP:
+ if (DEBUG) Log.v(TAG, "*** UP ***");
+ handleMove(event);
+ handleUp(event);
+ handled = true;
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ if (DEBUG) Log.v(TAG, "*** CANCEL ***");
+ handleMove(event);
+ handleCancel(event);
+ handled = true;
+ break;
+ }
+ invalidate();
+ return handled ? true : super.onTouchEvent(event);
+ }
+
+ private void updateGlowPosition(float x, float y) {
+ mPointCloud.glowManager.setX(x);
+ mPointCloud.glowManager.setY(y);
+ }
+
+ private void handleDown(MotionEvent event) {
+ int actionIndex = event.getActionIndex();
+ float eventX = event.getX(actionIndex);
+ float eventY = event.getY(actionIndex);
+ switchToState(STATE_START, eventX, eventY);
+ if (!trySwitchToFirstTouchState(eventX, eventY)) {
+ mDragging = false;
+ } else {
+ mPointerId = event.getPointerId(actionIndex);
+ updateGlowPosition(eventX, eventY);
+ }
+ }
+
+ private void handleUp(MotionEvent event) {
+ if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE");
+ int actionIndex = event.getActionIndex();
+ if (event.getPointerId(actionIndex) == mPointerId) {
+ switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex));
+ }
+ }
+
+ private void handleCancel(MotionEvent event) {
+ if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL");
+
+ // We should drop the active target here but it interferes with
+ // moving off the screen in the direction of the navigation bar. At some point we may
+ // want to revisit how we handle this. For now we'll allow a canceled event to
+ // activate the current target.
+
+ // mActiveTarget = -1; // Drop the active target if canceled.
+
+ int actionIndex = event.findPointerIndex(mPointerId);
+ actionIndex = actionIndex == -1 ? 0 : actionIndex;
+ switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex));
+ }
+
+ private void handleMove(MotionEvent event) {
+ int activeTarget = -1;
+ final int historySize = event.getHistorySize();
+ ArrayList<TargetDrawable> targets = mTargetDrawables;
+ int ntargets = targets.size();
+ float x = 0.0f;
+ float y = 0.0f;
+ int actionIndex = event.findPointerIndex(mPointerId);
+
+ if (actionIndex == -1) {
+ return; // no data for this pointer
+ }
+
+ for (int k = 0; k < historySize + 1; k++) {
+ float eventX = k < historySize ? event.getHistoricalX(actionIndex, k)
+ : event.getX(actionIndex);
+ float eventY = k < historySize ? event.getHistoricalY(actionIndex, k)
+ :event.getY(actionIndex);
+ // tx and ty are relative to wave center
+ float tx = eventX - mWaveCenterX;
+ float ty = eventY - mWaveCenterY;
+ float touchRadius = (float) Math.sqrt(dist2(tx, ty));
+ final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f;
+ float limitX = tx * scale;
+ float limitY = ty * scale;
+ double angleRad = Math.atan2(-ty, tx);
+
+ if (!mDragging) {
+ trySwitchToFirstTouchState(eventX, eventY);
+ }
+
+ if (mDragging) {
+ // For multiple targets, snap to the one that matches
+ final float snapRadius = mOuterRadius - mSnapMargin;
+ final float snapDistance2 = snapRadius * snapRadius;
+ // Find first target in range
+ for (int i = 0; i < ntargets; i++) {
+ TargetDrawable target = targets.get(i);
+
+ double targetMinRad = (i - 0.5) * 2 * Math.PI / ntargets;
+ double targetMaxRad = (i + 0.5) * 2 * Math.PI / ntargets;
+ if (target.isEnabled()) {
+ boolean angleMatches =
+ (angleRad > targetMinRad && angleRad <= targetMaxRad) ||
+ (angleRad + 2 * Math.PI > targetMinRad &&
+ angleRad + 2 * Math.PI <= targetMaxRad);
+ if (angleMatches && (dist2(tx, ty) > snapDistance2)) {
+ activeTarget = i;
+ }
+ }
+ }
+ }
+ x = limitX;
+ y = limitY;
+ }
+
+ if (!mDragging) {
+ return;
+ }
+
+ if (activeTarget != -1) {
+ switchToState(STATE_SNAP, x,y);
+ updateGlowPosition(x, y);
+ } else {
+ switchToState(STATE_TRACKING, x, y);
+ updateGlowPosition(x, y);
+ }
+
+ if (mActiveTarget != activeTarget) {
+ // Defocus the old target
+ if (mActiveTarget != -1) {
+ TargetDrawable target = targets.get(mActiveTarget);
+ target.setState(TargetDrawable.STATE_INACTIVE);
+ }
+ // Focus the new target
+ if (activeTarget != -1) {
+ TargetDrawable target = targets.get(activeTarget);
+ target.setState(TargetDrawable.STATE_FOCUSED);
+ final AccessibilityManager accessibilityManager =
+ (AccessibilityManager) getContext().getSystemService(
+ Context.ACCESSIBILITY_SERVICE);
+ if (accessibilityManager.isEnabled()) {
+ String targetContentDescription = getTargetDescription(activeTarget);
+ announceForAccessibility(targetContentDescription);
+ }
+ }
+ }
+ mActiveTarget = activeTarget;
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ final AccessibilityManager accessibilityManager =
+ (AccessibilityManager) getContext().getSystemService(
+ Context.ACCESSIBILITY_SERVICE);
+ if (accessibilityManager.isTouchExplorationEnabled()) {
+ final int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ event.setAction(MotionEvent.ACTION_DOWN);
+ break;
+ case MotionEvent.ACTION_HOVER_MOVE:
+ event.setAction(MotionEvent.ACTION_MOVE);
+ break;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ event.setAction(MotionEvent.ACTION_UP);
+ break;
+ }
+ onTouchEvent(event);
+ event.setAction(action);
+ }
+ super.onHoverEvent(event);
+ return true;
+ }
+
+ /**
+ * Sets the current grabbed state, and dispatches a grabbed state change
+ * event to our listener.
+ */
+ private void setGrabbedState(int newState) {
+ if (newState != mGrabbedState) {
+ if (newState != OnTriggerListener.NO_HANDLE) {
+ vibrate();
+ }
+ mGrabbedState = newState;
+ if (mOnTriggerListener != null) {
+ if (newState == OnTriggerListener.NO_HANDLE) {
+ mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE);
+ } else {
+ mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE);
+ }
+ mOnTriggerListener.onGrabbedStateChange(this, newState);
+ }
+ }
+ }
+
+ private boolean trySwitchToFirstTouchState(float x, float y) {
+ final float tx = x - mWaveCenterX;
+ final float ty = y - mWaveCenterY;
+ if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledGlowRadiusSquared()) {
+ if (DEBUG) Log.v(TAG, "** Handle HIT");
+ switchToState(STATE_FIRST_TOUCH, x, y);
+ updateGlowPosition(tx, ty);
+ mDragging = true;
+ return true;
+ }
+ return false;
+ }
+
+ private void assignDefaultsIfNeeded() {
+ if (mOuterRadius == 0.0f) {
+ mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight())/2.0f;
+ }
+ if (mSnapMargin == 0.0f) {
+ mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics());
+ }
+ if (mInnerRadius == 0.0f) {
+ mInnerRadius = mHandleDrawable.getWidth() / 10.0f;
+ }
+ }
+
+ private void computeInsets(int dx, int dy) {
+ final int layoutDirection = getLayoutDirection();
+ final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
+
+ switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.LEFT:
+ mHorizontalInset = 0;
+ break;
+ case Gravity.RIGHT:
+ mHorizontalInset = dx;
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ default:
+ mHorizontalInset = dx / 2;
+ break;
+ }
+ switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
+ case Gravity.TOP:
+ mVerticalInset = 0;
+ break;
+ case Gravity.BOTTOM:
+ mVerticalInset = dy;
+ break;
+ case Gravity.CENTER_VERTICAL:
+ default:
+ mVerticalInset = dy / 2;
+ break;
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ final int width = right - left;
+ final int height = bottom - top;
+
+ // Target placement width/height. This puts the targets on the greater of the ring
+ // width or the specified outer radius.
+ final float placementWidth = Math.max(mOuterRing.getWidth(), 2 * mOuterRadius);
+ final float placementHeight = Math.max(mOuterRing.getHeight(), 2 * mOuterRadius);
+ float newWaveCenterX = mHorizontalInset
+ + Math.max(width, mMaxTargetWidth + placementWidth) / 2;
+ float newWaveCenterY = mVerticalInset
+ + Math.max(height, + mMaxTargetHeight + placementHeight) / 2;
+
+ if (mInitialLayout) {
+ stopAndHideWaveAnimation();
+ hideTargets(false, false);
+ mInitialLayout = false;
+ }
+
+ mOuterRing.setPositionX(newWaveCenterX);
+ mOuterRing.setPositionY(newWaveCenterY);
+
+ mHandleDrawable.setPositionX(newWaveCenterX);
+ mHandleDrawable.setPositionY(newWaveCenterY);
+
+ updateTargetPositions(newWaveCenterX, newWaveCenterY);
+ updatePointCloudPosition(newWaveCenterX, newWaveCenterY);
+ updateGlowPosition(newWaveCenterX, newWaveCenterY);
+
+ mWaveCenterX = newWaveCenterX;
+ mWaveCenterY = newWaveCenterY;
+
+ if (DEBUG) dump();
+ }
+
+ private void updateTargetPositions(float centerX, float centerY) {
+ // Reposition the target drawables if the view changed.
+ ArrayList<TargetDrawable> targets = mTargetDrawables;
+ final int size = targets.size();
+ final float alpha = (float) (-2.0f * Math.PI / size);
+ for (int i = 0; i < size; i++) {
+ final TargetDrawable targetIcon = targets.get(i);
+ final float angle = alpha * i;
+ targetIcon.setPositionX(centerX);
+ targetIcon.setPositionY(centerY);
+ targetIcon.setX(mOuterRadius * (float) Math.cos(angle));
+ targetIcon.setY(mOuterRadius * (float) Math.sin(angle));
+ }
+ }
+
+ private void updatePointCloudPosition(float centerX, float centerY) {
+ mPointCloud.setCenter(centerX, centerY);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ mPointCloud.draw(canvas);
+ mOuterRing.draw(canvas);
+ final int ntargets = mTargetDrawables.size();
+ for (int i = 0; i < ntargets; i++) {
+ TargetDrawable target = mTargetDrawables.get(i);
+ if (target != null) {
+ target.draw(canvas);
+ }
+ }
+ mHandleDrawable.draw(canvas);
+ }
+
+ public void setOnTriggerListener(OnTriggerListener listener) {
+ mOnTriggerListener = listener;
+ }
+
+ private float square(float d) {
+ return d * d;
+ }
+
+ private float dist2(float dx, float dy) {
+ return dx*dx + dy*dy;
+ }
+
+ private float getScaledGlowRadiusSquared() {
+ final float scaledTapRadius;
+ final AccessibilityManager accessibilityManager =
+ (AccessibilityManager) getContext().getSystemService(
+ Context.ACCESSIBILITY_SERVICE);
+ if (accessibilityManager.isEnabled()) {
+ scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mGlowRadius;
+ } else {
+ scaledTapRadius = mGlowRadius;
+ }
+ return square(scaledTapRadius);
+ }
+
+ private void announceTargets() {
+ StringBuilder utterance = new StringBuilder();
+ final int targetCount = mTargetDrawables.size();
+ for (int i = 0; i < targetCount; i++) {
+ String targetDescription = getTargetDescription(i);
+ String directionDescription = getDirectionDescription(i);
+ if (!TextUtils.isEmpty(targetDescription)
+ && !TextUtils.isEmpty(directionDescription)) {
+ String text = String.format(directionDescription, targetDescription);
+ utterance.append(text);
+ }
+ }
+ if (utterance.length() > 0) {
+ announceForAccessibility(utterance.toString());
+ }
+ }
+
+ private String getTargetDescription(int index) {
+ if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) {
+ mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId);
+ if (mTargetDrawables.size() != mTargetDescriptions.size()) {
+ Log.w(TAG, "The number of target drawables must be"
+ + " equal to the number of target descriptions.");
+ return null;
+ }
+ }
+ return mTargetDescriptions.get(index);
+ }
+
+ private String getDirectionDescription(int index) {
+ if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) {
+ mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId);
+ if (mTargetDrawables.size() != mDirectionDescriptions.size()) {
+ Log.w(TAG, "The number of target drawables must be"
+ + " equal to the number of direction descriptions.");
+ return null;
+ }
+ }
+ return mDirectionDescriptions.get(index);
+ }
+
+ private ArrayList<String> loadDescriptions(int resourceId) {
+ TypedArray array = getContext().getResources().obtainTypedArray(resourceId);
+ final int count = array.length();
+ ArrayList<String> targetContentDescriptions = new ArrayList<String>(count);
+ for (int i = 0; i < count; i++) {
+ String contentDescription = array.getString(i);
+ targetContentDescriptions.add(contentDescription);
+ }
+ array.recycle();
+ return targetContentDescriptions;
+ }
+
+ public int getResourceIdForTarget(int index) {
+ final TargetDrawable drawable = mTargetDrawables.get(index);
+ return drawable == null ? 0 : drawable.getResourceId();
+ }
+
+ public void setEnableTarget(int resourceId, boolean enabled) {
+ for (int i = 0; i < mTargetDrawables.size(); i++) {
+ final TargetDrawable target = mTargetDrawables.get(i);
+ if (target.getResourceId() == resourceId) {
+ target.setEnabled(enabled);
+ break; // should never be more than one match
+ }
+ }
+ }
+
+ /**
+ * Gets the position of a target in the array that matches the given resource.
+ * @param resourceId
+ * @return the index or -1 if not found
+ */
+ public int getTargetPosition(int resourceId) {
+ for (int i = 0; i < mTargetDrawables.size(); i++) {
+ final TargetDrawable target = mTargetDrawables.get(i);
+ if (target.getResourceId() == resourceId) {
+ return i; // should never be more than one match
+ }
+ }
+ return -1;
+ }
+
+ private boolean replaceTargetDrawables(Resources res, int existingResourceId,
+ int newResourceId) {
+ if (existingResourceId == 0 || newResourceId == 0) {
+ return false;
+ }
+
+ boolean result = false;
+ final ArrayList<TargetDrawable> drawables = mTargetDrawables;
+ final int size = drawables.size();
+ for (int i = 0; i < size; i++) {
+ final TargetDrawable target = drawables.get(i);
+ if (target != null && target.getResourceId() == existingResourceId) {
+ target.setDrawable(res, newResourceId);
+ result = true;
+ }
+ }
+
+ if (result) {
+ requestLayout(); // in case any given drawable's size changes
+ }
+
+ return result;
+ }
+
+ /**
+ * Searches the given package for a resource to use to replace the Drawable on the
+ * target with the given resource id
+ * @param component of the .apk that contains the resource
+ * @param name of the metadata in the .apk
+ * @param existingResId the resource id of the target to search for
+ * @return true if found in the given package and replaced at least one target Drawables
+ */
+ public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name,
+ int existingResId) {
+ if (existingResId == 0) return false;
+
+ boolean replaced = false;
+ if (component != null) {
+ try {
+ PackageManager packageManager = getContext().getPackageManager();
+ // Look for the search icon specified in the activity meta-data
+ Bundle metaData = packageManager.getActivityInfo(
+ component, PackageManager.GET_META_DATA).metaData;
+ if (metaData != null) {
+ int iconResId = metaData.getInt(name);
+ if (iconResId != 0) {
+ Resources res = packageManager.getResourcesForActivity(component);
+ replaced = replaceTargetDrawables(res, existingResId, iconResId);
+ }
+ }
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Failed to swap drawable; "
+ + component.flattenToShortString() + " not found", e);
+ } catch (Resources.NotFoundException nfe) {
+ Log.w(TAG, "Failed to swap drawable from "
+ + component.flattenToShortString(), nfe);
+ }
+ }
+ if (!replaced) {
+ // Restore the original drawable
+ replaceTargetDrawables(getContext().getResources(), existingResId, existingResId);
+ }
+ return replaced;
+ }
+}
236 library/src/com/fima/glowpadview/PointCloud.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.fima.glowpadview;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.util.FloatMath;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+public class PointCloud {
+ private static final float MIN_POINT_SIZE = 2.0f;
+ private static final float MAX_POINT_SIZE = 4.0f;
+ private static final int INNER_POINTS = 8;
+ private static final String TAG = "PointCloud";
+ private ArrayList<Point> mPointCloud = new ArrayList<Point>();
+ private Drawable mDrawable;
+ private float mCenterX;
+ private float mCenterY;
+ private Paint mPaint;
+ private float mScale = 1.0f;
+ private static final float PI = (float) Math.PI;
+
+ // These allow us to have multiple concurrent animations.
+ WaveManager waveManager = new WaveManager();
+ GlowManager glowManager = new GlowManager();
+ private float mOuterRadius;
+
+ public class WaveManager {
+ private float radius = 50;
+ private float width = 200.0f; // TODO: Make configurable
+ private float alpha = 0.0f;
+ public void setRadius(float r) {
+ radius = r;
+ }
+
+ public float getRadius() {
+ return radius;
+ }
+
+ public void setAlpha(float a) {
+ alpha = a;
+ }
+
+ public float getAlpha() {
+ return alpha;
+ }
+ };
+
+ public class GlowManager {
+ private float x;
+ private float y;
+ private float radius = 0.0f;
+ private float alpha = 0.0f;
+
+ public void setX(float x1) {
+ x = x1;
+ }
+
+ public float getX() {
+ return x;
+ }
+
+ public void setY(float y1) {
+ y = y1;
+ }
+
+ public float getY() {
+ return y;
+ }
+
+ public void setAlpha(float a) {
+ alpha = a;
+ }
+
+ public float getAlpha() {
+ return alpha;
+ }
+
+ public void setRadius(float r) {
+ radius = r;
+ }
+
+ public float getRadius() {
+ return radius;
+ }
+ }
+
+ class Point {
+ float x;
+ float y;
+ float radius;
+
+ public Point(float x2, float y2, float r) {
+ x = (float) x2;
+ y = (float) y2;
+ radius = r;
+ }
+ }
+
+ public PointCloud(Drawable drawable) {
+ mPaint = new Paint();
+ mPaint.setFilterBitmap(true);
+ mPaint.setColor(Color.rgb(255, 255, 255)); // TODO: make configurable
+ mPaint.setAntiAlias(true);
+ mPaint.setDither(true);
+
+ mDrawable = drawable;
+ if (mDrawable != null) {
+ drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+ }
+ }
+
+ public void setCenter(float x, float y) {
+ mCenterX = x;
+ mCenterY = y;
+ }
+
+ public void makePointCloud(float innerRadius, float outerRadius) {
+ if (innerRadius == 0) {
+ Log.w(TAG, "Must specify an inner radius");
+ return;
+ }
+ mOuterRadius = outerRadius;
+ mPointCloud.clear();
+ final float pointAreaRadius = (outerRadius - innerRadius);
+ final float ds = (2.0f * PI * innerRadius / INNER_POINTS);
+ final int bands = (int) Math.round(pointAreaRadius / ds);
+ final float dr = pointAreaRadius / bands;
+ float r = innerRadius;
+ for (int b = 0; b <= bands; b++, r += dr) {
+ float circumference = 2.0f * PI * r;
+ final int pointsInBand = (int) (circumference / ds);
+ float eta = PI/2.0f;
+ float dEta = 2.0f * PI / pointsInBand;
+ for (int i = 0; i < pointsInBand; i++) {
+ float x = r * FloatMath.cos(eta);
+ float y = r * FloatMath.sin(eta);
+ eta += dEta;
+ mPointCloud.add(new Point(x, y, r));
+ }
+ }
+ }
+
+ public void setScale(float scale) {
+ mScale = scale;
+ }
+
+ public float getScale() {
+ return mScale;
+ }
+
+ private static float hypot(float x, float y) {
+ return FloatMath.sqrt(x*x + y*y);
+ }
+
+ private static float max(float a, float b) {
+ return a > b ? a : b;
+ }
+
+ public int getAlphaForPoint(Point point) {
+ // Contribution from positional glow
+ float glowDistance = hypot(glowManager.x - point.x, glowManager.y - point.y);
+ float glowAlpha = 0.0f;
+
+ if (glowDistance < glowManager.radius) {
+ float cosf = FloatMath.cos(PI * 0.25f * glowDistance / glowManager.radius);
+ glowAlpha = glowManager.alpha * max(0.0f, (float) Math.pow(cosf, 10.0f));
+ }
+
+ // Compute contribution from Wave
+ float radius = hypot(point.x, point.y);
+ float distanceToWaveRing = (radius - waveManager.radius);
+ float waveAlpha = 0.0f;
+ if (distanceToWaveRing < waveManager.width * 0.5f && distanceToWaveRing < 0.0f) {
+ float cosf = FloatMath.cos(PI * 0.25f * distanceToWaveRing / waveManager.width);
+ waveAlpha = waveManager.alpha * max(0.0f, (float) Math.pow(cosf, 20.0f));
+ }
+
+ return (int) (max(glowAlpha, waveAlpha) * 255);
+ }
+
+ private float interp(float min, float max, float f) {
+ return min + (max - min) * f;
+ }
+
+ public void draw(Canvas canvas) {
+ ArrayList<Point> points = mPointCloud;
+ canvas.save(Canvas.MATRIX_SAVE_FLAG);
+ canvas.scale(mScale, mScale, mCenterX, mCenterY);
+ for (int i = 0; i < points.size(); i++) {
+ Point point = points.get(i);
+ final float pointSize = interp(MAX_POINT_SIZE, MIN_POINT_SIZE,
+ point.radius / mOuterRadius);
+ final float px = point.x + mCenterX;
+ final float py = point.y + mCenterY;
+ int alpha = getAlphaForPoint(point);
+
+ if (alpha == 0) continue;
+
+ if (mDrawable != null) {
+ canvas.save(Canvas.MATRIX_SAVE_FLAG);
+ final float cx = mDrawable.getIntrinsicWidth() * 0.5f;
+ final float cy = mDrawable.getIntrinsicHeight() * 0.5f;
+ final float s = pointSize / MAX_POINT_SIZE;
+ canvas.scale(s, s, px, py);
+ canvas.translate(px - cx, py - cy);
+ mDrawable.setAlpha(alpha);
+ mDrawable.draw(canvas);
+ canvas.restore();
+ } else {
+ mPaint.setAlpha(alpha);
+ canvas.drawCircle(px, py, pointSize, mPaint);
+ }
+ }
+ canvas.restore();
+ }
+
+}
267 library/src/com/fima/glowpadview/TargetDrawable.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.fima.glowpadview;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+import android.util.Log;
+
+public class TargetDrawable {
+ private static final String TAG = "TargetDrawable";
+ private static final boolean DEBUG = false;
+
+ public static final int[] STATE_ACTIVE =
+ { android.R.attr.state_enabled, android.R.attr.state_active };
+ public static final int[] STATE_INACTIVE =
+ { android.R.attr.state_enabled, -android.R.attr.state_active };
+ public static final int[] STATE_FOCUSED =
+ { android.R.attr.state_enabled, -android.R.attr.state_active,
+ android.R.attr.state_focused };
+
+ private float mTranslationX = 0.0f;
+ private float mTranslationY = 0.0f;
+ private float mPositionX = 0.0f;
+ private float mPositionY = 0.0f;
+ private float mScaleX = 1.0f;
+ private float mScaleY = 1.0f;
+ private float mAlpha = 1.0f;
+ private Drawable mDrawable;
+ private boolean mEnabled = true;
+ private final int mResourceId;
+ private int mNumDrawables = 1;
+
+ /* package */ static class DrawableWithAlpha extends Drawable {
+ private float mAlpha = 1.0f;
+ private Drawable mRealDrawable;
+ public DrawableWithAlpha(Drawable realDrawable) {
+ mRealDrawable = realDrawable;
+ }
+ public void setAlpha(float alpha) {
+ mAlpha = alpha;
+ }