summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Blitzstein <sblitz@google.com>2013-02-15 16:46:06 -0800
committerSam Blitzstein <sblitz@google.com>2013-03-20 10:21:01 -0700
commit6e896f805cac499b777c98755149f07ccd7ba5c3 (patch)
tree1a5467332ae3039da901983fdf46725211417229
parentebd2a4069dda00781262a1cbfd4a9d22fce15ed7 (diff)
downloaddatetimepicker-6e896f805cac499b777c98755149f07ccd7ba5c3.tar.gz
Adding new timepicker library.
Timepicker is a radial, animated selector. Change-Id: Ib6a6deebf7673dcb14561261314a0e082d4a3ffc
-rw-r--r--.classpath8
-rw-r--r--.project33
-rw-r--r--Android.mk32
-rw-r--r--AndroidManifest.xml22
l---------libs/android-support-v4.jar1
-rw-r--r--proguard.flags7
-rw-r--r--project.properties15
-rw-r--r--res/layout-land/time_picker_dialog.xml112
-rw-r--r--res/layout-sw600dp/time_picker_dialog.xml105
-rw-r--r--res/layout/time_picker_dialog.xml105
-rw-r--r--res/values-land/dimens.xml28
-rw-r--r--res/values-sw600dp-land/dimens.xml31
-rw-r--r--res/values-sw600dp/dimens.xml31
-rw-r--r--res/values-v11/styles.xml11
-rw-r--r--res/values-v14/styles.xml11
-rw-r--r--res/values-v16/styles.xml22
-rw-r--r--res/values-v17/styles.xml22
-rw-r--r--res/values/arrays.xml62
-rw-r--r--res/values/colors.xml24
-rw-r--r--res/values/dimens.xml43
-rw-r--r--res/values/ids.xml19
-rw-r--r--res/values/strings.xml26
-rw-r--r--res/values/styles.xml51
-rw-r--r--src/com/android/datetimepicker/AmPmCirclesView.java178
-rw-r--r--src/com/android/datetimepicker/CircleView.java111
-rw-r--r--src/com/android/datetimepicker/RadialSelectorView.java345
-rw-r--r--src/com/android/datetimepicker/RadialTextsView.java315
-rw-r--r--src/com/android/datetimepicker/TimePicker.java527
-rw-r--r--src/com/android/datetimepicker/TimePickerDialog.java301
29 files changed, 2598 insertions, 0 deletions
diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..3f9691c
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="src" path="gen"/>
+ <classpathentry kind="output" path="bin/classes"/>
+</classpath>
diff --git a/.project b/.project
new file mode 100644
index 0000000..4d3f8e9
--- /dev/null
+++ b/.project
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>datetimepicker</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>
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..71027e5
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,32 @@
+# Copyright (C) 2013 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := android-opt-datetimepicker
+
+LOCAL_SDK_VERSION := 17
+
+LOCAL_SRC_FILES := \
+ $(call all-java-files-under, src) \
+ $(call all-logtags-files-under, src)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
+
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..1b4672e
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<!-- Copyright (C) 2013 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.datetimepicker"
+ android:versionCode="1" >
+ <uses-sdk android:maxSdkVersion="17" android:minSdkVersion="14"/>
+
+ <uses-permission android:name="android.permission.VIBRATE" />
+</manifest>
diff --git a/libs/android-support-v4.jar b/libs/android-support-v4.jar
new file mode 120000
index 0000000..d36be8f
--- /dev/null
+++ b/libs/android-support-v4.jar
@@ -0,0 +1 @@
+../../../../out/target/common/obj/JAVA_LIBRARIES/android-support-v4_intermediates/javalib.jar \ No newline at end of file
diff --git a/proguard.flags b/proguard.flags
new file mode 100644
index 0000000..807160f
--- /dev/null
+++ b/proguard.flags
@@ -0,0 +1,7 @@
+-keepclassmembers class com.android.datetimepicker.RadialSelectorView {
+ *** setAnimationRadiusMultiplier(...);
+}
+
+-keepclassmembers class com.android.datetimepicker.RadialTextsView {
+ *** setAnimationRadiusMultiplier(...);
+}
diff --git a/project.properties b/project.properties
new file mode 100644
index 0000000..484dab0
--- /dev/null
+++ b/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
diff --git a/res/layout-land/time_picker_dialog.xml b/res/layout-land/time_picker_dialog.xml
new file mode 100644
index 0000000..7409c6e
--- /dev/null
+++ b/res/layout-land/time_picker_dialog.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2013 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
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:background="@color/gray" >
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <FrameLayout
+ android:layout_width="@dimen/left_side_width"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:background="@color/white" >
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center" >
+ <View
+ android:id="@+id/empty_view"
+ android:layout_width="1dp"
+ android:layout_height="1dp"
+ android:background="#00000000"
+ android:layout_centerInParent="true" />
+ <TextView
+ android:id="@+id/hours"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/time_placeholder"
+ android:textColor="@color/blue"
+ android:layout_toLeftOf="@+id/separator"
+ android:layout_centerVertical="true"
+ style="@style/time_label" />
+ <TextView
+ android:id="@+id/separator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/time_separator"
+ android:paddingLeft="@dimen/separator_padding"
+ android:paddingRight="@dimen/separator_padding"
+ android:layout_alignRight="@+id/empty_view"
+ android:layout_centerVertical="true"
+ style="@style/time_label" />
+ <TextView
+ android:id="@+id/minutes"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/time_placeholder"
+ android:layout_toRightOf="@+id/separator"
+ android:layout_centerVertical="true"
+ style="@style/time_label" />
+ <View
+ android:id="@+id/ampm_hitspace"
+ android:layout_width="@dimen/ampm_label_size"
+ android:layout_height="wrap_content"
+ android:layout_alignTop="@+id/minutes"
+ android:layout_alignBottom="@+id/minutes"
+ android:layout_alignLeft="@+id/ampm_label"
+ android:layout_alignRight="@+id/ampm_label" />
+ <TextView
+ android:id="@+id/ampm_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/time_placeholder"
+ android:paddingLeft="@dimen/ampm_left_padding"
+ android:paddingRight="@dimen/ampm_left_padding"
+ android:layout_toRightOf="@+id/minutes"
+ android:layout_alignBaseline="@+id/minutes"
+ style="@style/ampm_label" />
+ </RelativeLayout>
+ </FrameLayout>
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:background="@color/lighter_gray" />
+ <LinearLayout
+ style="?android:attr/buttonBarStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:background="@color/white" >
+ <Button
+ android:id="@+id/done_button"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/done_label" />
+ </LinearLayout>
+ </LinearLayout>
+ <com.android.datetimepicker.TimePicker
+ android:id="@+id/time_picker"
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center" />
+</LinearLayout>
diff --git a/res/layout-sw600dp/time_picker_dialog.xml b/res/layout-sw600dp/time_picker_dialog.xml
new file mode 100644
index 0000000..ae01ad0
--- /dev/null
+++ b/res/layout-sw600dp/time_picker_dialog.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2013 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
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="@color/gray" >
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/header_height"
+ android:background="@color/white" >
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center" >
+ <View
+ android:id="@+id/empty_view"
+ android:layout_width="1dp"
+ android:layout_height="1dp"
+ android:background="#00000000"
+ android:layout_centerInParent="true" />
+ <TextView
+ android:id="@+id/hours"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/time_placeholder"
+ android:textColor="@color/blue"
+ android:layout_toLeftOf="@+id/separator"
+ android:layout_centerVertical="true"
+ style="@style/time_label" />
+ <TextView
+ android:id="@+id/separator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/time_separator"
+ android:paddingLeft="@dimen/separator_padding"
+ android:paddingRight="@dimen/separator_padding"
+ android:layout_alignRight="@+id/empty_view"
+ android:layout_centerVertical="true"
+ style="@style/time_label" />
+ <TextView
+ android:id="@+id/minutes"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/time_placeholder"
+ android:layout_toRightOf="@+id/separator"
+ android:layout_centerVertical="true"
+ style="@style/time_label" />
+ <View
+ android:id="@+id/ampm_hitspace"
+ android:layout_width="@dimen/ampm_label_size"
+ android:layout_height="wrap_content"
+ android:layout_alignTop="@+id/minutes"
+ android:layout_alignBottom="@+id/minutes"
+ android:layout_alignLeft="@+id/ampm_label"
+ android:layout_alignRight="@+id/ampm_label" />
+ <TextView
+ android:id="@+id/ampm_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/time_placeholder"
+ android:paddingLeft="@dimen/ampm_left_padding"
+ android:paddingRight="@dimen/ampm_left_padding"
+ android:layout_toRightOf="@+id/minutes"
+ android:layout_alignBaseline="@+id/minutes"
+ style="@style/ampm_label" />
+ </RelativeLayout>
+ </FrameLayout>
+ <com.android.datetimepicker.TimePicker
+ android:id="@+id/time_picker"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:gravity="center" />
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:background="@color/lighter_gray" />
+ <LinearLayout
+ style="?android:attr/buttonBarStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+ <Button
+ android:id="@+id/done_button"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/done_label" />
+ </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/time_picker_dialog.xml b/res/layout/time_picker_dialog.xml
new file mode 100644
index 0000000..ae01ad0
--- /dev/null
+++ b/res/layout/time_picker_dialog.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2013 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
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="@color/gray" >
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/header_height"
+ android:background="@color/white" >
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center" >
+ <View
+ android:id="@+id/empty_view"
+ android:layout_width="1dp"
+ android:layout_height="1dp"
+ android:background="#00000000"
+ android:layout_centerInParent="true" />
+ <TextView
+ android:id="@+id/hours"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/time_placeholder"
+ android:textColor="@color/blue"
+ android:layout_toLeftOf="@+id/separator"
+ android:layout_centerVertical="true"
+ style="@style/time_label" />
+ <TextView
+ android:id="@+id/separator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/time_separator"
+ android:paddingLeft="@dimen/separator_padding"
+ android:paddingRight="@dimen/separator_padding"
+ android:layout_alignRight="@+id/empty_view"
+ android:layout_centerVertical="true"
+ style="@style/time_label" />
+ <TextView
+ android:id="@+id/minutes"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/time_placeholder"
+ android:layout_toRightOf="@+id/separator"
+ android:layout_centerVertical="true"
+ style="@style/time_label" />
+ <View
+ android:id="@+id/ampm_hitspace"
+ android:layout_width="@dimen/ampm_label_size"
+ android:layout_height="wrap_content"
+ android:layout_alignTop="@+id/minutes"
+ android:layout_alignBottom="@+id/minutes"
+ android:layout_alignLeft="@+id/ampm_label"
+ android:layout_alignRight="@+id/ampm_label" />
+ <TextView
+ android:id="@+id/ampm_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/time_placeholder"
+ android:paddingLeft="@dimen/ampm_left_padding"
+ android:paddingRight="@dimen/ampm_left_padding"
+ android:layout_toRightOf="@+id/minutes"
+ android:layout_alignBaseline="@+id/minutes"
+ style="@style/ampm_label" />
+ </RelativeLayout>
+ </FrameLayout>
+ <com.android.datetimepicker.TimePicker
+ android:id="@+id/time_picker"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:gravity="center" />
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:background="@color/lighter_gray" />
+ <LinearLayout
+ style="?android:attr/buttonBarStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+ <Button
+ android:id="@+id/done_button"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/done_label" />
+ </LinearLayout>
+</LinearLayout>
diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml
new file mode 100644
index 0000000..dca1757
--- /dev/null
+++ b/res/values-land/dimens.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2013, 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.
+*/
+-->
+
+<resources
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"
+ xmlns:android="http://schemas.android.com/apk/res/android" >
+ <dimen name="time_label_right_padding">10sp</dimen>
+ <dimen name="time_label_size">40sp</dimen>
+ <dimen name="ampm_label_size">14sp</dimen>
+ <dimen name="header_height">64dip</dimen>
+ <dimen name="footer_height">48dip</dimen>
+</resources> \ No newline at end of file
diff --git a/res/values-sw600dp-land/dimens.xml b/res/values-sw600dp-land/dimens.xml
new file mode 100644
index 0000000..70c7b73
--- /dev/null
+++ b/res/values-sw600dp-land/dimens.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2013, 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.
+*/
+-->
+
+<resources
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"
+ xmlns:android="http://schemas.android.com/apk/res/android" >
+ <dimen name="time_label_right_padding">16sp</dimen>
+ <dimen name="time_label_size">64sp</dimen>
+ <dimen name="ampm_label_size">21sp</dimen>
+ <dimen name="done_label_size">14sp</dimen>
+ <dimen name="ampm_left_padding">7dip</dimen>
+ <dimen name="separator_padding">5dip</dimen>
+ <dimen name="header_height">96dip</dimen>
+ <dimen name="footer_height">48dip</dimen>
+</resources> \ No newline at end of file
diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml
new file mode 100644
index 0000000..7efc81d
--- /dev/null
+++ b/res/values-sw600dp/dimens.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2013, 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.
+*/
+-->
+
+<resources
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"
+ xmlns:android="http://schemas.android.com/apk/res/android" >
+ <dimen name="time_label_right_padding">34sp</dimen>
+ <dimen name="time_label_size">96sp</dimen>
+ <dimen name="ampm_label_size">32sp</dimen>
+ <dimen name="done_label_size">21sp</dimen>
+ <dimen name="ampm_left_padding">16dip</dimen>
+ <dimen name="separator_padding">8dip</dimen>
+ <dimen name="header_height">144dip</dimen>
+ <dimen name="footer_height">72dip</dimen>
+</resources> \ No newline at end of file
diff --git a/res/values-v11/styles.xml b/res/values-v11/styles.xml
new file mode 100644
index 0000000..541752f
--- /dev/null
+++ b/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.Light">
+ <!-- API 11 theme customizations can go here. -->
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/res/values-v14/styles.xml b/res/values-v14/styles.xml
new file mode 100644
index 0000000..1ebe523
--- /dev/null
+++ b/res/values-v14/styles.xml
@@ -0,0 +1,11 @@
+<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.Light.DarkActionBar">
+ <!-- API 14 theme customizations can go here. -->
+ </style>
+</resources> \ No newline at end of file
diff --git a/res/values-v16/styles.xml b/res/values-v16/styles.xml
new file mode 100644
index 0000000..beada7b
--- /dev/null
+++ b/res/values-v16/styles.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Style for dialog labels. -->
+ <style name="label">
+ <item name="android:fontFamily">sans-serif-light</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/res/values-v17/styles.xml b/res/values-v17/styles.xml
new file mode 100644
index 0000000..4aadec9
--- /dev/null
+++ b/res/values-v17/styles.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Style for dialog labels. -->
+ <style name="label">
+ <item name="android:fontFamily">sans-serif-thin</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
new file mode 100644
index 0000000..b18b2a5
--- /dev/null
+++ b/res/values/arrays.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="hours">
+ <item>12</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ <item>4</item>
+ <item>5</item>
+ <item>6</item>
+ <item>7</item>
+ <item>8</item>
+ <item>9</item>
+ <item>10</item>
+ <item>11</item>
+ </string-array>
+
+ <string-array name="hours_24">
+ <item>00</item>
+ <item>13</item>
+ <item>14</item>
+ <item>15</item>
+ <item>16</item>
+ <item>17</item>
+ <item>18</item>
+ <item>19</item>
+ <item>20</item>
+ <item>21</item>
+ <item>22</item>
+ <item>23</item>
+ </string-array>
+
+ <string-array name="minutes">
+ <item>00</item>
+ <item>05</item>
+ <item>10</item>
+ <item>15</item>
+ <item>20</item>
+ <item>25</item>
+ <item>30</item>
+ <item>35</item>
+ <item>40</item>
+ <item>45</item>
+ <item>50</item>
+ <item>55</item>
+ </string-array>
+</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
new file mode 100644
index 0000000..03c35a7
--- /dev/null
+++ b/res/values/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<resources>
+ <color name="white">#ffffff</color>
+ <color name="gray">#08000000</color>
+ <color name="lighter_gray">#15000000</color>
+ <color name="dark_gray">#70000000</color>
+ <color name="black">#000000</color>
+ <color name="blue">#33b5e5</color>
+</resources> \ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
new file mode 100644
index 0000000..cd5f2f9
--- /dev/null
+++ b/res/values/dimens.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2013, 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.
+*/
+-->
+
+<resources
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"
+ xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item name="circle_radius_multiplier" format="float" type="string">0.84</item>
+ <item name="circle_radius_multiplier_24HourMode" format="float" type="string">0.87</item>
+ <item name="selection_radius_multiplier" format="float" type="string">0.16</item>
+ <item name="ampm_circle_radius_multiplier" format="float" type="string">0.175</item>
+ <item name="numbers_radius_multiplier_normal" format="float" type="string">0.8</item>
+ <item name="numbers_radius_multiplier_inner" format="float" type="string">0.60</item>
+ <item name="numbers_radius_multiplier_outer" format="float" type="string">0.83</item>
+ <item name="text_size_multiplier_normal" format="float" type="string">0.2</item>
+ <item name="text_size_multiplier_inner" format="float" type="string">0.15</item>
+ <item name="text_size_multiplier_outer" format="float" type="string">0.11</item>
+ <dimen name="time_label_right_padding">12sp</dimen>
+ <dimen name="time_label_size">48sp</dimen>
+ <dimen name="ampm_label_size">16sp</dimen>
+ <dimen name="done_label_size">14sp</dimen>
+ <dimen name="ampm_left_padding">7dip</dimen>
+ <dimen name="separator_padding">4dip</dimen>
+ <dimen name="header_height">96dip</dimen>
+ <dimen name="footer_height">48dip</dimen>
+ <dimen name="left_side_width">250dip</dimen>
+ <dimen name="left_side_height">450dip</dimen>
+</resources> \ No newline at end of file
diff --git a/res/values/ids.xml b/res/values/ids.xml
new file mode 100644
index 0000000..f4374b8
--- /dev/null
+++ b/res/values/ids.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<resources>
+ <item type="id" name="numbers_key" />
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..9533ea7
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name">TimePicker</string>
+
+ <string name="trailing_hour_digit">1</string>
+ <string name="time_placeholder">--</string>
+ <string name="time_separator">:</string>
+ <string name="done_label">Done</string>
+ <string name="am_label">AM</string>
+ <string name="pm_label">PM</string>
+</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
new file mode 100644
index 0000000..767d8ee
--- /dev/null
+++ b/res/values/styles.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <!--
+ 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>
+
+ <!-- Styles for dialog labels. -->
+ <style name="label" />
+
+ <style name="time_label" parent="label">
+ <item name="android:textSize">@dimen/time_label_size</item>
+ </style>
+
+ <style name="ampm_label" parent="label">
+ <item name="android:textSize">@dimen/ampm_label_size</item>
+ <item name="android:textAllCaps">true</item>
+ <item name="android:textColor">@color/black</item>
+ </style>
+
+ <style name="done_label" parent="label">
+ <item name="android:textSize">@dimen/done_label_size</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/src/com/android/datetimepicker/AmPmCirclesView.java b/src/com/android/datetimepicker/AmPmCirclesView.java
new file mode 100644
index 0000000..cb04cad
--- /dev/null
+++ b/src/com/android/datetimepicker/AmPmCirclesView.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2013 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.android.datetimepicker;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.Paint.Align;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+import com.android.datetimepicker.R;
+
+public class AmPmCirclesView extends View {
+ private static final String TAG = "AmPmCirclesView";
+
+ private final Paint mPaint = new Paint();
+ private int mWhite;
+ private int mDarkGray;
+ private int mBlue;
+ private float mCircleRadiusMultiplier;
+ private float mAmPmCircleRadiusMultiplier;
+ private String mAmText;
+ private String mPmText;
+ private boolean mIsInitialized;
+
+ private static final int AM = TimePickerDialog.AM;
+ private static final int PM = TimePickerDialog.PM;
+
+ private boolean mDrawValuesReady;
+ private int mAmPmCircleRadius;
+ private int mAmXCenter;
+ private int mPmXCenter;
+ private int mAmPmYCenter;
+ private int mAmOrPm;
+ private int mAmOrPmPressed;
+
+ public AmPmCirclesView(Context context) {
+ super(context);
+ mIsInitialized = false;
+ }
+
+ public void initialize(Context context, int amOrPm) {
+ if (mIsInitialized) {
+ Log.e(TAG, "AmPmCirclesView may only be initialized once.");
+ return;
+ }
+
+ Resources res = context.getResources();
+ mWhite = res.getColor(R.color.white);
+ mDarkGray = res.getColor(R.color.dark_gray);
+ mBlue = res.getColor(R.color.blue);
+ Typeface tf = Typeface.create("sans-serif-thin", Typeface.NORMAL);
+ mPaint.setTypeface(tf);
+ mPaint.setAntiAlias(true);
+ mPaint.setTextAlign(Align.CENTER);
+
+ mCircleRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.circle_radius_multiplier));
+ mAmPmCircleRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier));
+ mAmText = res.getString(R.string.am_label);
+ mPmText = res.getString(R.string.pm_label);
+
+ setAmOrPm(amOrPm);
+ mAmOrPmPressed = -1;
+
+ mIsInitialized = true;
+ }
+
+ public void setAmOrPm(int amOrPm) {
+ mAmOrPm = amOrPm;
+ }
+
+ public void setAmOrPmPressed(int amOrPmPressed) {
+ mAmOrPmPressed = amOrPmPressed;
+ }
+
+ public int getIsTouchingAmOrPm(float xCoord, float yCoord) {
+ if (!mDrawValuesReady) {
+ return -1;
+ }
+
+ int squaredYDistance = (int) ((yCoord - mAmPmYCenter)*(yCoord - mAmPmYCenter));
+
+ int distanceToAmCenter =
+ (int) Math.sqrt((xCoord - mAmXCenter)*(xCoord - mAmXCenter) + squaredYDistance);
+ if (distanceToAmCenter <= mAmPmCircleRadius) {
+ return AM;
+ }
+
+ int distanceToPmCenter =
+ (int) Math.sqrt((xCoord - mPmXCenter)*(xCoord - mPmXCenter) + squaredYDistance);
+ if (distanceToPmCenter <= mAmPmCircleRadius) {
+ return PM;
+ }
+
+ // Neither was close enough.
+ return -1;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ int viewWidth = getWidth();
+ if (viewWidth == 0 || !mIsInitialized) {
+ return;
+ }
+
+ if (!mDrawValuesReady) {
+ int layoutXCenter = getWidth() / 2;
+ int layoutYCenter = getHeight() / 2;
+ int circleRadius =
+ (int) (Math.min(layoutXCenter, layoutYCenter) * mCircleRadiusMultiplier);
+ mAmPmCircleRadius = (int) (circleRadius * mAmPmCircleRadiusMultiplier);
+ int textSize = mAmPmCircleRadius * 2 / 3;
+ mPaint.setTextSize(textSize);
+
+ // Line up the vertical center of the AM/PM circles with the bottom of the main circle.
+ mAmPmYCenter = layoutYCenter - mAmPmCircleRadius / 2 + circleRadius;
+ // Line up the horizontal edges of the AM/PM circles with the horizontal edges
+ // of the main circle.
+ mAmXCenter = layoutXCenter - circleRadius + mAmPmCircleRadius;
+ mPmXCenter = layoutXCenter + circleRadius - mAmPmCircleRadius;
+
+ mDrawValuesReady = true;
+ }
+
+ int amColor = mWhite;
+ int amAlpha = 255;
+ int pmColor = mWhite;
+ int pmAlpha = 255;
+ if (mAmOrPm == AM) {
+ amColor = mBlue;
+ amAlpha = 45;
+ } else if (mAmOrPm == PM) {
+ pmColor = mBlue;
+ pmAlpha = 45;
+ }
+ if (mAmOrPmPressed == AM) {
+ amColor = mBlue;
+ amAlpha = 175;
+ } else if (mAmOrPmPressed == PM) {
+ pmColor = mBlue;
+ pmAlpha = 175;
+ }
+
+ mPaint.setColor(amColor);
+ mPaint.setAlpha(amAlpha);
+ canvas.drawCircle(mAmXCenter, mAmPmYCenter, mAmPmCircleRadius, mPaint);
+ mPaint.setColor(pmColor);
+ mPaint.setAlpha(pmAlpha);
+ canvas.drawCircle(mPmXCenter, mAmPmYCenter, mAmPmCircleRadius, mPaint);
+
+ mPaint.setColor(mDarkGray);
+ int textYCenter = mAmPmYCenter - (int) (mPaint.descent() + mPaint.ascent()) / 2;
+ canvas.drawText(mAmText, mAmXCenter, textYCenter, mPaint);
+ canvas.drawText(mPmText, mPmXCenter, textYCenter, mPaint);
+ }
+}
diff --git a/src/com/android/datetimepicker/CircleView.java b/src/com/android/datetimepicker/CircleView.java
new file mode 100644
index 0000000..cd89410
--- /dev/null
+++ b/src/com/android/datetimepicker/CircleView.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2013 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.android.datetimepicker;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+import com.android.datetimepicker.R;
+
+public class CircleView extends View {
+ private static final String TAG = "CircleView";
+
+ private final Paint mPaint = new Paint();
+ private boolean mIs24HourMode;
+ private int mWhite;
+ private int mBlack;
+ private float mCircleRadiusMultiplier;
+ private float mAmPmCircleRadiusMultiplier;
+ private boolean mIsInitialized;
+
+ private boolean mDrawValuesReady;
+ private int mXCenter;
+ private int mYCenter;
+ private int mCircleRadius;
+
+ public CircleView(Context context) {
+ super(context);
+
+ Resources res = context.getResources();
+ mWhite = res.getColor(R.color.white);
+ mBlack = res.getColor(R.color.black);
+ mPaint.setAntiAlias(true);
+
+ mIsInitialized = false;
+ }
+
+ public void initialize(Context context, boolean is24HourMode) {
+ if (mIsInitialized) {
+ Log.e(TAG, "CircleView may only be initialized once.");
+ return;
+ }
+
+ Resources res = context.getResources();
+ mIs24HourMode = is24HourMode;
+ if (is24HourMode) {
+ mCircleRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.circle_radius_multiplier_24HourMode));
+ } else {
+ mCircleRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.circle_radius_multiplier));
+ mAmPmCircleRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier));
+ }
+
+ mIsInitialized = true;
+ }
+
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ int viewWidth = getWidth();
+ if (viewWidth == 0 || !mIsInitialized) {
+ return;
+ }
+
+ if (!mDrawValuesReady) {
+ mXCenter = getWidth() / 2;
+ mYCenter = getHeight() / 2;
+ mCircleRadius = (int) (Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier);
+
+ if (!mIs24HourMode) {
+ // We'll need to draw the AM/PM circles, so the main circle will need to have
+ // a slightly higher center. To keep the entire view centered vertically, we'll
+ // have to push it up by half the radius of the AM/PM circles.
+ int amPmCircleRadius = (int) (mCircleRadius * mAmPmCircleRadiusMultiplier);
+ mYCenter -= amPmCircleRadius / 2;
+ }
+
+ mDrawValuesReady = true;
+ }
+
+ mPaint.setColor(mWhite);
+ canvas.drawCircle(mXCenter, mYCenter, mCircleRadius, mPaint);
+
+ mPaint.setColor(mBlack);
+ canvas.drawCircle(mXCenter, mYCenter, 2, mPaint);
+ }
+}
diff --git a/src/com/android/datetimepicker/RadialSelectorView.java b/src/com/android/datetimepicker/RadialSelectorView.java
new file mode 100644
index 0000000..5f56b56
--- /dev/null
+++ b/src/com/android/datetimepicker/RadialSelectorView.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2013 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.android.datetimepicker;
+
+import android.animation.Keyframe;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.app.Service;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.os.SystemClock;
+import android.os.Vibrator;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+import com.android.datetimepicker.R;
+
+public class RadialSelectorView extends View {
+ private static final String TAG = "RadialSelectorView";
+
+ private final Paint mPaint = new Paint();
+
+ private boolean mIsInitialized;
+ private boolean mDrawValuesReady;
+
+ private float mCircleRadiusMultiplier;
+ private float mAmPmCircleRadiusMultiplier;
+ private float mInnerNumbersRadiusMultiplier;
+ private float mOuterNumbersRadiusMultiplier;
+ private float mNumbersRadiusMultiplier;
+ private float mSelectionRadiusMultiplier;
+ private float mAnimationRadiusMultiplier;
+ private boolean mIs24HourMode;
+ private boolean mHasInnerCircle;
+
+ private int mXCenter;
+ private int mYCenter;
+ private int mCircleRadius;
+ private float mTransitionMidRadiusMultiplier;
+ private float mTransitionEndRadiusMultiplier;
+ private int mLineLength;
+ private int mSelectionRadius;
+ private InvalidateUpdateListener mInvalidateUpdateListener;
+
+ private int mSelectionDegrees;
+ private double mSelectionRadians;
+ private boolean mDrawLine;
+ private boolean mForceDrawDot;
+
+ public RadialSelectorView(Context context) {
+ super(context);
+ mIsInitialized = false;
+ }
+
+ public void initialize(Context context, int selectionDegrees, boolean is24HourMode,
+ boolean hasInnerCircle, boolean isInnerCircle, boolean disappearsOut) {
+ if (mIsInitialized) {
+ Log.e(TAG, "This RadialSelectorView may only be initialized once.");
+ return;
+ }
+
+ Resources res = context.getResources();
+
+ int blue = res.getColor(R.color.blue);
+ mPaint.setColor(blue);
+ mPaint.setAntiAlias(true);
+
+ mIs24HourMode = is24HourMode;
+ if (is24HourMode) {
+ mCircleRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.circle_radius_multiplier_24HourMode));
+ } else {
+ mCircleRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.circle_radius_multiplier));
+ mAmPmCircleRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier));
+ }
+
+ mHasInnerCircle = hasInnerCircle;
+ if (hasInnerCircle) {
+ mInnerNumbersRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.numbers_radius_multiplier_inner));
+ mOuterNumbersRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.numbers_radius_multiplier_outer));
+ } else {
+ mNumbersRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.numbers_radius_multiplier_normal));
+ }
+ mSelectionRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.selection_radius_multiplier));
+
+ setSelection(selectionDegrees, isInnerCircle, false, false);
+
+ mAnimationRadiusMultiplier = 1;
+ mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut? -1 : 1));
+ mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut? 1 : -1));
+ mInvalidateUpdateListener = new InvalidateUpdateListener();
+
+ mIsInitialized = true;
+ }
+
+ public void setSelection(int selectionDegrees, boolean isInnerCircle,
+ boolean drawLine, boolean forceDrawDot) {
+ mSelectionDegrees = selectionDegrees;
+ mSelectionRadians = selectionDegrees * Math.PI / 180;
+ mDrawLine = drawLine;
+ mForceDrawDot = forceDrawDot;
+
+ if (mHasInnerCircle) {
+ if (isInnerCircle) {
+ mNumbersRadiusMultiplier = mInnerNumbersRadiusMultiplier;
+ } else {
+ mNumbersRadiusMultiplier = mOuterNumbersRadiusMultiplier;
+ }
+ }
+ }
+
+ public void setDrawLine(boolean drawLine) {
+ mDrawLine = drawLine;
+ }
+
+ public void setAnimationRadiusMultiplier(float animationRadiusMultiplier) {
+ mAnimationRadiusMultiplier = animationRadiusMultiplier;
+ }
+
+ public int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal,
+ final Boolean[] isInnerCircle) {
+ if (!mDrawValuesReady) {
+ return -1;
+ }
+
+ double hypotenuse = Math.sqrt(
+ (pointY - mYCenter)*(pointY - mYCenter) +
+ (pointX - mXCenter)*(pointX - mXCenter));
+ // Check if we're outside the range
+ if (mHasInnerCircle) {
+ if (forceLegal) {
+ // If we're told to force the coordinates to be legal, we'll set the isInnerCircle
+ // boolean based based off whichever number the coordinates are closer to.
+ int innerNumberRadius = (int) (mCircleRadius * mInnerNumbersRadiusMultiplier);
+ int distanceToInnerNumber = (int) Math.abs(hypotenuse - innerNumberRadius);
+ int outerNumberRadius = (int) (mCircleRadius * mOuterNumbersRadiusMultiplier);
+ int distanceToOuterNumber = (int) Math.abs(hypotenuse - outerNumberRadius);
+
+ isInnerCircle[0] = (distanceToInnerNumber <= distanceToOuterNumber);
+ } else {
+ // Otherwise, if we're close enough to either number (with the space between the
+ // two allotted equally), set the isInnerCircle boolean as the closer one.
+ // appropriately, but otherwise return -1.
+ int minAllowedHypotenuseForInnerNumber =
+ (int) (mCircleRadius * mInnerNumbersRadiusMultiplier) - mSelectionRadius;
+ int maxAllowedHypotenuseForOuterNumber =
+ (int) (mCircleRadius * mOuterNumbersRadiusMultiplier) + mSelectionRadius;
+ int halfwayHypotenusePoint = (int) (mCircleRadius *
+ ((mOuterNumbersRadiusMultiplier + mInnerNumbersRadiusMultiplier) / 2));
+
+ if (hypotenuse >= minAllowedHypotenuseForInnerNumber &&
+ hypotenuse <= halfwayHypotenusePoint) {
+ isInnerCircle[0] = true;
+ } else if (hypotenuse <= maxAllowedHypotenuseForOuterNumber &&
+ hypotenuse >= halfwayHypotenusePoint) {
+ isInnerCircle[0] = false;
+ } else {
+ return -1;
+ }
+ }
+ } else {
+ // If there's just one circle, we'll need to return -1 if:
+ // we're not told to force the coordinates to be legal, and
+ // the coordinates' distance to the number is within the allowed distance.
+ if (!forceLegal) {
+ int distanceToNumber = (int) Math.abs(hypotenuse - mLineLength);
+ // The max allowed distance will be defined as the distance from the center of the
+ // number to the edge of the circle.
+ int maxAllowedDistance = (int) (mCircleRadius * (1 - mNumbersRadiusMultiplier));
+ if (distanceToNumber > maxAllowedDistance) {
+ return -1;
+ }
+ }
+ }
+
+
+ float opposite = Math.abs(pointY - mYCenter);
+ double radians = Math.asin(opposite / hypotenuse);
+ int degrees = (int) (radians * 180 / Math.PI);
+
+ // Now we have to translate to the correct quadrant.
+ boolean rightSide = (pointX > mXCenter);
+ boolean topSide = (pointY < mYCenter);
+ if (rightSide && topSide) {
+ degrees = 90 - degrees;
+ } else if (rightSide && !topSide) {
+ degrees = 90 + degrees;
+ } else if (!rightSide && !topSide) {
+ degrees = 270 - degrees;
+ } else if (!rightSide && topSide) {
+ degrees = 270 + degrees;
+ }
+ return degrees;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ int viewWidth = getWidth();
+ if (viewWidth == 0 || !mIsInitialized) {
+ return;
+ }
+
+ if (!mDrawValuesReady) {
+ mXCenter = getWidth() / 2;
+ mYCenter = getHeight() / 2;
+ mCircleRadius = (int) (Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier);
+
+ if (!mIs24HourMode) {
+ // We'll need to draw the AM/PM circles, so the main circle will need to have
+ // a slightly higher center. To keep the entire view centered vertically, we'll
+ // have to push it up by half the radius of the AM/PM circles.
+ int amPmCircleRadius = (int) (mCircleRadius * mAmPmCircleRadiusMultiplier);
+ mYCenter -= amPmCircleRadius / 2;
+ }
+
+ mSelectionRadius = (int) (mCircleRadius * mSelectionRadiusMultiplier);
+
+ mDrawValuesReady = true;
+ }
+
+ mLineLength = (int) (mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier);
+ int pointX = mXCenter + (int) (mLineLength * Math.sin(mSelectionRadians));
+ int pointY = mYCenter - (int) (mLineLength * Math.cos(mSelectionRadians));
+
+ mPaint.setAlpha(75);
+ canvas.drawCircle(pointX, pointY, mSelectionRadius, mPaint);
+
+ if (mForceDrawDot | mSelectionDegrees % 30 != 0) {
+ // We're not on a direct tick.
+ mPaint.setAlpha(255);
+ canvas.drawCircle(pointX, pointY, mSelectionRadius / 4, mPaint);
+ } else {
+ int lineLength = mLineLength;
+ lineLength -= mSelectionRadius;
+ pointX = mXCenter + (int) (lineLength * Math.sin(mSelectionRadians));
+ pointY = mYCenter - (int) (lineLength * Math.cos(mSelectionRadians));
+ }
+
+ if (mDrawLine || true) {
+ mPaint.setAlpha(255);
+ mPaint.setStrokeWidth(1);
+ canvas.drawLine(mXCenter, mYCenter, pointX, pointY, mPaint);
+ }
+ }
+
+ public ObjectAnimator getDisappearAnimator() {
+ if (!mIsInitialized || !mDrawValuesReady) {
+ Log.e(TAG, "RadialSelectorView was not ready for animation.");
+ return null;
+ }
+
+ Keyframe kf0, kf1, kf2;
+ float midwayPoint = 0.2f;
+ int duration = 500;
+
+ kf0 = Keyframe.ofFloat(0f, 1);
+ kf1 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
+ kf2 = Keyframe.ofFloat(1f, mTransitionEndRadiusMultiplier);
+ PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe(
+ "animationRadiusMultiplier", kf0, kf1, kf2);
+
+ kf0 = Keyframe.ofFloat(0f, 1f);
+ kf1 = Keyframe.ofFloat(1f, 0f);
+ PropertyValuesHolder fadeOut = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1);
+
+ ObjectAnimator disappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
+ this, radiusDisappear, fadeOut).setDuration(duration);
+ disappearAnimator.addUpdateListener(mInvalidateUpdateListener);
+
+ return disappearAnimator;
+ }
+
+ public ObjectAnimator getReappearAnimator() {
+ if (!mIsInitialized || !mDrawValuesReady) {
+ Log.e(TAG, "RadialSelectorView was not ready for animation.");
+ return null;
+ }
+
+ Keyframe kf0, kf1, kf2, kf3;
+ float midwayPoint = 0.2f;
+ int duration = 500;
+
+ // The time points are half of what they would normally be, because this animation is
+ // staggered against the disappear so they happen seamlessly. The reappear starts
+ // halfway into the disappear.
+ float delayMultiplier = 0.5f;
+ float transitionDurationMultiplier = 0.75f;
+ float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier;
+ int totalDuration = (int) (duration * totalDurationMultiplier);
+ float delayPoint = (delayMultiplier * duration) / totalDuration;
+ midwayPoint = 1 - (midwayPoint * (1 - delayPoint));
+
+ kf0 = Keyframe.ofFloat(0f, mTransitionEndRadiusMultiplier);
+ kf1 = Keyframe.ofFloat(delayPoint, mTransitionEndRadiusMultiplier);
+ kf2 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
+ kf3 = Keyframe.ofFloat(1f, 1);
+ PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe(
+ "animationRadiusMultiplier", kf0, kf1, kf2, kf3);
+
+ kf0 = Keyframe.ofFloat(0f, 0f);
+ kf1 = Keyframe.ofFloat(delayPoint, 0f);
+ kf2 = Keyframe.ofFloat(1f, 1f);
+ PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2);
+
+ ObjectAnimator reappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
+ this, radiusReappear, fadeIn).setDuration(totalDuration);
+ reappearAnimator.addUpdateListener(mInvalidateUpdateListener);
+ return reappearAnimator;
+ }
+
+ private class InvalidateUpdateListener implements AnimatorUpdateListener {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ RadialSelectorView.this.invalidate();
+ }
+ }
+}
diff --git a/src/com/android/datetimepicker/RadialTextsView.java b/src/com/android/datetimepicker/RadialTextsView.java
new file mode 100644
index 0000000..6909d5c
--- /dev/null
+++ b/src/com/android/datetimepicker/RadialTextsView.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2013 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.android.datetimepicker;
+
+import android.animation.Keyframe;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.Paint.Align;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+import com.android.datetimepicker.R;
+
+public class RadialTextsView extends View {
+ private final static String TAG = "RadialTextsView";
+
+ private final Paint mPaint = new Paint();
+
+ private boolean mDrawValuesReady;
+ private boolean mIsInitialized;
+
+ private String[] mTexts;
+ private String[] mInnerTexts;
+ private boolean mIs24HourMode;
+ private boolean mHasInnerCircle;
+ private float mCircleRadiusMultiplier;
+ private float mAmPmCircleRadiusMultiplier;
+ private float mNumbersRadiusMultiplier;
+ private float mInnerNumbersRadiusMultiplier;
+ private float mTextSizeMultiplier;
+ private float mInnerTextSizeMultiplier;
+
+ private int mXCenter;
+ private int mYCenter;
+ private float mCircleRadius;
+ private boolean mTextGridValuesDirty;
+ private float mTextSize;
+ private float mInnerTextSize;
+ private float[] mTextGridHeights;
+ private float[] mTextGridWidths;
+ private float[] mInnerTextGridHeights;
+ private float[] mInnerTextGridWidths;
+
+ private float mAnimationRadiusMultiplier;
+ private float mTransitionMidRadiusMultiplier;
+ private float mTransitionEndRadiusMultiplier;
+ ObjectAnimator mDisappearAnimator;
+ ObjectAnimator mReappearAnimator;
+ private InvalidateUpdateListener mInvalidateUpdateListener;
+
+ public RadialTextsView(Context context) {
+ super(context);
+ mIsInitialized = false;
+ }
+
+ public void initialize(Resources res, String[] texts, String[] innerTexts,
+ boolean is24HourMode, boolean disappearsOut) {
+ if (mIsInitialized) {
+ Log.e(TAG, "This RadialTextsView may only be initialized once.");
+ return;
+ }
+
+ int black = res.getColor(R.color.black);
+ mPaint.setColor(black);
+ Typeface tf = Typeface.create("sans-serif-thin", Typeface.NORMAL);
+ mPaint.setTypeface(tf);
+ mPaint.setAntiAlias(true);
+ mPaint.setTextAlign(Align.CENTER);
+
+ mTexts = texts;
+ mInnerTexts = innerTexts;
+ mIs24HourMode = is24HourMode;
+ mHasInnerCircle = (innerTexts != null);
+
+ if (is24HourMode) {
+ mCircleRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.circle_radius_multiplier_24HourMode));
+ } else {
+ mCircleRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.circle_radius_multiplier));
+ mAmPmCircleRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier));
+ }
+
+ mTextGridHeights = new float[7];
+ mTextGridWidths = new float[7];
+ if (mHasInnerCircle) {
+ mNumbersRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.numbers_radius_multiplier_outer));
+ mTextSizeMultiplier = Float.parseFloat(
+ res.getString(R.string.text_size_multiplier_outer));
+ mInnerNumbersRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.numbers_radius_multiplier_inner));
+ mInnerTextSizeMultiplier = Float.parseFloat(
+ res.getString(R.string.text_size_multiplier_inner));
+
+ mInnerTextGridHeights = new float[7];
+ mInnerTextGridWidths = new float[7];
+ } else {
+ mNumbersRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.numbers_radius_multiplier_normal));
+ mTextSizeMultiplier = Float.parseFloat(
+ res.getString(R.string.text_size_multiplier_normal));
+ }
+
+ mAnimationRadiusMultiplier = 1;
+ mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut? -1 : 1));
+ mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut? 1 : -1));
+ mInvalidateUpdateListener = new InvalidateUpdateListener();
+
+ mTextGridValuesDirty = true;
+ mIsInitialized = true;
+ }
+
+ public void setAnimationRadiusMultiplier(float animationRadiusMultiplier) {
+ mAnimationRadiusMultiplier = animationRadiusMultiplier;
+ mTextGridValuesDirty = true;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ int viewWidth = getWidth();
+ if (viewWidth == 0 || !mIsInitialized) {
+ return;
+ }
+
+ if (!mDrawValuesReady) {
+ mXCenter = getWidth() / 2;
+ mYCenter = getHeight() / 2;
+ mCircleRadius = Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier;
+ if (!mIs24HourMode) {
+ // We'll need to draw the AM/PM circles, so the main circle will need to have
+ // a slightly higher center. To keep the entire view centered vertically, we'll
+ // have to push it up by half the radius of the AM/PM circles.
+ float amPmCircleRadius = mCircleRadius * mAmPmCircleRadiusMultiplier;
+ mYCenter -= amPmCircleRadius / 2;
+ }
+
+ mTextSize = mCircleRadius * mTextSizeMultiplier;
+ if (mHasInnerCircle) {
+ mInnerTextSize = mCircleRadius * mInnerTextSizeMultiplier;
+ }
+
+ // Set up the spots for the animation.
+ renderAnimations();
+
+ mTextGridValuesDirty = true;
+ mDrawValuesReady = true;
+ }
+
+ if (mTextGridValuesDirty) {
+ float numbersRadius =
+ mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier;
+
+ calculateGridSizes(numbersRadius, mXCenter, mYCenter,
+ mTextSize, mTextGridHeights, mTextGridWidths);
+ if (mHasInnerCircle) {
+ float innerNumbersRadius =
+ mCircleRadius * mInnerNumbersRadiusMultiplier * mAnimationRadiusMultiplier;
+ calculateGridSizes(innerNumbersRadius, mXCenter, mYCenter,
+ mInnerTextSize, mInnerTextGridHeights, mInnerTextGridWidths);
+ }
+ mTextGridValuesDirty = false;
+ }
+
+ drawTexts(canvas, mTextSize, mTexts, mTextGridWidths, mTextGridHeights);
+ if (mHasInnerCircle) {
+ drawTexts(canvas, mInnerTextSize, mInnerTexts,
+ mInnerTextGridWidths, mInnerTextGridHeights);
+ }
+ }
+
+ private void calculateGridSizes(float numbersRadius, float xCenter, float yCenter,
+ float textSize, float[] textGridHeights, float[] textGridWidths) {
+ /*
+ * In the interest of efficient drawing, the following formulas have been simplified
+ * as much as possible.
+ * The numbers need to be drawn in a 7x7 grid representing the points on the Unit Circle.
+ */
+ float offset1 = numbersRadius;
+ // cos(30) = a / r => r * cos(30) = a => r * √3/2 = a
+ float offset2 = numbersRadius * ((float) Math.sqrt(3)) / 2f;
+ // sin(30) = o / r => r * sin(30) = o => r / 2 = a
+ float offset3 = numbersRadius / 2f;
+ // We'll need yTextBase to be slightly lower to account for the text's baseline.
+ mPaint.setTextSize(textSize);
+ yCenter -= (mPaint.descent() + mPaint.ascent()) / 2;
+ textGridHeights[0] = yCenter - offset1;
+ textGridWidths[0] = xCenter - offset1;
+ textGridHeights[1] = yCenter - offset2;
+ textGridWidths[1] = xCenter - offset2;
+ textGridHeights[2] = yCenter - offset3;
+ textGridWidths[2] = xCenter - offset3;
+ textGridHeights[3] = yCenter;
+ textGridWidths[3] = xCenter;
+ textGridHeights[4] = yCenter + offset3;
+ textGridWidths[4] = xCenter + offset3;
+ textGridHeights[5] = yCenter + offset2;
+ textGridWidths[5] = xCenter + offset2;
+ textGridHeights[6] = yCenter + offset1;
+ textGridWidths[6] = xCenter + offset1;
+ }
+
+ private void drawTexts(Canvas canvas, float textSize, String[] texts,
+ float[] textGridWidths, float[] textGridHeights) {
+ mPaint.setTextSize(textSize);
+ canvas.drawText(texts[0], textGridWidths[3], textGridHeights[0], mPaint);
+ canvas.drawText(texts[1], textGridWidths[4], textGridHeights[1], mPaint);
+ canvas.drawText(texts[2], textGridWidths[5], textGridHeights[2], mPaint);
+ canvas.drawText(texts[3], textGridWidths[6], textGridHeights[3], mPaint);
+ canvas.drawText(texts[4], textGridWidths[5], textGridHeights[4], mPaint);
+ canvas.drawText(texts[5], textGridWidths[4], textGridHeights[5], mPaint);
+ canvas.drawText(texts[6], textGridWidths[3], textGridHeights[6], mPaint);
+ canvas.drawText(texts[7], textGridWidths[2], textGridHeights[5], mPaint);
+ canvas.drawText(texts[8], textGridWidths[1], textGridHeights[4], mPaint);
+ canvas.drawText(texts[9], textGridWidths[0], textGridHeights[3], mPaint);
+ canvas.drawText(texts[10], textGridWidths[1], textGridHeights[2], mPaint);
+ canvas.drawText(texts[11], textGridWidths[2], textGridHeights[1], mPaint);
+ }
+
+ private void renderAnimations() {
+ Keyframe kf0, kf1, kf2, kf3;
+ float midwayPoint = 0.2f;
+ int duration = 500;
+
+ // Set up animator for disappearing.
+ kf0 = Keyframe.ofFloat(0f, 1);
+ kf1 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
+ kf2 = Keyframe.ofFloat(1f, mTransitionEndRadiusMultiplier);
+ PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe(
+ "animationRadiusMultiplier", kf0, kf1, kf2);
+
+ kf0 = Keyframe.ofFloat(0f, 1f);
+ kf1 = Keyframe.ofFloat(1f, 0f);
+ PropertyValuesHolder fadeOut = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1);
+
+ mDisappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
+ this, radiusDisappear, fadeOut).setDuration(duration);
+ mDisappearAnimator.addUpdateListener(mInvalidateUpdateListener);
+
+
+ // Set up animator for reappearing.
+ float delayMultiplier = 0.5f;
+ float transitionDurationMultiplier = 0.75f;
+ float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier;
+ int totalDuration = (int) (duration * totalDurationMultiplier);
+ float delayPoint = (delayMultiplier * duration) / totalDuration;
+ midwayPoint = 1 - (midwayPoint * (1 - delayPoint));
+
+ kf0 = Keyframe.ofFloat(0f, mTransitionEndRadiusMultiplier);
+ kf1 = Keyframe.ofFloat(delayPoint, mTransitionEndRadiusMultiplier);
+ kf2 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
+ kf3 = Keyframe.ofFloat(1f, 1);
+ PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe(
+ "animationRadiusMultiplier", kf0, kf1, kf2, kf3);
+
+ kf0 = Keyframe.ofFloat(0f, 0f);
+ kf1 = Keyframe.ofFloat(delayPoint, 0f);
+ kf2 = Keyframe.ofFloat(1f, 1f);
+ PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2);
+
+ mReappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
+ this, radiusReappear, fadeIn).setDuration(totalDuration);
+ mReappearAnimator.addUpdateListener(mInvalidateUpdateListener);
+ }
+
+ public ObjectAnimator getDisappearAnimator() {
+ if (!mIsInitialized || !mDrawValuesReady) {
+ Log.e(TAG, "RadialTextView was not ready for animation.");
+ return null;
+ }
+
+ return mDisappearAnimator;
+ }
+
+ public ObjectAnimator getReappearAnimator() {
+ if (!mIsInitialized || !mDrawValuesReady) {
+ Log.e(TAG, "RadialTextView was not ready for animation.");
+ return null;
+ }
+
+ return mReappearAnimator;
+ }
+
+ private class InvalidateUpdateListener implements AnimatorUpdateListener {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ RadialTextsView.this.invalidate();
+ }
+ }
+}
diff --git a/src/com/android/datetimepicker/TimePicker.java b/src/com/android/datetimepicker/TimePicker.java
new file mode 100644
index 0000000..201ba75
--- /dev/null
+++ b/src/com/android/datetimepicker/TimePicker.java
@@ -0,0 +1,527 @@
+/*
+ * Copyright (C) 2013 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.android.datetimepicker;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.Service;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.Vibrator;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.View.OnClickListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationSet;
+import android.view.animation.TranslateAnimation;
+import android.widget.FrameLayout;
+
+import com.android.datetimepicker.R;
+
+public class TimePicker extends FrameLayout implements OnTouchListener {
+ private static final String TAG = "TimePicker";
+
+ private final int TOUCH_SLOP;
+ private final int TAP_TIMEOUT;
+ private final int PRESSED_STATE_DURATION;
+ private static final int HOUR_VALUE_TO_DEGREES_STEP_SIZE = 30;
+ private static final int MINUTE_VALUE_TO_DEGREES_STEP_SIZE = 6;
+ private static final int HOUR_INDEX = TimePickerDialog.HOUR_INDEX;
+ private static final int MINUTE_INDEX = TimePickerDialog.MINUTE_INDEX;
+ private static final int AMPM_INDEX = TimePickerDialog.AMPM_INDEX;
+ private static final int AM = TimePickerDialog.AM;
+ private static final int PM = TimePickerDialog.PM;
+
+ private Vibrator mVibrator;
+ private long mLastVibrate;
+ private int mLastValueSelected;
+
+ private OnValueSelectedListener mListener;
+ private boolean mTimeInitialized;
+ private int mCurrentHoursOfDay;
+ private int mCurrentMinutes;
+ private boolean mIs24HourMode;
+ private int mCurrentItemShowing;
+
+ private CircleView mCircleView;
+ private AmPmCirclesView mAmPmCirclesView;
+ private RadialTextsView mHourRadialTextsView;
+ private RadialTextsView mMinuteRadialTextsView;
+ private RadialSelectorView mHourRadialSelectorView;
+ private RadialSelectorView mMinuteRadialSelectorView;
+
+ private int mIsTouchingAmOrPm = -1;
+ private boolean mDoingMove;
+ private int mDownDegrees;
+ private float mDownX;
+ private float mDownY;
+
+ private ReselectSelectorRunnable mReselectSelectorRunnable;
+
+ private Handler mHandler = new Handler();
+
+ public interface OnValueSelectedListener {
+ void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance);
+ }
+
+ public TimePicker(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ setOnTouchListener(this);
+ ViewConfiguration vc = ViewConfiguration.get(context);
+ TOUCH_SLOP = vc.getScaledTouchSlop();
+ TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
+ PRESSED_STATE_DURATION = ViewConfiguration.getPressedStateDuration();
+ mDoingMove = false;
+
+ mCircleView = new CircleView(context);
+ addView(mCircleView);
+
+ mAmPmCirclesView = new AmPmCirclesView(context);
+ addView(mAmPmCirclesView);
+
+ mHourRadialTextsView = new RadialTextsView(context);
+ addView(mHourRadialTextsView);
+ mMinuteRadialTextsView = new RadialTextsView(context);
+ addView(mMinuteRadialTextsView);
+
+ mHourRadialSelectorView = new RadialSelectorView(context);
+ addView(mHourRadialSelectorView);
+ mMinuteRadialSelectorView = new RadialSelectorView(context);
+ addView(mMinuteRadialSelectorView);
+
+ setCurrentItemShowing(HOUR_INDEX, false);
+
+ mReselectSelectorRunnable = new ReselectSelectorRunnable(this);
+
+ mVibrator = (Vibrator) context.getSystemService(Service.VIBRATOR_SERVICE);
+ mLastVibrate = 0;
+ mLastValueSelected = -1;
+
+ mTimeInitialized = false;
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
+ super.onMeasure(widthMeasureSpec,
+ measuredWidth < measuredHeight? widthMeasureSpec : heightMeasureSpec);
+ }
+
+ public void setOnValueSelectedListener(OnValueSelectedListener listener) {
+ mListener = listener;
+ }
+
+ public void initialize(Context context, int initialHoursOfDay, int initialMinutes,
+ boolean is24HourMode) {
+ if (mTimeInitialized) {
+ Log.e(TAG, "Time has already been initialized.");
+ return;
+ }
+
+ setValueForItem(HOUR_INDEX, initialHoursOfDay);
+ setValueForItem(MINUTE_INDEX, initialMinutes);
+ mIs24HourMode = is24HourMode;
+
+ mCircleView.initialize(context, is24HourMode);
+ mCircleView.invalidate();
+ if (!is24HourMode) {
+ mAmPmCirclesView.initialize(context, initialHoursOfDay < 12? AM : PM);
+ mAmPmCirclesView.invalidate();
+ }
+
+ Resources res = context.getResources();
+ String[] hoursTexts = res.getStringArray(is24HourMode? R.array.hours_24 : R.array.hours);
+ String[] innerHoursTexts = res.getStringArray(R.array.hours);
+ String[] minutesTexts = res.getStringArray(R.array.minutes);
+ mHourRadialTextsView.initialize(res,
+ hoursTexts, (is24HourMode? innerHoursTexts : null), is24HourMode, true);
+ mHourRadialTextsView.invalidate();
+ mMinuteRadialTextsView.initialize(res, minutesTexts, null, is24HourMode, false);
+ mMinuteRadialTextsView.invalidate();
+
+ int initialHourDegrees = (initialHoursOfDay % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE;
+ int initialMinuteDegrees = initialMinutes * MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
+ mHourRadialSelectorView.initialize(context, initialHourDegrees,
+ is24HourMode, is24HourMode, isHourInnerCircle(initialHoursOfDay), true);
+ mHourRadialSelectorView.invalidate();
+ mMinuteRadialSelectorView.initialize(context, initialMinuteDegrees,
+ is24HourMode, false, false, false);
+ mHourRadialSelectorView.invalidate();
+
+
+ mTimeInitialized = true;
+ }
+
+ private boolean isHourInnerCircle(int hourOfDay) {
+ // We'll have the 00 hours on the outside circle.
+ return mIs24HourMode && (hourOfDay <= 12 && hourOfDay != 0);
+ }
+
+ public int getHours() {
+ return mCurrentHoursOfDay;
+ }
+
+ public int getMinutes() {
+ return mCurrentMinutes;
+ }
+
+ private int getCurrentlyShowingValue() {
+ int currentIndex = getCurrentItemShowing();
+ if (currentIndex == HOUR_INDEX) {
+ return mCurrentHoursOfDay;
+ } else if (currentIndex == MINUTE_INDEX) {
+ return mCurrentMinutes;
+ } else {
+ return -1;
+ }
+ }
+
+ public int getIsCurrentlyAmOrPm() {
+ if (mCurrentHoursOfDay < 12) {
+ return AM;
+ } else if (mCurrentHoursOfDay < 24) {
+ return PM;
+ }
+ return -1;
+ }
+
+ private void setValueForItem(int index, int value) {
+ if (index == HOUR_INDEX) {
+ mCurrentHoursOfDay = value;
+ } else if (index == MINUTE_INDEX){
+ mCurrentMinutes = value;
+ } else if (index == AMPM_INDEX) {
+ if (value == AM) {
+ mCurrentHoursOfDay = mCurrentHoursOfDay % 12;
+ } else if (value == PM) {
+ mCurrentHoursOfDay = (mCurrentHoursOfDay % 12) + 12;
+ }
+ }
+ }
+
+ public void setAmOrPm(int amOrPm) {
+ mAmPmCirclesView.setAmOrPm(amOrPm);
+ mAmPmCirclesView.invalidate();
+ setValueForItem(AMPM_INDEX, amOrPm);
+ }
+
+ private int reselectSelector(int index, int degrees, boolean isInnerCircle,
+ boolean forceNotFineGrained, boolean forceDrawLine, boolean forceDrawDot) {
+ if (degrees == -1 || (index != 0 && index != 1)) {
+ return -1;
+ }
+
+ int stepSize;
+ int currentShowing = getCurrentItemShowing();
+ if (!forceNotFineGrained && (currentShowing == 1)) {
+ stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
+ } else {
+ stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
+ }
+ int floor = (degrees / stepSize) * stepSize;
+ int ceiling = floor + stepSize;
+ if ((degrees - floor) < (ceiling - degrees)) {
+ degrees = floor;
+ } else {
+ degrees = ceiling;
+ }
+
+ RadialSelectorView radialSelectorView;
+ if (index == 0) {
+ // Index == 0, hours.
+ radialSelectorView = mHourRadialSelectorView;
+ stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
+ } else {
+ // Index == 1, minutes.
+ radialSelectorView = mMinuteRadialSelectorView;
+ stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
+ }
+ radialSelectorView.setSelection(degrees, isInnerCircle, forceDrawLine, forceDrawDot);
+ radialSelectorView.invalidate();
+
+
+ if (currentShowing == HOUR_INDEX) {
+ if (mIs24HourMode) {
+ if (degrees == 0 && isInnerCircle) {
+ degrees = 360;
+ } else if (degrees == 360 && !isInnerCircle) {
+ degrees = 0;
+ }
+ } else if (degrees == 0) {
+ degrees = 360;
+ }
+ } else if (degrees == 360 && currentShowing == MINUTE_INDEX) {
+ degrees = 0;
+ }
+
+ int value = degrees / stepSize;
+ if (currentShowing == HOUR_INDEX && mIs24HourMode && !isInnerCircle && degrees != 0) {
+ value += 12;
+ }
+ return value;
+ }
+
+ private int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal,
+ final Boolean[] isInnerCircle) {
+ int currentItem = getCurrentItemShowing();
+ if (currentItem == 0) {
+ return mHourRadialSelectorView.getDegreesFromCoords(
+ pointX, pointY, forceLegal, isInnerCircle);
+ } else if (currentItem == 1) {
+ return mMinuteRadialSelectorView.getDegreesFromCoords(
+ pointX, pointY, forceLegal, isInnerCircle);
+ } else {
+ return -1;
+ }
+ }
+
+ public int getCurrentItemShowing() {
+ if (mCurrentItemShowing != HOUR_INDEX && mCurrentItemShowing != MINUTE_INDEX) {
+ Log.e(TAG, "Current item showing was unfortunately set to "+mCurrentItemShowing);
+ return -1;
+ }
+ return mCurrentItemShowing;
+ }
+
+ public void setCurrentItemShowing(int index, boolean animate) {
+ if (index != HOUR_INDEX && index != MINUTE_INDEX) {
+ Log.e(TAG, "TimePicker does not support view at index "+index);
+ return;
+ }
+
+ if (animate && (index != getCurrentItemShowing())) {
+ ObjectAnimator[] anims = new ObjectAnimator[4];
+ if (index == MINUTE_INDEX) {
+ anims[0] = mHourRadialTextsView.getDisappearAnimator();
+ anims[1] = mHourRadialSelectorView.getDisappearAnimator();
+ anims[2] = mMinuteRadialTextsView.getReappearAnimator();
+ anims[3] = mMinuteRadialSelectorView.getReappearAnimator();
+ } else if (index == HOUR_INDEX){
+ anims[0] = mHourRadialTextsView.getReappearAnimator();
+ anims[1] = mHourRadialSelectorView.getReappearAnimator();
+ anims[2] = mMinuteRadialTextsView.getDisappearAnimator();
+ anims[3] = mMinuteRadialSelectorView.getDisappearAnimator();
+ }
+
+ AnimatorSet transition = new AnimatorSet();
+ transition.playTogether(anims);
+ transition.start();
+ } else {
+ int hourAlpha = (index == 0) ? 255 : 0;
+ int minuteAlpha = (index == 1) ? 255 : 0;
+ mHourRadialTextsView.setAlpha(hourAlpha);
+ mHourRadialSelectorView.setAlpha(hourAlpha);
+ mMinuteRadialTextsView.setAlpha(minuteAlpha);
+ mMinuteRadialSelectorView.setAlpha(minuteAlpha);
+ }
+
+ mCurrentItemShowing = index;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ final float eventX = event.getX();
+ final float eventY = event.getY();
+ int degrees;
+ int value;
+ final int currentShowing = getCurrentItemShowing();
+ final Boolean[] isInnerCircle = new Boolean[1];
+ isInnerCircle[0] = false;
+
+ long millis = SystemClock.uptimeMillis();
+
+ switch(event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mDownX = eventX;
+ mDownY = eventY;
+
+ mLastValueSelected = -1;
+ mDoingMove = false;
+ if (!mIs24HourMode) {
+ mIsTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
+ } else {
+ mIsTouchingAmOrPm = -1;
+ }
+ if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
+ tryVibrate();
+ mDownDegrees = -1;
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mAmPmCirclesView.setAmOrPmPressed(mIsTouchingAmOrPm);
+ mAmPmCirclesView.invalidate();
+ }
+ }, TAP_TIMEOUT);
+ } else {
+ mDownDegrees = getDegreesFromCoords(eventX, eventY, false, isInnerCircle);
+ if (mDownDegrees != -1) {
+ tryTick();
+ mLastValueSelected = getCurrentlyShowingValue();
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mDoingMove = true;
+ int value = reselectSelector(currentShowing, mDownDegrees,
+ isInnerCircle[0], false, true, true);
+ mListener.onValueSelected(getCurrentItemShowing(), value, false);
+ }
+ }, TAP_TIMEOUT);
+ }
+ }
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ float dY = Math.abs(eventY - mDownY);
+ float dX = Math.abs(eventX - mDownX);
+
+ if (!mDoingMove && dX <= TOUCH_SLOP && dY <= TOUCH_SLOP) {
+ // Hasn't registered down yet, just slight, accidental movement of finger.
+ break;
+ }
+
+ // If we're in the middle of touching down on AM or PM, check if we still are.
+ // If so, no-op. If not, remove its pressed state. Either way, no need to check
+ // for touches on the other circle.
+ if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
+ mHandler.removeCallbacksAndMessages(null);
+ int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
+ if (isTouchingAmOrPm != mIsTouchingAmOrPm) {
+ mAmPmCirclesView.setAmOrPmPressed(-1);
+ mAmPmCirclesView.invalidate();
+ mIsTouchingAmOrPm = -1;
+ }
+ break;
+ }
+
+ if (mDownDegrees == -1) {
+ // Original down was illegal, so no movement will register.
+ break;
+ }
+
+ mDoingMove = true;
+ mHandler.removeCallbacksAndMessages(null);
+ degrees = getDegreesFromCoords(eventX, eventY, true, isInnerCircle);
+ if (degrees != -1) {
+ value = reselectSelector(currentShowing, degrees,
+ isInnerCircle[0], false, true, true);
+ if (value != mLastValueSelected) {
+ tryTick();
+ mLastValueSelected = value;
+ }
+ mListener.onValueSelected(getCurrentItemShowing(), value, false);
+ }
+ return true;
+ case MotionEvent.ACTION_UP:
+ mHandler.removeCallbacksAndMessages(null);
+
+ if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
+ int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
+ mAmPmCirclesView.setAmOrPmPressed(-1);
+ mAmPmCirclesView.invalidate();
+
+ if (isTouchingAmOrPm == mIsTouchingAmOrPm) {
+ mAmPmCirclesView.setAmOrPm(isTouchingAmOrPm);
+ if (getIsCurrentlyAmOrPm() != isTouchingAmOrPm) {
+ mListener.onValueSelected(AMPM_INDEX, mIsTouchingAmOrPm, false);
+ setValueForItem(AMPM_INDEX, isTouchingAmOrPm);
+ }
+ }
+ mIsTouchingAmOrPm = -1;
+ break;
+ }
+
+ if (mDownDegrees != -1) {
+ degrees = getDegreesFromCoords(eventX, eventY, mDoingMove, isInnerCircle);
+ if (degrees != -1) {
+ value = reselectSelector(currentShowing, degrees, isInnerCircle[0],
+ !mDoingMove, true, false);
+ mListener.onValueSelected(getCurrentItemShowing(), value, true);
+
+ if (currentShowing == HOUR_INDEX && !mIs24HourMode) {
+ int amOrPm = getIsCurrentlyAmOrPm();
+ if (amOrPm == AM && value == 12) {
+ value = 0;
+ } else if (amOrPm == PM && value != 12) {
+ value += 12;
+ }
+ }
+ setValueForItem(getCurrentItemShowing(), value);
+ }
+ }
+ mDoingMove = false;
+ return true;
+ default:
+ break;
+ }
+ return false;
+ }
+
+ private class ReselectSelectorRunnable implements Runnable {
+ TimePicker mTimePicker;
+ private int mIndex;
+ private int mDegrees;
+ private boolean mIsInnerCircle;
+ private boolean mForceNotFineGrained;
+ private boolean mForceDrawLine;
+ private boolean mForceDrawDot;
+
+ public ReselectSelectorRunnable(TimePicker timePicker) {
+ mTimePicker = timePicker;
+ }
+
+ public void initializeValues(int index, int degrees, boolean isInnerCircle,
+ boolean forceNotFineGrained, boolean forceDrawLine, boolean forceDrawDot) {
+ mIndex = index;
+ mDegrees = degrees;
+ mIsInnerCircle = isInnerCircle;
+ mForceNotFineGrained = forceNotFineGrained;
+ mForceDrawDot = forceDrawDot;
+ }
+
+ @Override
+ public void run() {
+ mTimePicker.reselectSelector(mIndex, mDegrees, mIsInnerCircle, mForceNotFineGrained,
+ mForceDrawLine, mForceDrawDot);
+ }
+ }
+
+ public void tryVibrate() {
+ if (mVibrator != null) {
+ long now = SystemClock.uptimeMillis();
+ // We want to try to vibrate each individual tick discretely.
+ if (now - mLastVibrate >= 100) {
+ mVibrator.vibrate(5);
+ mLastVibrate = now;
+ }
+ }
+ }
+
+ public void tryTick() {
+ tryVibrate();
+ }
+}
diff --git a/src/com/android/datetimepicker/TimePickerDialog.java b/src/com/android/datetimepicker/TimePickerDialog.java
new file mode 100644
index 0000000..d76ebd4
--- /dev/null
+++ b/src/com/android/datetimepicker/TimePickerDialog.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2013 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.android.datetimepicker;
+
+import android.app.ActionBar.LayoutParams;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.text.style.AlignmentSpan;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.datetimepicker.R;
+
+import com.android.datetimepicker.TimePicker.OnValueSelectedListener;
+
+/**
+ * Dialog to set a time.
+ */
+public class TimePickerDialog extends DialogFragment implements OnValueSelectedListener{
+ private static final String TAG = "TimePickerDialog";
+
+ private static final String KEY_HOUR_OF_DAY = "hour_of_day";
+ private static final String KEY_MINUTE = "minute";
+ private static final String KEY_IS_24_HOUR_VIEW = "is_24_hour_view";
+ public static final int HOUR_INDEX = 0;
+ public static final int MINUTE_INDEX = 1;
+ public static final int AMPM_INDEX = 2; // NOT a real index for the purpose of what's showing.
+ public static final int AM = 0;
+ public static final int PM = 1;
+
+ private Handler mHandler = new Handler();
+
+ private OnTimeSetListener mCallback;
+
+ private Button mDoneButton;
+ private TextView mHourView;
+ private TextView mMinuteView;
+ private TextView mAmPmTextView;
+ private TimePicker mTimePicker;
+
+ private int mBlue;
+ private int mBlack;
+ private String mAmText;
+ private String mPmText;
+
+ private boolean mAllowAutoAdvance;
+ private int mInitialHourOfDay;
+ private int mInitialMinute;
+ private boolean mIs24HourMode;
+ private int mWidthPixels;
+
+ /**
+ * The callback interface used to indicate the user is done filling in
+ * the time (they clicked on the 'Set' button).
+ */
+ public interface OnTimeSetListener {
+
+ /**
+ * @param view The view associated with this listener.
+ * @param hourOfDay The hour that was set.
+ * @param minute The minute that was set.
+ */
+ void onTimeSet(TimePicker view, int hourOfDay, int minute);
+ }
+
+ public TimePickerDialog() {
+ // Empty constructor required for dialog fragment.
+ }
+
+ public TimePickerDialog(Context context, int theme, OnTimeSetListener callback,
+ int hourOfDay, int minute, boolean is24HourMode) {
+ // Empty constructor required for dialog fragment.
+ }
+
+ public static TimePickerDialog newInstance(OnTimeSetListener callback,
+ int hourOfDay, int minute, boolean is24HourMode) {
+ TimePickerDialog ret = new TimePickerDialog();
+ ret.initialize(callback, hourOfDay, minute, is24HourMode);
+ return ret;
+ }
+
+ public void initialize(OnTimeSetListener callback,
+ int hourOfDay, int minute, boolean is24HourMode) {
+ mCallback = callback;
+
+ mInitialHourOfDay = hourOfDay;
+ mInitialMinute = minute;
+ mIs24HourMode = is24HourMode;
+ }
+
+ public void setOnTimeSetListener(OnTimeSetListener callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null && savedInstanceState.containsKey(KEY_HOUR_OF_DAY)
+ && savedInstanceState.containsKey(KEY_MINUTE)
+ && savedInstanceState.containsKey(KEY_IS_24_HOUR_VIEW)) {
+ mInitialHourOfDay = savedInstanceState.getInt(KEY_HOUR_OF_DAY);
+ mInitialMinute = savedInstanceState.getInt(KEY_MINUTE);
+ mIs24HourMode = savedInstanceState.getBoolean(KEY_IS_24_HOUR_VIEW);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+
+ View view = inflater.inflate(R.layout.time_picker_dialog, null);
+ Resources res = getResources();
+
+ mBlue = res.getColor(R.color.blue);
+ mBlack = res.getColor(R.color.black);
+
+ mHourView = (TextView) view.findViewById(R.id.hours);
+ mMinuteView = (TextView) view.findViewById(R.id.minutes);
+ mAmPmTextView = (TextView) view.findViewById(R.id.ampm_label);
+ mAmText = res.getString(R.string.am_label);
+ mPmText = res.getString(R.string.pm_label);
+
+ mTimePicker = (TimePicker) view.findViewById(R.id.time_picker);
+ mTimePicker.setOnValueSelectedListener(this);
+ mTimePicker.initialize(getActivity(), mInitialHourOfDay, mInitialMinute, mIs24HourMode);
+ mTimePicker.invalidate();
+
+ mHourView.setTextColor(mBlue);
+ mHourView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setCurrentItemShowing(HOUR_INDEX, true);
+ mTimePicker.tryVibrate();
+ }
+ });
+ mMinuteView.setTextColor(mBlack);
+ mMinuteView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setCurrentItemShowing(MINUTE_INDEX, true);
+ mTimePicker.tryVibrate();
+ }
+ });
+
+ mDoneButton = (Button) view.findViewById(R.id.done_button);
+ mDoneButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mTimePicker.tryVibrate();
+ if (mCallback != null) {
+ mCallback.onTimeSet(mTimePicker,
+ mTimePicker.getHours(), mTimePicker.getMinutes());
+ }
+ dismiss();
+ }
+ });
+
+ DisplayMetrics metrics = new DisplayMetrics();
+ getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
+ mWidthPixels = metrics.widthPixels;
+
+ if (mIs24HourMode) {
+ mAmPmTextView.setVisibility(View.GONE);
+
+ RelativeLayout.LayoutParams paramsSeparator = new RelativeLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ paramsSeparator.addRule(RelativeLayout.CENTER_IN_PARENT);
+ TextView separatorView = (TextView) view.findViewById(R.id.separator);
+ separatorView.setLayoutParams(paramsSeparator);
+ } else {
+ mAmPmTextView.setVisibility(View.VISIBLE);
+ updateAmPmDisplay(mInitialHourOfDay < 12? AM : PM);
+ View amPmHitspace = view.findViewById(R.id.ampm_hitspace);
+ amPmHitspace.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mTimePicker.tryVibrate();
+ int amOrPm = mTimePicker.getIsCurrentlyAmOrPm();
+ if (amOrPm == AM) {
+ amOrPm = PM;
+ } else if (amOrPm == PM){
+ amOrPm = AM;
+ }
+ updateAmPmDisplay(amOrPm);
+ mTimePicker.setAmOrPm(amOrPm);
+ }
+ });
+ }
+
+ mAllowAutoAdvance = true;
+ setHour(mInitialHourOfDay);
+ setMinute(mInitialMinute);
+
+ return view;
+ }
+
+ private void updateAmPmDisplay(int amOrPm) {
+ if (amOrPm == AM) {
+ mAmPmTextView.setText(mAmText);
+ } else if (amOrPm == PM){
+ mAmPmTextView.setText(mPmText);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (mTimePicker != null) {
+ outState.putInt(KEY_HOUR_OF_DAY, mTimePicker.getHours());
+ outState.putInt(KEY_MINUTE, mTimePicker.getMinutes());
+ outState.putBoolean(KEY_IS_24_HOUR_VIEW, mIs24HourMode);
+ }
+ }
+
+ @Override
+ public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) {
+ if (pickerIndex == HOUR_INDEX) {
+ setHour(newValue);
+ if (mAllowAutoAdvance && autoAdvance) {
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ setCurrentItemShowing(MINUTE_INDEX, true);
+ }
+ }, 150);
+ }
+ } else if (pickerIndex == MINUTE_INDEX){
+ setMinute(newValue);
+ } else if (pickerIndex == AMPM_INDEX) {
+ updateAmPmDisplay(newValue);
+ }
+ }
+
+ private void setHour(int value) {
+ String format;
+ if (mIs24HourMode) {
+ format = "%02d";
+ } else {
+ format = "%d";
+ value = value % 12;
+ if (value == 0) {
+ value = 12;
+ }
+ }
+
+ mHourView.setText(String.format(format, value));
+ }
+
+ private void setMinute(int value) {
+ if (value == 60) {
+ value = 0;
+ }
+ mMinuteView.setText(String.format("%02d", value));
+ }
+
+ private void setCurrentItemShowing(int index, boolean animate) {
+/*
+ if (mAllowAutoAdvance && index == 1) {
+ // Once we've seen the minutes, no need to auto-advance.
+ mAllowAutoAdvance = false;
+ }
+*/
+ mTimePicker.setCurrentItemShowing(index, animate);
+ int hourColor = (index == HOUR_INDEX)? mBlue : mBlack;
+ int minuteColor = (index == MINUTE_INDEX)? mBlue : mBlack;
+ mHourView.setTextColor(hourColor);
+ mMinuteView.setTextColor(minuteColor);
+ }
+}