mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 17:18:52 -06:00
Flatten uhabits-android into a single gradle module
This commit is contained in:
306
uhabits-android/src/main/AndroidManifest.xml
Normal file
306
uhabits-android/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,306 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
~
|
||||
~ This file is part of Loop Habit Tracker.
|
||||
~
|
||||
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by the
|
||||
~ Free Software Foundation, either version 3 of the License, or (at your
|
||||
~ option) any later version.
|
||||
~
|
||||
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
~ more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License along
|
||||
~ with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.isoron.uhabits">
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".HabitsApplication"
|
||||
android:allowBackup="true"
|
||||
android:backupAgent=".HabitsBackupAgent"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/main_activity_title"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppBaseTheme">
|
||||
|
||||
<activity
|
||||
android:name=".activities.habits.edit.EditHabitActivity"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.habits.list.ListHabitsActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.sync.SyncActivity"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.settings.SettingsActivity" />
|
||||
</activity>
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
android:value="AEdPqrEAAAAI6aeWncbnMNo8E5GWeZ44dlc5cQ7tCROwFhOtiw" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.habits.list.ListHabitsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/main_activity_title"
|
||||
android:launchMode="singleTop">
|
||||
<tools:validation testUrl="https://loophabits.org/sync/123" />
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="loophabits.org"
|
||||
android:pathPrefix="/sync" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/main_activity_title"
|
||||
android:launchMode="singleTop"
|
||||
android:targetActivity=".activities.habits.list.ListHabitsActivity">
|
||||
<intent-filter android:label="@string/main_activity_title">
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:name=".activities.habits.show.ShowHabitActivity"
|
||||
android:label="@string/title_activity_show_habit">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.habits.list.ListHabitsActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.settings.SettingsActivity"
|
||||
android:label="@string/settings">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.habits.list.ListHabitsActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.intro.IntroActivity"
|
||||
android:label=""
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.HabitPickerDialog"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.BooleanHabitPickerDialog"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.NumericalHabitPickerDialog"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.about.AboutActivity"
|
||||
android:label="@string/about">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.habits.list.ListHabitsActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.NumericalCheckmarkWidgetActivity"
|
||||
android:label="NumericalCheckmarkWidget"
|
||||
android:noHistory="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="org.isoron.uhabits.ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".notifications.SnoozeDelayPickerActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.CheckmarkWidgetProvider"
|
||||
android:label="@string/checkmark">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_checkmark_info" />
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".widgets.StackWidgetService"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.HistoryWidgetProvider"
|
||||
android:label="@string/history">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_history_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.ScoreWidgetProvider"
|
||||
android:label="@string/score">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_score_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.StreakWidgetProvider"
|
||||
android:label="@string/streaks">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_streak_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.FrequencyWidgetProvider"
|
||||
android:label="@string/frequency">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_frequency_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.TargetWidgetProvider"
|
||||
android:label="@string/target">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_target_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".receivers.ReminderReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".receivers.WidgetReceiver">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="org.isoron.uhabits.ACTION_SET_NUMERICAL_VALUE" />
|
||||
<data
|
||||
android:host="org.isoron.uhabits"
|
||||
android:scheme="content" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<action android:name="org.isoron.uhabits.ACTION_TOGGLE_REPETITION" />
|
||||
|
||||
<data
|
||||
android:host="org.isoron.uhabits"
|
||||
android:scheme="content" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<action android:name="org.isoron.uhabits.ACTION_ADD_REPETITION" />
|
||||
|
||||
<data
|
||||
android:host="org.isoron.uhabits"
|
||||
android:scheme="content" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<action android:name="org.isoron.uhabits.ACTION_REMOVE_REPETITION" />
|
||||
|
||||
<data
|
||||
android:host="org.isoron.uhabits"
|
||||
android:scheme="content" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Locale/Tasker -->
|
||||
<activity
|
||||
android:name=".automation.EditSettingActivity"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Locale/Tasker -->
|
||||
<receiver
|
||||
android:name=".automation.FireSettingReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="org.isoron.uhabits"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
41
uhabits-android/src/main/assets/cacert.pem
Normal file
41
uhabits-android/src/main/assets/cacert.pem
Normal file
@@ -0,0 +1,41 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290
|
||||
IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB
|
||||
IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA
|
||||
Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO
|
||||
BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi
|
||||
MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ
|
||||
ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
|
||||
CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ
|
||||
8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6
|
||||
zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y
|
||||
fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7
|
||||
w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc
|
||||
G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k
|
||||
epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q
|
||||
laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ
|
||||
QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU
|
||||
fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826
|
||||
YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w
|
||||
ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY
|
||||
gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe
|
||||
MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0
|
||||
IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy
|
||||
dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw
|
||||
czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0
|
||||
dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl
|
||||
aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC
|
||||
AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg
|
||||
b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB
|
||||
ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc
|
||||
nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg
|
||||
18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c
|
||||
gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl
|
||||
Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY
|
||||
sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T
|
||||
SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF
|
||||
CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum
|
||||
GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk
|
||||
zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW
|
||||
omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD
|
||||
-----END CERTIFICATE-----
|
||||
BIN
uhabits-android/src/main/assets/fontawesome-webfont.ttf
Normal file
BIN
uhabits-android/src/main/assets/fontawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
uhabits-android/src/main/ic_launcher-web.png
Normal file
BIN
uhabits-android/src/main/ic_launcher-web.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
uhabits-android/src/main/ic_small_widget_preview-web.png
Normal file
BIN
uhabits-android/src/main/ic_small_widget_preview-web.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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.colorpicker;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDialogFragment;
|
||||
|
||||
import com.android.colorpicker.ColorPickerSwatch.OnColorSelectedListener;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
/**
|
||||
* A dialog which takes in as input an array of palette and creates a palette allowing the user to
|
||||
* select a specific color swatch, which invokes a listener.
|
||||
*/
|
||||
public class ColorPickerDialog extends AppCompatDialogFragment implements OnColorSelectedListener {
|
||||
|
||||
public static final int SIZE_LARGE = 1;
|
||||
public static final int SIZE_SMALL = 2;
|
||||
|
||||
protected AlertDialog mAlertDialog;
|
||||
|
||||
protected static final String KEY_TITLE_ID = "title_id";
|
||||
protected static final String KEY_COLORS = "palette";
|
||||
protected static final String KEY_SELECTED_COLOR = "selected_color";
|
||||
protected static final String KEY_COLUMNS = "columns";
|
||||
protected static final String KEY_SIZE = "size";
|
||||
|
||||
protected int mTitleResId = R.string.color_picker_default_title;
|
||||
protected int[] mColors = null;
|
||||
protected int mSelectedColor;
|
||||
protected int mColumns;
|
||||
protected int mSize;
|
||||
|
||||
private ColorPickerPalette mPalette;
|
||||
private ProgressBar mProgress;
|
||||
|
||||
protected OnColorSelectedListener mListener;
|
||||
|
||||
public ColorPickerDialog() {
|
||||
// Empty constructor required for dialog fragments.
|
||||
}
|
||||
|
||||
public static ColorPickerDialog newInstance(int titleResId, int[] colors, int selectedColor,
|
||||
int columns, int size) {
|
||||
ColorPickerDialog ret = new ColorPickerDialog();
|
||||
ret.initialize(titleResId, colors, selectedColor, columns, size);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void initialize(int titleResId, int[] colors, int selectedColor, int columns, int size) {
|
||||
setArguments(titleResId, columns, size);
|
||||
setColors(colors, selectedColor);
|
||||
}
|
||||
|
||||
public void setArguments(int titleResId, int columns, int size) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(KEY_TITLE_ID, titleResId);
|
||||
bundle.putInt(KEY_COLUMNS, columns);
|
||||
bundle.putInt(KEY_SIZE, size);
|
||||
setArguments(bundle);
|
||||
}
|
||||
|
||||
public void setOnColorSelectedListener(OnColorSelectedListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (getArguments() != null) {
|
||||
mTitleResId = getArguments().getInt(KEY_TITLE_ID);
|
||||
mColumns = getArguments().getInt(KEY_COLUMNS);
|
||||
mSize = getArguments().getInt(KEY_SIZE);
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
mColors = savedInstanceState.getIntArray(KEY_COLORS);
|
||||
mSelectedColor = (Integer) savedInstanceState.getSerializable(KEY_SELECTED_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity activity = getActivity();
|
||||
|
||||
View view = LayoutInflater.from(getActivity()).inflate(R.layout.color_picker_dialog, null);
|
||||
mProgress = (ProgressBar) view.findViewById(android.R.id.progress);
|
||||
mPalette = (ColorPickerPalette) view.findViewById(R.id.color_picker);
|
||||
mPalette.init(mSize, mColumns, this);
|
||||
|
||||
if (mColors != null) {
|
||||
showPaletteView();
|
||||
}
|
||||
|
||||
mAlertDialog = new AlertDialog.Builder(activity)
|
||||
.setTitle(mTitleResId)
|
||||
.setView(view)
|
||||
.create();
|
||||
|
||||
return mAlertDialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorSelected(int color) {
|
||||
if (mListener != null) {
|
||||
mListener.onColorSelected(color);
|
||||
}
|
||||
|
||||
if (getTargetFragment() instanceof OnColorSelectedListener) {
|
||||
final OnColorSelectedListener listener =
|
||||
(OnColorSelectedListener) getTargetFragment();
|
||||
listener.onColorSelected(color);
|
||||
}
|
||||
|
||||
if (color != mSelectedColor) {
|
||||
mSelectedColor = color;
|
||||
// Redraw palette to show checkmark on newly selected color before dismissing.
|
||||
mPalette.drawPalette(mColors, mSelectedColor);
|
||||
}
|
||||
|
||||
dismiss();
|
||||
}
|
||||
|
||||
public void showPaletteView() {
|
||||
if (mProgress != null && mPalette != null) {
|
||||
mProgress.setVisibility(View.GONE);
|
||||
refreshPalette();
|
||||
mPalette.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public void showProgressBarView() {
|
||||
if (mProgress != null && mPalette != null) {
|
||||
mProgress.setVisibility(View.VISIBLE);
|
||||
mPalette.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setColors(int[] colors, int selectedColor) {
|
||||
if (mColors != colors || mSelectedColor != selectedColor) {
|
||||
mColors = colors;
|
||||
mSelectedColor = selectedColor;
|
||||
refreshPalette();
|
||||
}
|
||||
}
|
||||
|
||||
public void setColors(int[] colors) {
|
||||
if (mColors != colors) {
|
||||
mColors = colors;
|
||||
refreshPalette();
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelectedColor(int color) {
|
||||
if (mSelectedColor != color) {
|
||||
mSelectedColor = color;
|
||||
refreshPalette();
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshPalette() {
|
||||
if (mPalette != null && mColors != null) {
|
||||
mPalette.drawPalette(mColors, mSelectedColor);
|
||||
}
|
||||
}
|
||||
|
||||
public int[] getColors() {
|
||||
return mColors;
|
||||
}
|
||||
|
||||
public int getSelectedColor() {
|
||||
return mSelectedColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putIntArray(KEY_COLORS, mColors);
|
||||
outState.putSerializable(KEY_SELECTED_COLOR, mSelectedColor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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.colorpicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TableLayout;
|
||||
import android.widget.TableRow;
|
||||
|
||||
import com.android.colorpicker.ColorPickerSwatch.OnColorSelectedListener;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
/**
|
||||
* A color picker custom view which creates an grid of color squares. The number of squares per
|
||||
* row (and the padding between the squares) is determined by the user.
|
||||
*/
|
||||
public class ColorPickerPalette extends TableLayout {
|
||||
|
||||
public OnColorSelectedListener mOnColorSelectedListener;
|
||||
|
||||
private String mDescription;
|
||||
private String mDescriptionSelected;
|
||||
|
||||
private int mSwatchLength;
|
||||
private int mMarginSize;
|
||||
private int mNumColumns;
|
||||
|
||||
public ColorPickerPalette(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public ColorPickerPalette(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the size, columns, and listener. Size should be a pre-defined size (SIZE_LARGE
|
||||
* or SIZE_SMALL) from ColorPickerDialogFragment.
|
||||
*/
|
||||
public void init(int size, int columns, OnColorSelectedListener listener) {
|
||||
mNumColumns = columns;
|
||||
Resources res = getResources();
|
||||
if (size == ColorPickerDialog.SIZE_LARGE) {
|
||||
mSwatchLength = res.getDimensionPixelSize(R.dimen.color_swatch_large);
|
||||
mMarginSize = res.getDimensionPixelSize(R.dimen.color_swatch_margins_large);
|
||||
} else {
|
||||
mSwatchLength = res.getDimensionPixelSize(R.dimen.color_swatch_small);
|
||||
mMarginSize = res.getDimensionPixelSize(R.dimen.color_swatch_margins_small);
|
||||
}
|
||||
mOnColorSelectedListener = listener;
|
||||
|
||||
mDescription = res.getString(R.string.color_swatch_description);
|
||||
mDescriptionSelected = res.getString(R.string.color_swatch_description_selected);
|
||||
}
|
||||
|
||||
private TableRow createTableRow() {
|
||||
TableRow row = new TableRow(getContext());
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.WRAP_CONTENT);
|
||||
row.setLayoutParams(params);
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds swatches to table in a serpentine format.
|
||||
*/
|
||||
public void drawPalette(int[] colors, int selectedColor) {
|
||||
if (colors == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeAllViews();
|
||||
int tableElements = 0;
|
||||
int rowElements = 0;
|
||||
int rowNumber = 0;
|
||||
|
||||
// Fills the table with swatches based on the array of palette.
|
||||
TableRow row = createTableRow();
|
||||
for (int color : colors) {
|
||||
tableElements++;
|
||||
|
||||
View colorSwatch = createColorSwatch(color, selectedColor);
|
||||
setSwatchDescription(rowNumber, tableElements, rowElements, color == selectedColor,
|
||||
colorSwatch);
|
||||
addSwatchToRow(row, colorSwatch, rowNumber);
|
||||
|
||||
rowElements++;
|
||||
if (rowElements == mNumColumns) {
|
||||
addView(row);
|
||||
row = createTableRow();
|
||||
rowElements = 0;
|
||||
rowNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
// Create blank views to fill the row if the last row has not been filled.
|
||||
if (rowElements > 0) {
|
||||
while (rowElements != mNumColumns) {
|
||||
addSwatchToRow(row, createBlankSpace(), rowNumber);
|
||||
rowElements++;
|
||||
}
|
||||
addView(row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a swatch to the end of the row for even-numbered rows (starting with row 0),
|
||||
* to the beginning of a row for odd-numbered rows.
|
||||
*/
|
||||
private static void addSwatchToRow(TableRow row, View swatch, int rowNumber) {
|
||||
if (rowNumber % 2 == 0) {
|
||||
row.addView(swatch);
|
||||
} else {
|
||||
row.addView(swatch, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a content description to the specified swatch view. Because the palette get added in a
|
||||
* snaking form, every other row will need to compensate for the fact that the palette are added
|
||||
* in an opposite direction from their left->right/top->bottom order, which is how the system
|
||||
* will arrange them for accessibility purposes.
|
||||
*/
|
||||
private void setSwatchDescription(int rowNumber, int index, int rowElements, boolean selected,
|
||||
View swatch) {
|
||||
int accessibilityIndex;
|
||||
if (rowNumber % 2 == 0) {
|
||||
// We're in a regular-ordered row
|
||||
accessibilityIndex = index;
|
||||
} else {
|
||||
// We're in a backwards-ordered row.
|
||||
int rowMax = ((rowNumber + 1) * mNumColumns);
|
||||
accessibilityIndex = rowMax - rowElements;
|
||||
}
|
||||
|
||||
String description;
|
||||
if (selected) {
|
||||
description = String.format(mDescriptionSelected, accessibilityIndex);
|
||||
} else {
|
||||
description = String.format(mDescription, accessibilityIndex);
|
||||
}
|
||||
swatch.setContentDescription(description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a blank space to fill the row.
|
||||
*/
|
||||
private ImageView createBlankSpace() {
|
||||
ImageView view = new ImageView(getContext());
|
||||
TableRow.LayoutParams params = new TableRow.LayoutParams(mSwatchLength, mSwatchLength);
|
||||
params.setMargins(mMarginSize, mMarginSize, mMarginSize, mMarginSize);
|
||||
view.setLayoutParams(params);
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a color swatch.
|
||||
*/
|
||||
private ColorPickerSwatch createColorSwatch(int color, int selectedColor) {
|
||||
ColorPickerSwatch view = new ColorPickerSwatch(getContext(), color,
|
||||
color == selectedColor, mOnColorSelectedListener);
|
||||
TableRow.LayoutParams params = new TableRow.LayoutParams(mSwatchLength, mSwatchLength);
|
||||
params.setMargins(mMarginSize, mMarginSize, mMarginSize, mMarginSize);
|
||||
view.setLayoutParams(params);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.colorpicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
/**
|
||||
* Creates a circular swatch of a specified color. Adds a checkmark if marked as checked.
|
||||
*/
|
||||
public class ColorPickerSwatch extends FrameLayout implements View.OnClickListener {
|
||||
private int mColor;
|
||||
private ImageView mSwatchImage;
|
||||
private ImageView mCheckmarkImage;
|
||||
private OnColorSelectedListener mOnColorSelectedListener;
|
||||
|
||||
/**
|
||||
* Interface for a callback when a color square is selected.
|
||||
*/
|
||||
public interface OnColorSelectedListener {
|
||||
|
||||
/**
|
||||
* Called when a specific color square has been selected.
|
||||
*/
|
||||
public void onColorSelected(int color);
|
||||
}
|
||||
|
||||
public ColorPickerSwatch(Context context, int color, boolean checked,
|
||||
OnColorSelectedListener listener) {
|
||||
super(context);
|
||||
mColor = color;
|
||||
mOnColorSelectedListener = listener;
|
||||
|
||||
LayoutInflater.from(context).inflate(R.layout.color_picker_swatch, this);
|
||||
mSwatchImage = (ImageView) findViewById(R.id.color_picker_swatch);
|
||||
mCheckmarkImage = (ImageView) findViewById(R.id.color_picker_checkmark);
|
||||
setColor(color);
|
||||
setChecked(checked);
|
||||
setOnClickListener(this);
|
||||
}
|
||||
|
||||
protected void setColor(int color) {
|
||||
Drawable[] colorDrawable = new Drawable[]
|
||||
{getContext().getResources().getDrawable(R.drawable.color_picker_swatch)};
|
||||
mSwatchImage.setImageDrawable(new ColorStateDrawable(colorDrawable, color));
|
||||
}
|
||||
|
||||
private void setChecked(boolean checked) {
|
||||
if (checked) {
|
||||
mCheckmarkImage.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mCheckmarkImage.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mOnColorSelectedListener != null) {
|
||||
mOnColorSelectedListener.onColorSelected(mColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.colorpicker;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
|
||||
/**
|
||||
* A drawable which sets its color filter to a color specified by the user, and changes to a
|
||||
* slightly darker color when pressed or focused.
|
||||
*/
|
||||
public class ColorStateDrawable extends LayerDrawable {
|
||||
|
||||
private static final float PRESSED_STATE_MULTIPLIER = 0.70f;
|
||||
|
||||
private int mColor;
|
||||
|
||||
public ColorStateDrawable(Drawable[] layers, int color) {
|
||||
super(layers);
|
||||
mColor = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onStateChange(int[] states) {
|
||||
boolean pressedOrFocused = false;
|
||||
for (int state : states) {
|
||||
if (state == android.R.attr.state_pressed || state == android.R.attr.state_focused) {
|
||||
pressedOrFocused = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pressedOrFocused) {
|
||||
super.setColorFilter(getPressedColor(mColor), PorterDuff.Mode.SRC_ATOP);
|
||||
} else {
|
||||
super.setColorFilter(mColor, PorterDuff.Mode.SRC_ATOP);
|
||||
}
|
||||
|
||||
return super.onStateChange(states);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a particular color, adjusts its value by a multiplier.
|
||||
*/
|
||||
private static int getPressedColor(int color) {
|
||||
float[] hsv = new float[3];
|
||||
Color.colorToHSV(color, hsv);
|
||||
hsv[2] = hsv[2] * PRESSED_STATE_MULTIPLIER;
|
||||
return Color.HSVToColor(hsv);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStateful() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.colorpicker;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
/**
|
||||
* A color comparator which compares based on hue, saturation, and value.
|
||||
*/
|
||||
public class HsvColorComparator implements Comparator<Integer> {
|
||||
|
||||
@Override
|
||||
public int compare(Integer lhs, Integer rhs) {
|
||||
float[] hsv = new float[3];
|
||||
Color.colorToHSV(lhs, hsv);
|
||||
float hue1 = hsv[0];
|
||||
float sat1 = hsv[1];
|
||||
float val1 = hsv[2];
|
||||
|
||||
float[] hsv2 = new float[3];
|
||||
Color.colorToHSV(rhs, hsv2);
|
||||
float hue2 = hsv2[0];
|
||||
float sat2 = hsv2[1];
|
||||
float val2 = hsv2[2];
|
||||
|
||||
if (hue1 < hue2) {
|
||||
return 1;
|
||||
} else if (hue1 > hue2) {
|
||||
return -1;
|
||||
} else {
|
||||
if (sat1 < sat2) {
|
||||
return 1;
|
||||
} else if (sat1 > sat2) {
|
||||
return -1;
|
||||
} else {
|
||||
if (val1 < val2) {
|
||||
return 1;
|
||||
} else if (val1 > val2) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.util.AttributeSet;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
/**
|
||||
* Fake Button class, used so TextViews can announce themselves as Buttons, for accessibility.
|
||||
*/
|
||||
public class AccessibleLinearLayout extends LinearLayout {
|
||||
|
||||
public AccessibleLinearLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(event);
|
||||
event.setClassName(Button.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
||||
super.onInitializeAccessibilityNodeInfo(info);
|
||||
info.setClassName(Button.class.getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.util.AttributeSet;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
|
||||
/**
|
||||
* Fake Button class, used so TextViews can announce themselves as Buttons, for accessibility.
|
||||
*/
|
||||
public class AccessibleTextView extends TextView {
|
||||
|
||||
public AccessibleTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(event);
|
||||
event.setClassName(Button.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
||||
super.onInitializeAccessibilityNodeInfo(info);
|
||||
info.setClassName(Button.class.getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.android.datetimepicker;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.net.Uri;
|
||||
import android.os.SystemClock;
|
||||
import android.os.Vibrator;
|
||||
import android.provider.Settings;
|
||||
|
||||
/**
|
||||
* A simple utility class to handle haptic feedback.
|
||||
*/
|
||||
public class HapticFeedbackController {
|
||||
private static final int VIBRATE_DELAY_MS = 125;
|
||||
private static final int VIBRATE_LENGTH_MS = 5;
|
||||
|
||||
private static boolean checkGlobalSetting(Context context) {
|
||||
return Settings.System.getInt(context.getContentResolver(),
|
||||
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) == 1;
|
||||
}
|
||||
|
||||
private final Context mContext;
|
||||
private final ContentObserver mContentObserver;
|
||||
|
||||
private Vibrator mVibrator;
|
||||
private boolean mIsGloballyEnabled;
|
||||
private long mLastVibrate;
|
||||
|
||||
public HapticFeedbackController(Context context) {
|
||||
mContext = context;
|
||||
mContentObserver = new ContentObserver(null) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
mIsGloballyEnabled = checkGlobalSetting(mContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to setup the controller.
|
||||
*/
|
||||
public void start() {
|
||||
mVibrator = (Vibrator) mContext.getSystemService(Service.VIBRATOR_SERVICE);
|
||||
|
||||
// Setup a listener for changes in haptic feedback settings
|
||||
mIsGloballyEnabled = checkGlobalSetting(mContext);
|
||||
Uri uri = Settings.System.getUriFor(Settings.System.HAPTIC_FEEDBACK_ENABLED);
|
||||
mContext.getContentResolver().registerContentObserver(uri, false, mContentObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this when you don't need the controller anymore.
|
||||
*/
|
||||
public void stop() {
|
||||
mVibrator = null;
|
||||
mContext.getContentResolver().unregisterContentObserver(mContentObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to vibrate. To prevent this becoming a single continuous vibration, nothing will
|
||||
* happen if we have vibrated very recently.
|
||||
*/
|
||||
public void tryVibrate() {
|
||||
if (mVibrator != null && mIsGloballyEnabled) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
// We want to try to vibrate each individual tick discretely.
|
||||
if (now - mLastVibrate >= VIBRATE_DELAY_MS) {
|
||||
mVibrator.vibrate(VIBRATE_LENGTH_MS);
|
||||
mLastVibrate = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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 java.util.Calendar;
|
||||
|
||||
import android.animation.Keyframe;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.PropertyValuesHolder;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.text.format.Time;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* Utility helper functions for time and date pickers.
|
||||
*/
|
||||
public class Utils {
|
||||
|
||||
public static final int MONDAY_BEFORE_JULIAN_EPOCH = Time.EPOCH_JULIAN_DAY - 3;
|
||||
public static final int PULSE_ANIMATOR_DURATION = 544;
|
||||
|
||||
// Alpha level for time picker selection.
|
||||
public static final int SELECTED_ALPHA = 51;
|
||||
public static final int SELECTED_ALPHA_THEME_DARK = 102;
|
||||
// Alpha level for fully opaque.
|
||||
public static final int FULL_ALPHA = 255;
|
||||
|
||||
|
||||
static final String SHARED_PREFS_NAME = "com.android.calendar_preferences";
|
||||
|
||||
public static boolean isJellybeanOrLater() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to speak the specified text, for accessibility. Only available on JB or later.
|
||||
* @param text Text to announce.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
public static void tryAccessibilityAnnounce(View view, CharSequence text) {
|
||||
if (isJellybeanOrLater() && view != null && text != null) {
|
||||
view.announceForAccessibility(text);
|
||||
}
|
||||
}
|
||||
|
||||
public static int getDaysInMonth(int month, int year) {
|
||||
switch (month) {
|
||||
case Calendar.JANUARY:
|
||||
case Calendar.MARCH:
|
||||
case Calendar.MAY:
|
||||
case Calendar.JULY:
|
||||
case Calendar.AUGUST:
|
||||
case Calendar.OCTOBER:
|
||||
case Calendar.DECEMBER:
|
||||
return 31;
|
||||
case Calendar.APRIL:
|
||||
case Calendar.JUNE:
|
||||
case Calendar.SEPTEMBER:
|
||||
case Calendar.NOVEMBER:
|
||||
return 30;
|
||||
case Calendar.FEBRUARY:
|
||||
return (year % 4 == 0) ? 29 : 28;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid Month");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a number of weeks since the epoch and calculates the Julian day of
|
||||
* the Monday for that week.
|
||||
*
|
||||
* This assumes that the week containing the {@link Time#EPOCH_JULIAN_DAY}
|
||||
* is considered week 0. It returns the Julian day for the Monday
|
||||
* {@code week} weeks after the Monday of the week containing the epoch.
|
||||
*
|
||||
* @param week Number of weeks since the epoch
|
||||
* @return The julian day for the Monday of the given week since the epoch
|
||||
*/
|
||||
public static int getJulianMondayFromWeeksSinceEpoch(int week) {
|
||||
return MONDAY_BEFORE_JULIAN_EPOCH + week * 7;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the week since {@link Time#EPOCH_JULIAN_DAY} (Jan 1, 1970)
|
||||
* adjusted for first day of week.
|
||||
*
|
||||
* This takes a julian day and the week start day and calculates which
|
||||
* week since {@link Time#EPOCH_JULIAN_DAY} that day occurs in, starting
|
||||
* at 0. *Do not* use this to compute the ISO week number for the year.
|
||||
*
|
||||
* @param julianDay The julian day to calculate the week number for
|
||||
* @param firstDayOfWeek Which week day is the first day of the week,
|
||||
* see {@link Time#SUNDAY}
|
||||
* @return Weeks since the epoch
|
||||
*/
|
||||
public static int getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek) {
|
||||
int diff = Time.THURSDAY - firstDayOfWeek;
|
||||
if (diff < 0) {
|
||||
diff += 7;
|
||||
}
|
||||
int refDay = Time.EPOCH_JULIAN_DAY - diff;
|
||||
return (julianDay - refDay) / 7;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an animator to pulsate a view in place.
|
||||
* @param labelToAnimate the view to pulsate.
|
||||
* @return The animator object. Use .start() to begin.
|
||||
*/
|
||||
public static ObjectAnimator getPulseAnimator(View labelToAnimate, float decreaseRatio,
|
||||
float increaseRatio) {
|
||||
Keyframe k0 = Keyframe.ofFloat(0f, 1f);
|
||||
Keyframe k1 = Keyframe.ofFloat(0.275f, decreaseRatio);
|
||||
Keyframe k2 = Keyframe.ofFloat(0.69f, increaseRatio);
|
||||
Keyframe k3 = Keyframe.ofFloat(1f, 1f);
|
||||
|
||||
PropertyValuesHolder scaleX = PropertyValuesHolder.ofKeyframe("scaleX", k0, k1, k2, k3);
|
||||
PropertyValuesHolder scaleY = PropertyValuesHolder.ofKeyframe("scaleY", k0, k1, k2, k3);
|
||||
ObjectAnimator pulseAnimator =
|
||||
ObjectAnimator.ofPropertyValuesHolder(labelToAnimate, scaleX, scaleY);
|
||||
pulseAnimator.setDuration(PULSE_ANIMATOR_DURATION);
|
||||
|
||||
return pulseAnimator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.widget.ViewAnimator;
|
||||
|
||||
public class AccessibleDateAnimator extends ViewAnimator {
|
||||
private long mDateMillis;
|
||||
|
||||
public AccessibleDateAnimator(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public void setDateMillis(long dateMillis) {
|
||||
mDateMillis = dateMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Announce the currently-selected date when launched.
|
||||
*/
|
||||
@Override
|
||||
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
|
||||
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
|
||||
// Clear the event's current text so that only the current date will be spoken.
|
||||
event.getText().clear();
|
||||
int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
|
||||
DateUtils.FORMAT_SHOW_WEEKDAY;
|
||||
|
||||
String dateString = DateUtils.formatDateTime(getContext(), mDateMillis, flags);
|
||||
event.getText().add(dateString);
|
||||
return true;
|
||||
}
|
||||
return super.dispatchPopulateAccessibilityEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import com.android.datetimepicker.date.DatePickerDialog.OnDateChangedListener;
|
||||
import com.android.datetimepicker.date.MonthAdapter.CalendarDay;
|
||||
|
||||
/**
|
||||
* Controller class to communicate among the various components of the date picker dialog.
|
||||
*/
|
||||
public interface DatePickerController {
|
||||
|
||||
void onYearSelected(int year);
|
||||
|
||||
void onDayOfMonthSelected(int year, int month, int day);
|
||||
|
||||
void registerOnDateChangedListener(OnDateChangedListener listener);
|
||||
|
||||
void unregisterOnDateChangedListener(OnDateChangedListener listener);
|
||||
|
||||
CalendarDay getSelectedDay();
|
||||
|
||||
int getFirstDayOfWeek();
|
||||
|
||||
int getMinYear();
|
||||
|
||||
int getMaxYear();
|
||||
|
||||
void tryVibrate();
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.app.DialogFragment;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.datetimepicker.HapticFeedbackController;
|
||||
import com.android.datetimepicker.Utils;
|
||||
import com.android.datetimepicker.date.MonthAdapter.CalendarDay;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Dialog allowing users to select a date.
|
||||
*/
|
||||
public class DatePickerDialog extends DialogFragment implements
|
||||
OnClickListener, DatePickerController {
|
||||
|
||||
private static final String TAG = "DatePickerDialog";
|
||||
|
||||
private static final int UNINITIALIZED = -1;
|
||||
private static final int MONTH_AND_DAY_VIEW = 0;
|
||||
private static final int YEAR_VIEW = 1;
|
||||
|
||||
private static final String KEY_SELECTED_YEAR = "year";
|
||||
private static final String KEY_SELECTED_MONTH = "month";
|
||||
private static final String KEY_SELECTED_DAY = "day";
|
||||
private static final String KEY_LIST_POSITION = "list_position";
|
||||
private static final String KEY_WEEK_START = "week_start";
|
||||
private static final String KEY_YEAR_START = "year_start";
|
||||
private static final String KEY_YEAR_END = "year_end";
|
||||
private static final String KEY_CURRENT_VIEW = "current_view";
|
||||
private static final String KEY_LIST_POSITION_OFFSET = "list_position_offset";
|
||||
|
||||
private static final int DEFAULT_START_YEAR = 1900;
|
||||
private static final int DEFAULT_END_YEAR = 2100;
|
||||
|
||||
private static final int ANIMATION_DURATION = 300;
|
||||
private static final int ANIMATION_DELAY = 500;
|
||||
|
||||
private static SimpleDateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy", Locale.getDefault());
|
||||
private static SimpleDateFormat DAY_FORMAT = new SimpleDateFormat("dd", Locale.getDefault());
|
||||
|
||||
private final Calendar mCalendar = Calendar.getInstance();
|
||||
private OnDateSetListener mCallBack;
|
||||
private HashSet<OnDateChangedListener> mListeners = new HashSet<OnDateChangedListener>();
|
||||
|
||||
private AccessibleDateAnimator mAnimator;
|
||||
|
||||
private TextView mDayOfWeekView;
|
||||
private LinearLayout mMonthAndDayView;
|
||||
private TextView mSelectedMonthTextView;
|
||||
private TextView mSelectedDayTextView;
|
||||
private TextView mYearView;
|
||||
private DayPickerView mDayPickerView;
|
||||
private YearPickerView mYearPickerView;
|
||||
|
||||
private int mCurrentView = UNINITIALIZED;
|
||||
|
||||
private int mWeekStart = mCalendar.getFirstDayOfWeek();
|
||||
private int mMinYear = DEFAULT_START_YEAR;
|
||||
private int mMaxYear = DEFAULT_END_YEAR;
|
||||
|
||||
private HapticFeedbackController mHapticFeedbackController;
|
||||
|
||||
private boolean mDelayAnimation = true;
|
||||
|
||||
// Accessibility strings.
|
||||
private String mDayPickerDescription;
|
||||
private String mSelectDay;
|
||||
private String mYearPickerDescription;
|
||||
private String mSelectYear;
|
||||
|
||||
/**
|
||||
* The callback used to indicate the user is done filling in the date.
|
||||
*/
|
||||
public interface OnDateSetListener {
|
||||
|
||||
/**
|
||||
* @param view The view associated with this listener.
|
||||
* @param year The year that was set.
|
||||
* @param monthOfYear The month that was set (0-11) for compatibility
|
||||
* with {@link java.util.Calendar}.
|
||||
* @param dayOfMonth The day of the month that was set.
|
||||
*/
|
||||
void onDateSet(DatePickerDialog dialog, int year, int monthOfYear, int dayOfMonth);
|
||||
|
||||
void onDateCleared(DatePickerDialog dialog);
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback used to notify other date picker components of a change in selected date.
|
||||
*/
|
||||
public interface OnDateChangedListener {
|
||||
|
||||
public void onDateChanged();
|
||||
}
|
||||
|
||||
|
||||
public DatePickerDialog() {
|
||||
// Empty constructor required for dialog fragment.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callBack How the parent is notified that the date is set.
|
||||
* @param year The initial year of the dialog.
|
||||
* @param monthOfYear The initial month of the dialog.
|
||||
* @param dayOfMonth The initial day of the dialog.
|
||||
*/
|
||||
public static DatePickerDialog newInstance(OnDateSetListener callBack, int year,
|
||||
int monthOfYear,
|
||||
int dayOfMonth) {
|
||||
DatePickerDialog ret = new DatePickerDialog();
|
||||
ret.initialize(callBack, year, monthOfYear, dayOfMonth);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void initialize(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) {
|
||||
mCallBack = callBack;
|
||||
mCalendar.set(Calendar.YEAR, year);
|
||||
mCalendar.set(Calendar.MONTH, monthOfYear);
|
||||
mCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
final Activity activity = getActivity();
|
||||
activity.getWindow().setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
||||
if (savedInstanceState != null) {
|
||||
mCalendar.set(Calendar.YEAR, savedInstanceState.getInt(KEY_SELECTED_YEAR));
|
||||
mCalendar.set(Calendar.MONTH, savedInstanceState.getInt(KEY_SELECTED_MONTH));
|
||||
mCalendar.set(Calendar.DAY_OF_MONTH, savedInstanceState.getInt(KEY_SELECTED_DAY));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt(KEY_SELECTED_YEAR, mCalendar.get(Calendar.YEAR));
|
||||
outState.putInt(KEY_SELECTED_MONTH, mCalendar.get(Calendar.MONTH));
|
||||
outState.putInt(KEY_SELECTED_DAY, mCalendar.get(Calendar.DAY_OF_MONTH));
|
||||
outState.putInt(KEY_WEEK_START, mWeekStart);
|
||||
outState.putInt(KEY_YEAR_START, mMinYear);
|
||||
outState.putInt(KEY_YEAR_END, mMaxYear);
|
||||
outState.putInt(KEY_CURRENT_VIEW, mCurrentView);
|
||||
int listPosition = -1;
|
||||
if (mCurrentView == MONTH_AND_DAY_VIEW) {
|
||||
listPosition = mDayPickerView.getMostVisiblePosition();
|
||||
} else if (mCurrentView == YEAR_VIEW) {
|
||||
listPosition = mYearPickerView.getFirstVisiblePosition();
|
||||
outState.putInt(KEY_LIST_POSITION_OFFSET, mYearPickerView.getFirstPositionOffset());
|
||||
}
|
||||
outState.putInt(KEY_LIST_POSITION, listPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
Log.d(TAG, "onCreateView: ");
|
||||
getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
View view = inflater.inflate(R.layout.date_picker_dialog, null);
|
||||
|
||||
mDayOfWeekView = (TextView) view.findViewById(R.id.date_picker_header);
|
||||
mMonthAndDayView = (LinearLayout) view.findViewById(R.id.date_picker_month_and_day);
|
||||
mMonthAndDayView.setOnClickListener(this);
|
||||
mSelectedMonthTextView = (TextView) view.findViewById(R.id.date_picker_month);
|
||||
mSelectedDayTextView = (TextView) view.findViewById(R.id.date_picker_day);
|
||||
mYearView = (TextView) view.findViewById(R.id.date_picker_year);
|
||||
mYearView.setOnClickListener(this);
|
||||
|
||||
int listPosition = -1;
|
||||
int listPositionOffset = 0;
|
||||
int currentView = MONTH_AND_DAY_VIEW;
|
||||
if (savedInstanceState != null) {
|
||||
mWeekStart = savedInstanceState.getInt(KEY_WEEK_START);
|
||||
mMinYear = savedInstanceState.getInt(KEY_YEAR_START);
|
||||
mMaxYear = savedInstanceState.getInt(KEY_YEAR_END);
|
||||
currentView = savedInstanceState.getInt(KEY_CURRENT_VIEW);
|
||||
listPosition = savedInstanceState.getInt(KEY_LIST_POSITION);
|
||||
listPositionOffset = savedInstanceState.getInt(KEY_LIST_POSITION_OFFSET);
|
||||
}
|
||||
|
||||
final Activity activity = getActivity();
|
||||
mDayPickerView = new SimpleDayPickerView(activity, this);
|
||||
mYearPickerView = new YearPickerView(activity, this);
|
||||
|
||||
Resources res = getResources();
|
||||
mDayPickerDescription = res.getString(R.string.day_picker_description);
|
||||
mSelectDay = res.getString(R.string.select_day);
|
||||
mYearPickerDescription = res.getString(R.string.year_picker_description);
|
||||
mSelectYear = res.getString(R.string.select_year);
|
||||
|
||||
mAnimator = (AccessibleDateAnimator) view.findViewById(R.id.animator);
|
||||
mAnimator.addView(mDayPickerView);
|
||||
mAnimator.addView(mYearPickerView);
|
||||
mAnimator.setDateMillis(mCalendar.getTimeInMillis());
|
||||
// TODO: Replace with animation decided upon by the design team.
|
||||
Animation animation = new AlphaAnimation(0.0f, 1.0f);
|
||||
animation.setDuration(ANIMATION_DURATION);
|
||||
mAnimator.setInAnimation(animation);
|
||||
// TODO: Replace with animation decided upon by the design team.
|
||||
Animation animation2 = new AlphaAnimation(1.0f, 0.0f);
|
||||
animation2.setDuration(ANIMATION_DURATION);
|
||||
mAnimator.setOutAnimation(animation2);
|
||||
|
||||
Button mDoneButton = (Button) view.findViewById(R.id.done);
|
||||
mDoneButton.setOnClickListener(new OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v)
|
||||
{
|
||||
tryVibrate();
|
||||
if (mCallBack != null)
|
||||
{
|
||||
mCallBack.onDateSet(DatePickerDialog.this, mCalendar.get(Calendar.YEAR),
|
||||
mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH));
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
Button mClearButton = (Button) view.findViewById(R.id.clear);
|
||||
mClearButton.setOnClickListener(new OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v)
|
||||
{
|
||||
tryVibrate();
|
||||
if (mCallBack != null)
|
||||
{
|
||||
mCallBack.onDateCleared(DatePickerDialog.this);
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
updateDisplay(false);
|
||||
setCurrentView(currentView);
|
||||
|
||||
if (listPosition != -1) {
|
||||
if (currentView == MONTH_AND_DAY_VIEW) {
|
||||
mDayPickerView.postSetSelection(listPosition);
|
||||
} else if (currentView == YEAR_VIEW) {
|
||||
mYearPickerView.postSetSelectionFromTop(listPosition, listPositionOffset);
|
||||
}
|
||||
}
|
||||
|
||||
mHapticFeedbackController = new HapticFeedbackController(activity);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
mHapticFeedbackController.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
mHapticFeedbackController.stop();
|
||||
}
|
||||
|
||||
private void setCurrentView(final int viewIndex) {
|
||||
long millis = mCalendar.getTimeInMillis();
|
||||
|
||||
switch (viewIndex) {
|
||||
case MONTH_AND_DAY_VIEW:
|
||||
ObjectAnimator pulseAnimator = Utils.getPulseAnimator(mMonthAndDayView, 0.9f,
|
||||
1.05f);
|
||||
if (mDelayAnimation) {
|
||||
pulseAnimator.setStartDelay(ANIMATION_DELAY);
|
||||
mDelayAnimation = false;
|
||||
}
|
||||
mDayPickerView.onDateChanged();
|
||||
if (mCurrentView != viewIndex) {
|
||||
mMonthAndDayView.setSelected(true);
|
||||
mYearView.setSelected(false);
|
||||
mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW);
|
||||
mCurrentView = viewIndex;
|
||||
}
|
||||
pulseAnimator.start();
|
||||
|
||||
int flags = DateUtils.FORMAT_SHOW_DATE;
|
||||
String dayString = DateUtils.formatDateTime(getActivity(), millis, flags);
|
||||
mAnimator.setContentDescription(mDayPickerDescription+": "+dayString);
|
||||
Utils.tryAccessibilityAnnounce(mAnimator, mSelectDay);
|
||||
break;
|
||||
case YEAR_VIEW:
|
||||
pulseAnimator = Utils.getPulseAnimator(mYearView, 0.85f, 1.1f);
|
||||
if (mDelayAnimation) {
|
||||
pulseAnimator.setStartDelay(ANIMATION_DELAY);
|
||||
mDelayAnimation = false;
|
||||
}
|
||||
mYearPickerView.onDateChanged();
|
||||
if (mCurrentView != viewIndex) {
|
||||
mMonthAndDayView.setSelected(false);
|
||||
mYearView.setSelected(true);
|
||||
mAnimator.setDisplayedChild(YEAR_VIEW);
|
||||
mCurrentView = viewIndex;
|
||||
}
|
||||
pulseAnimator.start();
|
||||
|
||||
CharSequence yearString = YEAR_FORMAT.format(millis);
|
||||
mAnimator.setContentDescription(mYearPickerDescription+": "+yearString);
|
||||
Utils.tryAccessibilityAnnounce(mAnimator, mSelectYear);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDisplay(boolean announce) {
|
||||
if (mDayOfWeekView != null) {
|
||||
mDayOfWeekView.setText(mCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG,
|
||||
Locale.getDefault()).toUpperCase(Locale.getDefault()));
|
||||
}
|
||||
|
||||
mSelectedMonthTextView.setText(mCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT,
|
||||
Locale.getDefault()).toUpperCase(Locale.getDefault()));
|
||||
mSelectedDayTextView.setText(DAY_FORMAT.format(mCalendar.getTime()));
|
||||
mYearView.setText(YEAR_FORMAT.format(mCalendar.getTime()));
|
||||
|
||||
// Accessibility.
|
||||
long millis = mCalendar.getTimeInMillis();
|
||||
mAnimator.setDateMillis(millis);
|
||||
int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR;
|
||||
String monthAndDayText = DateUtils.formatDateTime(getActivity(), millis, flags);
|
||||
mMonthAndDayView.setContentDescription(monthAndDayText);
|
||||
|
||||
if (announce) {
|
||||
flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
|
||||
String fullDateText = DateUtils.formatDateTime(getActivity(), millis, flags);
|
||||
Utils.tryAccessibilityAnnounce(mAnimator, fullDateText);
|
||||
}
|
||||
}
|
||||
|
||||
public void setFirstDayOfWeek(int startOfWeek) {
|
||||
if (startOfWeek < Calendar.SUNDAY || startOfWeek > Calendar.SATURDAY) {
|
||||
throw new IllegalArgumentException("Value must be between Calendar.SUNDAY and " +
|
||||
"Calendar.SATURDAY");
|
||||
}
|
||||
mWeekStart = startOfWeek;
|
||||
if (mDayPickerView != null) {
|
||||
mDayPickerView.onChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void setYearRange(int startYear, int endYear) {
|
||||
if (endYear <= startYear) {
|
||||
throw new IllegalArgumentException("Year end must be larger than year start");
|
||||
}
|
||||
mMinYear = startYear;
|
||||
mMaxYear = endYear;
|
||||
if (mDayPickerView != null) {
|
||||
mDayPickerView.onChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnDateSetListener(OnDateSetListener listener) {
|
||||
mCallBack = listener;
|
||||
}
|
||||
|
||||
// If the newly selected month / year does not contain the currently selected day number,
|
||||
// change the selected day number to the last day of the selected month or year.
|
||||
// e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
|
||||
// e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
|
||||
private void adjustDayInMonthIfNeeded(int month, int year) {
|
||||
int day = mCalendar.get(Calendar.DAY_OF_MONTH);
|
||||
int daysInMonth = Utils.getDaysInMonth(month, year);
|
||||
if (day > daysInMonth) {
|
||||
mCalendar.set(Calendar.DAY_OF_MONTH, daysInMonth);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
tryVibrate();
|
||||
if (v.getId() == R.id.date_picker_year) {
|
||||
setCurrentView(YEAR_VIEW);
|
||||
} else if (v.getId() == R.id.date_picker_month_and_day) {
|
||||
setCurrentView(MONTH_AND_DAY_VIEW);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onYearSelected(int year) {
|
||||
adjustDayInMonthIfNeeded(mCalendar.get(Calendar.MONTH), year);
|
||||
mCalendar.set(Calendar.YEAR, year);
|
||||
updatePickers();
|
||||
setCurrentView(MONTH_AND_DAY_VIEW);
|
||||
updateDisplay(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDayOfMonthSelected(int year, int month, int day) {
|
||||
mCalendar.set(Calendar.YEAR, year);
|
||||
mCalendar.set(Calendar.MONTH, month);
|
||||
mCalendar.set(Calendar.DAY_OF_MONTH, day);
|
||||
updatePickers();
|
||||
updateDisplay(true);
|
||||
}
|
||||
|
||||
private void updatePickers() {
|
||||
Iterator<OnDateChangedListener> iterator = mListeners.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
iterator.next().onDateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public CalendarDay getSelectedDay() {
|
||||
return new CalendarDay(mCalendar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinYear() {
|
||||
return mMinYear;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxYear() {
|
||||
return mMaxYear;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirstDayOfWeek() {
|
||||
return mWeekStart;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerOnDateChangedListener(OnDateChangedListener listener) {
|
||||
mListeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterOnDateChangedListener(OnDateChangedListener listener) {
|
||||
mListeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tryVibrate() {
|
||||
mHapticFeedbackController.tryVibrate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.AbsListView.OnScrollListener;
|
||||
import android.widget.ListView;
|
||||
|
||||
import com.android.datetimepicker.Utils;
|
||||
import com.android.datetimepicker.date.DatePickerDialog.OnDateChangedListener;
|
||||
import com.android.datetimepicker.date.MonthAdapter.CalendarDay;
|
||||
|
||||
/**
|
||||
* This displays a list of months in a calendar format with selectable days.
|
||||
*/
|
||||
public abstract class DayPickerView extends ListView implements OnScrollListener,
|
||||
OnDateChangedListener {
|
||||
|
||||
private static final String TAG = "MonthFragment";
|
||||
|
||||
// Affects when the month selection will change while scrolling up
|
||||
protected static final int SCROLL_HYST_WEEKS = 2;
|
||||
// How long the GoTo fling animation should last
|
||||
protected static final int GOTO_SCROLL_DURATION = 250;
|
||||
// How long to wait after receiving an onScrollStateChanged notification
|
||||
// before acting on it
|
||||
protected static final int SCROLL_CHANGE_DELAY = 40;
|
||||
// The number of days to display in each week
|
||||
public static final int DAYS_PER_WEEK = 7;
|
||||
public static int LIST_TOP_OFFSET = -1; // so that the top line will be
|
||||
// under the separator
|
||||
// You can override these numbers to get a different appearance
|
||||
protected int mNumWeeks = 6;
|
||||
protected boolean mShowWeekNumber = false;
|
||||
protected int mDaysPerWeek = 7;
|
||||
private static SimpleDateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy", Locale.getDefault());
|
||||
|
||||
// These affect the scroll speed and feel
|
||||
protected float mFriction = 1.0f;
|
||||
|
||||
protected Context mContext;
|
||||
protected Handler mHandler;
|
||||
|
||||
// highlighted time
|
||||
protected CalendarDay mSelectedDay = new CalendarDay();
|
||||
protected MonthAdapter mAdapter;
|
||||
|
||||
protected CalendarDay mTempDay = new CalendarDay();
|
||||
|
||||
// When the week starts; numbered like Time.<WEEKDAY> (e.g. SUNDAY=0).
|
||||
protected int mFirstDayOfWeek;
|
||||
// The last name announced by accessibility
|
||||
protected CharSequence mPrevMonthName;
|
||||
// which month should be displayed/highlighted [0-11]
|
||||
protected int mCurrentMonthDisplayed;
|
||||
// used for tracking during a scroll
|
||||
protected long mPreviousScrollPosition;
|
||||
// used for tracking what state listview is in
|
||||
protected int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE;
|
||||
// used for tracking what state listview is in
|
||||
protected int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE;
|
||||
|
||||
private DatePickerController mController;
|
||||
private boolean mPerformingScroll;
|
||||
|
||||
public DayPickerView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public DayPickerView(Context context, DatePickerController controller) {
|
||||
super(context);
|
||||
init(context);
|
||||
setController(controller);
|
||||
}
|
||||
|
||||
public void setController(DatePickerController controller) {
|
||||
mController = controller;
|
||||
mController.registerOnDateChangedListener(this);
|
||||
refreshAdapter();
|
||||
onDateChanged();
|
||||
}
|
||||
|
||||
public void init(Context context) {
|
||||
mHandler = new Handler();
|
||||
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
|
||||
setDrawSelectorOnTop(false);
|
||||
|
||||
mContext = context;
|
||||
setUpListView();
|
||||
}
|
||||
|
||||
public void onChange() {
|
||||
refreshAdapter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new adapter if necessary and sets up its parameters. Override
|
||||
* this method to provide a custom adapter.
|
||||
*/
|
||||
protected void refreshAdapter() {
|
||||
if (mAdapter == null) {
|
||||
mAdapter = createMonthAdapter(getContext(), mController);
|
||||
} else {
|
||||
mAdapter.setSelectedDay(mSelectedDay);
|
||||
}
|
||||
// refresh the view with the new parameters
|
||||
setAdapter(mAdapter);
|
||||
}
|
||||
|
||||
public abstract MonthAdapter createMonthAdapter(Context context,
|
||||
DatePickerController controller);
|
||||
|
||||
/*
|
||||
* Sets all the required fields for the list view. Override this method to
|
||||
* set a different list view behavior.
|
||||
*/
|
||||
protected void setUpListView() {
|
||||
// Transparent background on scroll
|
||||
setCacheColorHint(0);
|
||||
// No dividers
|
||||
setDivider(null);
|
||||
// Items are clickable
|
||||
setItemsCanFocus(true);
|
||||
// The thumb gets in the way, so disable it
|
||||
setFastScrollEnabled(false);
|
||||
setVerticalScrollBarEnabled(false);
|
||||
setOnScrollListener(this);
|
||||
setFadingEdgeLength(0);
|
||||
// Make the scrolling behavior nicer
|
||||
setFriction(ViewConfiguration.getScrollFriction() * mFriction);
|
||||
}
|
||||
|
||||
/**
|
||||
* This moves to the specified time in the view. If the time is not already
|
||||
* in range it will move the list so that the first of the month containing
|
||||
* the time is at the top of the view. If the new time is already in view
|
||||
* the list will not be scrolled unless forceScroll is true. This time may
|
||||
* optionally be highlighted as selected as well.
|
||||
*
|
||||
* @param time The time to move to
|
||||
* @param animate Whether to scroll to the given time or just redraw at the
|
||||
* new location
|
||||
* @param setSelected Whether to set the given time as selected
|
||||
* @param forceScroll Whether to recenter even if the time is already
|
||||
* visible
|
||||
* @return Whether or not the view animated to the new location
|
||||
*/
|
||||
public boolean goTo(CalendarDay day, boolean animate, boolean setSelected, boolean forceScroll) {
|
||||
|
||||
// Set the selected day
|
||||
if (setSelected) {
|
||||
mSelectedDay.set(day);
|
||||
}
|
||||
|
||||
mTempDay.set(day);
|
||||
final int position = (day.year - mController.getMinYear())
|
||||
* MonthAdapter.MONTHS_IN_YEAR + day.month;
|
||||
|
||||
View child;
|
||||
int i = 0;
|
||||
int top = 0;
|
||||
// Find a child that's completely in the view
|
||||
do {
|
||||
child = getChildAt(i++);
|
||||
if (child == null) {
|
||||
break;
|
||||
}
|
||||
top = child.getTop();
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "child at " + (i - 1) + " has top " + top);
|
||||
}
|
||||
} while (top < 0);
|
||||
|
||||
// Compute the first and last position visible
|
||||
int selectedPosition;
|
||||
if (child != null) {
|
||||
selectedPosition = getPositionForView(child);
|
||||
} else {
|
||||
selectedPosition = 0;
|
||||
}
|
||||
|
||||
if (setSelected) {
|
||||
mAdapter.setSelectedDay(mSelectedDay);
|
||||
}
|
||||
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "GoTo position " + position);
|
||||
}
|
||||
// Check if the selected day is now outside of our visible range
|
||||
// and if so scroll to the month that contains it
|
||||
if (position != selectedPosition || forceScroll) {
|
||||
setMonthDisplayed(mTempDay);
|
||||
mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING;
|
||||
if (animate) {
|
||||
smoothScrollToPositionFromTop(
|
||||
position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION);
|
||||
return true;
|
||||
} else {
|
||||
postSetSelection(position);
|
||||
}
|
||||
} else if (setSelected) {
|
||||
setMonthDisplayed(mSelectedDay);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void postSetSelection(final int position) {
|
||||
clearFocus();
|
||||
post(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
setSelection(position);
|
||||
}
|
||||
});
|
||||
onScrollStateChanged(this, OnScrollListener.SCROLL_STATE_IDLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the title and selected month if the view has moved to a new
|
||||
* month.
|
||||
*/
|
||||
@Override
|
||||
public void onScroll(
|
||||
AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
|
||||
MonthView child = (MonthView) view.getChildAt(0);
|
||||
if (child == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Figure out where we are
|
||||
long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom();
|
||||
mPreviousScrollPosition = currScroll;
|
||||
mPreviousScrollState = mCurrentScrollState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the month displayed at the top of this view based on time. Override
|
||||
* to add custom events when the title is changed.
|
||||
*/
|
||||
protected void setMonthDisplayed(CalendarDay date) {
|
||||
mCurrentMonthDisplayed = date.month;
|
||||
invalidateViews();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollStateChanged(AbsListView view, int scrollState) {
|
||||
// use a post to prevent re-entering onScrollStateChanged before it
|
||||
// exits
|
||||
mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
|
||||
}
|
||||
|
||||
protected ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable();
|
||||
|
||||
protected class ScrollStateRunnable implements Runnable {
|
||||
private int mNewState;
|
||||
|
||||
/**
|
||||
* Sets up the runnable with a short delay in case the scroll state
|
||||
* immediately changes again.
|
||||
*
|
||||
* @param view The list view that changed state
|
||||
* @param scrollState The new state it changed to
|
||||
*/
|
||||
public void doScrollStateChange(AbsListView view, int scrollState) {
|
||||
mHandler.removeCallbacks(this);
|
||||
mNewState = scrollState;
|
||||
mHandler.postDelayed(this, SCROLL_CHANGE_DELAY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mCurrentScrollState = mNewState;
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG,
|
||||
"new scroll state: " + mNewState + " old state: " + mPreviousScrollState);
|
||||
}
|
||||
// Fix the position after a scroll or a fling ends
|
||||
if (mNewState == OnScrollListener.SCROLL_STATE_IDLE
|
||||
&& mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE
|
||||
&& mPreviousScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
|
||||
mPreviousScrollState = mNewState;
|
||||
int i = 0;
|
||||
View child = getChildAt(i);
|
||||
while (child != null && child.getBottom() <= 0) {
|
||||
child = getChildAt(++i);
|
||||
}
|
||||
if (child == null) {
|
||||
// The view is no longer visible, just return
|
||||
return;
|
||||
}
|
||||
int firstPosition = getFirstVisiblePosition();
|
||||
int lastPosition = getLastVisiblePosition();
|
||||
boolean scroll = firstPosition != 0 && lastPosition != getCount() - 1;
|
||||
final int top = child.getTop();
|
||||
final int bottom = child.getBottom();
|
||||
final int midpoint = getHeight() / 2;
|
||||
if (scroll && top < LIST_TOP_OFFSET) {
|
||||
if (bottom > midpoint) {
|
||||
smoothScrollBy(top, GOTO_SCROLL_DURATION);
|
||||
} else {
|
||||
smoothScrollBy(bottom, GOTO_SCROLL_DURATION);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mPreviousScrollState = mNewState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the position of the view that is most prominently displayed within the list view.
|
||||
*/
|
||||
public int getMostVisiblePosition() {
|
||||
final int firstPosition = getFirstVisiblePosition();
|
||||
final int height = getHeight();
|
||||
|
||||
int maxDisplayedHeight = 0;
|
||||
int mostVisibleIndex = 0;
|
||||
int i=0;
|
||||
int bottom = 0;
|
||||
while (bottom < height) {
|
||||
View child = getChildAt(i);
|
||||
if (child == null) {
|
||||
break;
|
||||
}
|
||||
bottom = child.getBottom();
|
||||
int displayedHeight = Math.min(bottom, height) - Math.max(0, child.getTop());
|
||||
if (displayedHeight > maxDisplayedHeight) {
|
||||
mostVisibleIndex = i;
|
||||
maxDisplayedHeight = displayedHeight;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return firstPosition + mostVisibleIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDateChanged() {
|
||||
goTo(mController.getSelectedDay(), false, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to return the date that has accessibility focus.
|
||||
*
|
||||
* @return The date that has accessibility focus, or {@code null} if no date
|
||||
* has focus.
|
||||
*/
|
||||
private CalendarDay findAccessibilityFocus() {
|
||||
final int childCount = getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = getChildAt(i);
|
||||
if (child instanceof MonthView) {
|
||||
final CalendarDay focus = ((MonthView) child).getAccessibilityFocus();
|
||||
if (focus != null) {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
// Clear focus to avoid ListView bug in Jelly Bean MR1.
|
||||
((MonthView) child).clearAccessibilityFocus();
|
||||
}
|
||||
return focus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to restore accessibility focus to a given date. No-op if
|
||||
* {@code day} is {@code null}.
|
||||
*
|
||||
* @param day The date that should receive accessibility focus
|
||||
* @return {@code true} if focus was restored
|
||||
*/
|
||||
private boolean restoreAccessibilityFocus(CalendarDay day) {
|
||||
if (day == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int childCount = getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = getChildAt(i);
|
||||
if (child instanceof MonthView) {
|
||||
if (((MonthView) child).restoreAccessibilityFocus(day)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
final CalendarDay focusedDay = findAccessibilityFocus();
|
||||
super.layoutChildren();
|
||||
if (mPerformingScroll) {
|
||||
mPerformingScroll = false;
|
||||
} else {
|
||||
restoreAccessibilityFocus(focusedDay);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(event);
|
||||
event.setItemCount(-1);
|
||||
}
|
||||
|
||||
private static String getMonthAndYearString(CalendarDay day) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.set(day.year, day.month, day.day);
|
||||
|
||||
StringBuffer sbuf = new StringBuffer();
|
||||
sbuf.append(cal.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault()));
|
||||
sbuf.append(" ");
|
||||
sbuf.append(YEAR_FORMAT.format(cal.getTime()));
|
||||
return sbuf.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Necessary for accessibility, to ensure we support "scrolling" forward and backward
|
||||
* in the month list.
|
||||
*/
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
||||
super.onInitializeAccessibilityNodeInfo(info);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
|
||||
}
|
||||
|
||||
/**
|
||||
* When scroll forward/backward events are received, announce the newly scrolled-to month.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
public boolean performAccessibilityAction(int action, Bundle arguments) {
|
||||
if (action != AccessibilityNodeInfo.ACTION_SCROLL_FORWARD &&
|
||||
action != AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
|
||||
return super.performAccessibilityAction(action, arguments);
|
||||
}
|
||||
|
||||
// Figure out what month is showing.
|
||||
int firstVisiblePosition = getFirstVisiblePosition();
|
||||
int month = firstVisiblePosition % 12;
|
||||
int year = firstVisiblePosition / 12 + mController.getMinYear();
|
||||
CalendarDay day = new CalendarDay(year, month, 1);
|
||||
|
||||
// Scroll either forward or backward one month.
|
||||
if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
|
||||
day.month++;
|
||||
if (day.month == 12) {
|
||||
day.month = 0;
|
||||
day.year++;
|
||||
}
|
||||
} else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
|
||||
View firstVisibleView = getChildAt(0);
|
||||
// If the view is fully visible, jump one month back. Otherwise, we'll just jump
|
||||
// to the first day of first visible month.
|
||||
if (firstVisibleView != null && firstVisibleView.getTop() >= -1) {
|
||||
// There's an off-by-one somewhere, so the top of the first visible item will
|
||||
// actually be -1 when it's at the exact top.
|
||||
day.month--;
|
||||
if (day.month == -1) {
|
||||
day.month = 11;
|
||||
day.year--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go to that month.
|
||||
Utils.tryAccessibilityAnnounce(this, getMonthAndYearString(day));
|
||||
goTo(day, true, false, true);
|
||||
mPerformingScroll = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.text.format.Time;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView.LayoutParams;
|
||||
import android.widget.BaseAdapter;
|
||||
|
||||
import com.android.datetimepicker.date.MonthView.OnDayClickListener;
|
||||
|
||||
/**
|
||||
* An adapter for a list of {@link MonthView} items.
|
||||
*/
|
||||
public abstract class MonthAdapter extends BaseAdapter implements OnDayClickListener {
|
||||
|
||||
private static final String TAG = "SimpleMonthAdapter";
|
||||
|
||||
private final Context mContext;
|
||||
private final DatePickerController mController;
|
||||
|
||||
private CalendarDay mSelectedDay;
|
||||
|
||||
protected static int WEEK_7_OVERHANG_HEIGHT = 7;
|
||||
protected static final int MONTHS_IN_YEAR = 12;
|
||||
|
||||
/**
|
||||
* A convenience class to represent a specific date.
|
||||
*/
|
||||
public static class CalendarDay {
|
||||
private Calendar calendar;
|
||||
private Time time;
|
||||
int year;
|
||||
int month;
|
||||
int day;
|
||||
|
||||
public CalendarDay() {
|
||||
setTime(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public CalendarDay(long timeInMillis) {
|
||||
setTime(timeInMillis);
|
||||
}
|
||||
|
||||
public CalendarDay(Calendar calendar) {
|
||||
year = calendar.get(Calendar.YEAR);
|
||||
month = calendar.get(Calendar.MONTH);
|
||||
day = calendar.get(Calendar.DAY_OF_MONTH);
|
||||
}
|
||||
|
||||
public CalendarDay(int year, int month, int day) {
|
||||
setDay(year, month, day);
|
||||
}
|
||||
|
||||
public void set(CalendarDay date) {
|
||||
year = date.year;
|
||||
month = date.month;
|
||||
day = date.day;
|
||||
}
|
||||
|
||||
public void setDay(int year, int month, int day) {
|
||||
this.year = year;
|
||||
this.month = month;
|
||||
this.day = day;
|
||||
}
|
||||
|
||||
public synchronized void setJulianDay(int julianDay) {
|
||||
if (time == null) {
|
||||
time = new Time();
|
||||
}
|
||||
time.setJulianDay(julianDay);
|
||||
setTime(time.toMillis(false));
|
||||
}
|
||||
|
||||
private void setTime(long timeInMillis) {
|
||||
if (calendar == null) {
|
||||
calendar = Calendar.getInstance();
|
||||
}
|
||||
calendar.setTimeInMillis(timeInMillis);
|
||||
month = calendar.get(Calendar.MONTH);
|
||||
year = calendar.get(Calendar.YEAR);
|
||||
day = calendar.get(Calendar.DAY_OF_MONTH);
|
||||
}
|
||||
}
|
||||
|
||||
public MonthAdapter(Context context,
|
||||
DatePickerController controller) {
|
||||
mContext = context;
|
||||
mController = controller;
|
||||
init();
|
||||
setSelectedDay(mController.getSelectedDay());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the selected day and related parameters.
|
||||
*
|
||||
* @param day The day to highlight
|
||||
*/
|
||||
public void setSelectedDay(CalendarDay day) {
|
||||
mSelectedDay = day;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public CalendarDay getSelectedDay() {
|
||||
return mSelectedDay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the gesture detector and selected time
|
||||
*/
|
||||
protected void init() {
|
||||
mSelectedDay = new CalendarDay(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return ((mController.getMaxYear() - mController.getMinYear()) + 1) * MONTHS_IN_YEAR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
MonthView v;
|
||||
HashMap<String, Integer> drawingParams = null;
|
||||
if (convertView != null) {
|
||||
v = (MonthView) convertView;
|
||||
// We store the drawing parameters in the view so it can be recycled
|
||||
drawingParams = (HashMap<String, Integer>) v.getTag();
|
||||
} else {
|
||||
v = createMonthView(mContext);
|
||||
// Set up the new view
|
||||
LayoutParams params = new LayoutParams(
|
||||
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
||||
v.setLayoutParams(params);
|
||||
v.setClickable(true);
|
||||
v.setOnDayClickListener(this);
|
||||
}
|
||||
if (drawingParams == null) {
|
||||
drawingParams = new HashMap<String, Integer>();
|
||||
}
|
||||
drawingParams.clear();
|
||||
|
||||
final int month = position % MONTHS_IN_YEAR;
|
||||
final int year = position / MONTHS_IN_YEAR + mController.getMinYear();
|
||||
|
||||
int selectedDay = -1;
|
||||
if (isSelectedDayInMonth(year, month)) {
|
||||
selectedDay = mSelectedDay.day;
|
||||
}
|
||||
|
||||
// Invokes requestLayout() to ensure that the recycled view is set with the appropriate
|
||||
// height/number of weeks before being displayed.
|
||||
v.reuse();
|
||||
|
||||
drawingParams.put(MonthView.VIEW_PARAMS_SELECTED_DAY, selectedDay);
|
||||
drawingParams.put(MonthView.VIEW_PARAMS_YEAR, year);
|
||||
drawingParams.put(MonthView.VIEW_PARAMS_MONTH, month);
|
||||
drawingParams.put(MonthView.VIEW_PARAMS_WEEK_START, mController.getFirstDayOfWeek());
|
||||
v.setMonthParams(drawingParams);
|
||||
v.invalidate();
|
||||
return v;
|
||||
}
|
||||
|
||||
public abstract MonthView createMonthView(Context context);
|
||||
|
||||
private boolean isSelectedDayInMonth(int year, int month) {
|
||||
return mSelectedDay.year == year && mSelectedDay.month == month;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDayClick(MonthView view, CalendarDay day) {
|
||||
if (day != null) {
|
||||
onDayTapped(day);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintains the same hour/min/sec but moves the day to the tapped day.
|
||||
*
|
||||
* @param day The day that was tapped
|
||||
*/
|
||||
protected void onDayTapped(CalendarDay day) {
|
||||
mController.tryVibrate();
|
||||
mController.onDayOfMonthSelected(day.year, day.month, day.day);
|
||||
setSelectedDay(day);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,681 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.*;
|
||||
import android.content.res.*;
|
||||
import android.graphics.*;
|
||||
import android.graphics.Paint.*;
|
||||
import android.os.*;
|
||||
import androidx.core.view.*;
|
||||
import androidx.core.view.accessibility.*;
|
||||
import androidx.core.widget.*;
|
||||
import android.text.format.*;
|
||||
import android.view.*;
|
||||
import android.view.accessibility.*;
|
||||
|
||||
import androidx.customview.widget.ExploreByTouchHelper;
|
||||
|
||||
import com.android.*;
|
||||
import com.android.datetimepicker.*;
|
||||
import com.android.datetimepicker.date.MonthAdapter.*;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
import java.security.*;
|
||||
import java.util.*;
|
||||
import java.util.Formatter;
|
||||
|
||||
/**
|
||||
* A calendar-like view displaying a specified month and the appropriate selectable day numbers
|
||||
* within the specified month.
|
||||
*/
|
||||
public abstract class MonthView extends View {
|
||||
private static final String TAG = "MonthView";
|
||||
|
||||
/**
|
||||
* These params can be passed into the view to control how it appears.
|
||||
* {@link #VIEW_PARAMS_WEEK} is the only required field, though the default
|
||||
* values are unlikely to fit most layouts correctly.
|
||||
*/
|
||||
/**
|
||||
* This sets the height of this week in pixels
|
||||
*/
|
||||
public static final String VIEW_PARAMS_HEIGHT = "height";
|
||||
/**
|
||||
* This specifies the position (or weeks since the epoch) of this week,
|
||||
* calculated using {@link Utils#getWeeksSinceEpochFromJulianDay}
|
||||
*/
|
||||
public static final String VIEW_PARAMS_MONTH = "month";
|
||||
/**
|
||||
* This specifies the position (or weeks since the epoch) of this week,
|
||||
* calculated using {@link Utils#getWeeksSinceEpochFromJulianDay}
|
||||
*/
|
||||
public static final String VIEW_PARAMS_YEAR = "year";
|
||||
/**
|
||||
* This sets one of the days in this view as selected {@link Time#SUNDAY}
|
||||
* through {@link Time#SATURDAY}.
|
||||
*/
|
||||
public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day";
|
||||
/**
|
||||
* Which day the week should start on. {@link Time#SUNDAY} through
|
||||
* {@link Time#SATURDAY}.
|
||||
*/
|
||||
public static final String VIEW_PARAMS_WEEK_START = "week_start";
|
||||
/**
|
||||
* How many days to display at a time. Days will be displayed starting with
|
||||
* {@link #mWeekStart}.
|
||||
*/
|
||||
public static final String VIEW_PARAMS_NUM_DAYS = "num_days";
|
||||
/**
|
||||
* Which month is currently in focus, as defined by {@link Time#month}
|
||||
* [0-11].
|
||||
*/
|
||||
public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month";
|
||||
/**
|
||||
* If this month should display week numbers. false if 0, true otherwise.
|
||||
*/
|
||||
public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num";
|
||||
|
||||
protected static int DEFAULT_HEIGHT = 32;
|
||||
protected static int MIN_HEIGHT = 10;
|
||||
protected static final int DEFAULT_SELECTED_DAY = -1;
|
||||
protected static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
|
||||
protected static final int DEFAULT_NUM_DAYS = 7;
|
||||
protected static final int DEFAULT_SHOW_WK_NUM = 0;
|
||||
protected static final int DEFAULT_FOCUS_MONTH = -1;
|
||||
protected static final int DEFAULT_NUM_ROWS = 6;
|
||||
protected static final int MAX_NUM_ROWS = 6;
|
||||
|
||||
private static final int SELECTED_CIRCLE_ALPHA = 60;
|
||||
|
||||
protected static int DAY_SEPARATOR_WIDTH = 1;
|
||||
protected static int MINI_DAY_NUMBER_TEXT_SIZE;
|
||||
protected static int MONTH_LABEL_TEXT_SIZE;
|
||||
protected static int MONTH_DAY_LABEL_TEXT_SIZE;
|
||||
protected static int MONTH_HEADER_SIZE;
|
||||
protected static int DAY_SELECTED_CIRCLE_SIZE;
|
||||
|
||||
// used for scaling to the device density
|
||||
protected static float mScale = 0;
|
||||
|
||||
// affects the padding on the sides of this view
|
||||
protected int mPadding = 0;
|
||||
|
||||
private String mDayOfWeekTypeface;
|
||||
private String mMonthTitleTypeface;
|
||||
|
||||
protected Paint mMonthNumPaint;
|
||||
protected Paint mMonthTitlePaint;
|
||||
protected Paint mMonthTitleBGPaint;
|
||||
protected Paint mSelectedCirclePaint;
|
||||
protected Paint mMonthDayLabelPaint;
|
||||
|
||||
private final Formatter mFormatter;
|
||||
private final StringBuilder mStringBuilder;
|
||||
|
||||
// The Julian day of the first day displayed by this item
|
||||
protected int mFirstJulianDay = -1;
|
||||
// The month of the first day in this week
|
||||
protected int mFirstMonth = -1;
|
||||
// The month of the last day in this week
|
||||
protected int mLastMonth = -1;
|
||||
|
||||
protected int mMonth;
|
||||
|
||||
protected int mYear;
|
||||
// Quick reference to the width of this view, matches parent
|
||||
protected int mWidth;
|
||||
// The height this view should draw at in pixels, set by height param
|
||||
protected int mRowHeight = DEFAULT_HEIGHT;
|
||||
// If this view contains the today
|
||||
protected boolean mHasToday = false;
|
||||
// Which day is selected [0-6] or -1 if no day is selected
|
||||
protected int mSelectedDay = -1;
|
||||
// Which day is today [0-6] or -1 if no day is today
|
||||
protected int mToday = DEFAULT_SELECTED_DAY;
|
||||
// Which day of the week to start on [0-6]
|
||||
protected int mWeekStart = DEFAULT_WEEK_START;
|
||||
// How many days to display
|
||||
protected int mNumDays = DEFAULT_NUM_DAYS;
|
||||
// The number of days + a spot for week number if it is displayed
|
||||
protected int mNumCells = mNumDays;
|
||||
// The left edge of the selected day
|
||||
protected int mSelectedLeft = -1;
|
||||
// The right edge of the selected day
|
||||
protected int mSelectedRight = -1;
|
||||
|
||||
private final Calendar mCalendar;
|
||||
private final Calendar mDayLabelCalendar;
|
||||
private final MonthViewTouchHelper mTouchHelper;
|
||||
|
||||
private int mNumRows = DEFAULT_NUM_ROWS;
|
||||
|
||||
// Optional listener for handling day click actions
|
||||
private OnDayClickListener mOnDayClickListener;
|
||||
// Whether to prevent setting the accessibility delegate
|
||||
private boolean mLockAccessibilityDelegate;
|
||||
|
||||
protected int mDayTextColor;
|
||||
protected int mTodayNumberColor;
|
||||
protected int mMonthTitleColor;
|
||||
protected int mMonthTitleBGColor;
|
||||
|
||||
public MonthView(Context context) {
|
||||
super(context);
|
||||
|
||||
Resources res = context.getResources();
|
||||
|
||||
mDayLabelCalendar = Calendar.getInstance();
|
||||
mCalendar = Calendar.getInstance();
|
||||
|
||||
mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface);
|
||||
mMonthTitleTypeface = res.getString(R.string.sans_serif);
|
||||
|
||||
mDayTextColor = res.getColor(R.color.date_picker_text_normal);
|
||||
mTodayNumberColor = res.getColor(R.color.blue);
|
||||
mMonthTitleColor = res.getColor(R.color.white);
|
||||
mMonthTitleBGColor = res.getColor(R.color.circle_background);
|
||||
|
||||
mStringBuilder = new StringBuilder(50);
|
||||
mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
|
||||
|
||||
MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.day_number_size);
|
||||
MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_label_size);
|
||||
MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_day_label_text_size);
|
||||
MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.month_list_item_header_height);
|
||||
DAY_SELECTED_CIRCLE_SIZE = res
|
||||
.getDimensionPixelSize(R.dimen.day_number_select_circle_radius);
|
||||
|
||||
mRowHeight = (res.getDimensionPixelOffset(R.dimen.date_picker_view_animator_height)
|
||||
- MONTH_HEADER_SIZE) / MAX_NUM_ROWS;
|
||||
|
||||
// Set up accessibility components.
|
||||
mTouchHelper = new MonthViewTouchHelper(this);
|
||||
ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
|
||||
ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
mLockAccessibilityDelegate = true;
|
||||
|
||||
// Sets up any standard paints that will be used
|
||||
initView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
|
||||
// Workaround for a JB MR1 issue where accessibility delegates on
|
||||
// top-level ListView items are overwritten.
|
||||
if (!mLockAccessibilityDelegate) {
|
||||
super.setAccessibilityDelegate(delegate);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnDayClickListener(OnDayClickListener listener) {
|
||||
mOnDayClickListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchHoverEvent(MotionEvent event) {
|
||||
// First right-of-refusal goes the touch exploration helper.
|
||||
if (mTouchHelper.dispatchHoverEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
return super.dispatchHoverEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_UP:
|
||||
final int day = getDayFromLocation(event.getX(), event.getY());
|
||||
if (day >= 0) {
|
||||
onDayClick(day);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the text and style properties for painting. Override this if you
|
||||
* want to use a different paint.
|
||||
*/
|
||||
protected void initView() {
|
||||
mMonthTitlePaint = new Paint();
|
||||
mMonthTitlePaint.setFakeBoldText(true);
|
||||
mMonthTitlePaint.setAntiAlias(true);
|
||||
mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE);
|
||||
mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD));
|
||||
mMonthTitlePaint.setColor(mDayTextColor);
|
||||
mMonthTitlePaint.setTextAlign(Align.CENTER);
|
||||
mMonthTitlePaint.setStyle(Style.FILL);
|
||||
|
||||
mMonthTitleBGPaint = new Paint();
|
||||
mMonthTitleBGPaint.setFakeBoldText(true);
|
||||
mMonthTitleBGPaint.setAntiAlias(true);
|
||||
mMonthTitleBGPaint.setColor(mMonthTitleBGColor);
|
||||
mMonthTitleBGPaint.setTextAlign(Align.CENTER);
|
||||
mMonthTitleBGPaint.setStyle(Style.FILL);
|
||||
|
||||
mSelectedCirclePaint = new Paint();
|
||||
mSelectedCirclePaint.setFakeBoldText(true);
|
||||
mSelectedCirclePaint.setAntiAlias(true);
|
||||
mSelectedCirclePaint.setColor(mTodayNumberColor);
|
||||
mSelectedCirclePaint.setTextAlign(Align.CENTER);
|
||||
mSelectedCirclePaint.setStyle(Style.FILL);
|
||||
mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA);
|
||||
|
||||
mMonthDayLabelPaint = new Paint();
|
||||
mMonthDayLabelPaint.setAntiAlias(true);
|
||||
mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE);
|
||||
mMonthDayLabelPaint.setColor(mDayTextColor);
|
||||
mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL));
|
||||
mMonthDayLabelPaint.setStyle(Style.FILL);
|
||||
mMonthDayLabelPaint.setTextAlign(Align.CENTER);
|
||||
mMonthDayLabelPaint.setFakeBoldText(true);
|
||||
|
||||
mMonthNumPaint = new Paint();
|
||||
mMonthNumPaint.setAntiAlias(true);
|
||||
mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE);
|
||||
mMonthNumPaint.setStyle(Style.FILL);
|
||||
mMonthNumPaint.setTextAlign(Align.CENTER);
|
||||
mMonthNumPaint.setFakeBoldText(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
drawMonthTitle(canvas);
|
||||
drawMonthDayLabels(canvas);
|
||||
drawMonthNums(canvas);
|
||||
}
|
||||
|
||||
private int mDayOfWeekStart = 0;
|
||||
|
||||
/**
|
||||
* Sets all the parameters for displaying this week. The only required
|
||||
* parameter is the week number. Other parameters have a default value and
|
||||
* will only update if a new value is included, except for focus month,
|
||||
* which will always default to no focus month if no value is passed in. See
|
||||
* {@link #VIEW_PARAMS_HEIGHT} for more info on parameters.
|
||||
*
|
||||
* @param params A map of the new parameters, see
|
||||
* {@link #VIEW_PARAMS_HEIGHT}
|
||||
*/
|
||||
public void setMonthParams(HashMap<String, Integer> params) {
|
||||
if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) {
|
||||
throw new InvalidParameterException("You must specify month and year for this view");
|
||||
}
|
||||
setTag(params);
|
||||
// We keep the current value for any params not present
|
||||
if (params.containsKey(VIEW_PARAMS_HEIGHT)) {
|
||||
mRowHeight = params.get(VIEW_PARAMS_HEIGHT);
|
||||
if (mRowHeight < MIN_HEIGHT) {
|
||||
mRowHeight = MIN_HEIGHT;
|
||||
}
|
||||
}
|
||||
if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) {
|
||||
mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY);
|
||||
}
|
||||
|
||||
// Allocate space for caching the day numbers and focus values
|
||||
mMonth = params.get(VIEW_PARAMS_MONTH);
|
||||
mYear = params.get(VIEW_PARAMS_YEAR);
|
||||
|
||||
// Figure out what day today is
|
||||
final Time today = new Time(Time.getCurrentTimezone());
|
||||
today.setToNow();
|
||||
mHasToday = false;
|
||||
mToday = -1;
|
||||
|
||||
mCalendar.set(Calendar.MONTH, mMonth);
|
||||
mCalendar.set(Calendar.YEAR, mYear);
|
||||
mCalendar.set(Calendar.DAY_OF_MONTH, 1);
|
||||
mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
|
||||
|
||||
if (params.containsKey(VIEW_PARAMS_WEEK_START)) {
|
||||
mWeekStart = params.get(VIEW_PARAMS_WEEK_START);
|
||||
} else {
|
||||
mWeekStart = mCalendar.getFirstDayOfWeek();
|
||||
}
|
||||
|
||||
mNumCells = Utils.getDaysInMonth(mMonth, mYear);
|
||||
for (int i = 0; i < mNumCells; i++) {
|
||||
final int day = i + 1;
|
||||
if (sameDay(day, today)) {
|
||||
mHasToday = true;
|
||||
mToday = day;
|
||||
}
|
||||
}
|
||||
mNumRows = calculateNumRows();
|
||||
|
||||
// Invalidate cached accessibility information.
|
||||
mTouchHelper.invalidateRoot();
|
||||
}
|
||||
|
||||
public void reuse() {
|
||||
mNumRows = DEFAULT_NUM_ROWS;
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
private int calculateNumRows() {
|
||||
int offset = findDayOffset();
|
||||
int dividend = (offset + mNumCells) / mNumDays;
|
||||
int remainder = (offset + mNumCells) % mNumDays;
|
||||
return (dividend + (remainder > 0 ? 1 : 0));
|
||||
}
|
||||
|
||||
private boolean sameDay(int day, Time today) {
|
||||
return mYear == today.year &&
|
||||
mMonth == today.month &&
|
||||
day == today.monthDay;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows
|
||||
+ MONTH_HEADER_SIZE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
mWidth = w;
|
||||
|
||||
// Invalidate cached accessibility information.
|
||||
mTouchHelper.invalidateRoot();
|
||||
}
|
||||
|
||||
private String getMonthAndYearString() {
|
||||
int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
|
||||
| DateUtils.FORMAT_NO_MONTH_DAY;
|
||||
mStringBuilder.setLength(0);
|
||||
long millis = mCalendar.getTimeInMillis();
|
||||
return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags,
|
||||
Time.getCurrentTimezone()).toString();
|
||||
}
|
||||
|
||||
private void drawMonthTitle(Canvas canvas) {
|
||||
int x = (mWidth + 2 * mPadding) / 2;
|
||||
int y = (MONTH_HEADER_SIZE - MONTH_DAY_LABEL_TEXT_SIZE) / 2 + (MONTH_LABEL_TEXT_SIZE / 3);
|
||||
canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint);
|
||||
}
|
||||
|
||||
private void drawMonthDayLabels(Canvas canvas) {
|
||||
int y = MONTH_HEADER_SIZE - (MONTH_DAY_LABEL_TEXT_SIZE / 2);
|
||||
int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
|
||||
|
||||
for (int i = 0; i < mNumDays; i++) {
|
||||
int calendarDay = (i + mWeekStart) % mNumDays;
|
||||
int x = (2 * i + 1) * dayWidthHalf + mPadding;
|
||||
mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
|
||||
canvas.drawText(mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT,
|
||||
Locale.getDefault()).toUpperCase(Locale.getDefault()), x, y,
|
||||
mMonthDayLabelPaint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the week and month day numbers for this week. Override this method
|
||||
* if you need different placement.
|
||||
*
|
||||
* @param canvas The canvas to draw on
|
||||
*/
|
||||
protected void drawMonthNums(Canvas canvas) {
|
||||
int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH)
|
||||
+ MONTH_HEADER_SIZE;
|
||||
int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
|
||||
int j = findDayOffset();
|
||||
for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) {
|
||||
int x = (2 * j + 1) * dayWidthHalf + mPadding;
|
||||
|
||||
int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH;
|
||||
|
||||
int startX = x - dayWidthHalf;
|
||||
int stopX = x + dayWidthHalf;
|
||||
int startY = y - yRelativeToDay;
|
||||
int stopY = startY + mRowHeight;
|
||||
|
||||
drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY);
|
||||
|
||||
j++;
|
||||
if (j == mNumDays) {
|
||||
j = 0;
|
||||
y += mRowHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should draw the month day. Implemented by sub-classes to allow customization.
|
||||
*
|
||||
* @param canvas The canvas to draw on
|
||||
* @param year The year of this month day
|
||||
* @param month The month of this month day
|
||||
* @param day The day number of this month day
|
||||
* @param x The default x position to draw the day number
|
||||
* @param y The default y position to draw the day number
|
||||
* @param startX The left boundary of the day number rect
|
||||
* @param stopX The right boundary of the day number rect
|
||||
* @param startY The top boundary of the day number rect
|
||||
* @param stopY The bottom boundary of the day number rect
|
||||
*/
|
||||
public abstract void drawMonthDay(Canvas canvas, int year, int month, int day,
|
||||
int x, int y, int startX, int stopX, int startY, int stopY);
|
||||
|
||||
private int findDayOffset() {
|
||||
return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart)
|
||||
- mWeekStart;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the day that the given x position is in, accounting for week
|
||||
* number. Returns the day or -1 if the position wasn't in a day.
|
||||
*
|
||||
* @param x The x position of the touch event
|
||||
* @return The day number, or -1 if the position wasn't in a day
|
||||
*/
|
||||
public int getDayFromLocation(float x, float y) {
|
||||
int dayStart = mPadding;
|
||||
if (x < dayStart || x > mWidth - mPadding) {
|
||||
return -1;
|
||||
}
|
||||
// Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
|
||||
int row = (int) (y - MONTH_HEADER_SIZE) / mRowHeight;
|
||||
int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding));
|
||||
|
||||
int day = column - findDayOffset() + 1;
|
||||
day += row * mNumDays;
|
||||
if (day < 1 || day > mNumCells) {
|
||||
return -1;
|
||||
}
|
||||
return day;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user clicks on a day. Handles callbacks to the
|
||||
* {@link OnDayClickListener} if one is set.
|
||||
*
|
||||
* @param day The day that was clicked
|
||||
*/
|
||||
private void onDayClick(int day) {
|
||||
if (mOnDayClickListener != null) {
|
||||
mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day));
|
||||
}
|
||||
|
||||
// This is a no-op if accessibility is turned off.
|
||||
mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The date that has accessibility focus, or {@code null} if no date
|
||||
* has focus
|
||||
*/
|
||||
public CalendarDay getAccessibilityFocus() {
|
||||
final int day = mTouchHelper.getFocusedVirtualView();
|
||||
if (day >= 0) {
|
||||
return new CalendarDay(mYear, mMonth, day);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears accessibility focus within the view. No-op if the view does not
|
||||
* contain accessibility focus.
|
||||
*/
|
||||
public void clearAccessibilityFocus() {
|
||||
mTouchHelper.clearFocusedVirtualView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to restore accessibility focus to the specified date.
|
||||
*
|
||||
* @param day The date which should receive focus
|
||||
* @return {@code false} if the date is not valid for this month view, or
|
||||
* {@code true} if the date received focus
|
||||
*/
|
||||
public boolean restoreAccessibilityFocus(CalendarDay day) {
|
||||
if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) {
|
||||
return false;
|
||||
}
|
||||
mTouchHelper.setFocusedVirtualView(day.day);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a virtual view hierarchy for interfacing with an accessibility
|
||||
* service.
|
||||
*/
|
||||
private class MonthViewTouchHelper extends ExploreByTouchHelper {
|
||||
private static final String DATE_FORMAT = "dd MMMM yyyy";
|
||||
|
||||
private final Rect mTempRect = new Rect();
|
||||
private final Calendar mTempCalendar = Calendar.getInstance();
|
||||
|
||||
public MonthViewTouchHelper(View host) {
|
||||
super(host);
|
||||
}
|
||||
|
||||
public void setFocusedVirtualView(int virtualViewId) {
|
||||
getAccessibilityNodeProvider(MonthView.this).performAction(
|
||||
virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
|
||||
}
|
||||
|
||||
public void clearFocusedVirtualView() {
|
||||
final int focusedVirtualView = getFocusedVirtualView();
|
||||
if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) {
|
||||
getAccessibilityNodeProvider(MonthView.this).performAction(
|
||||
focusedVirtualView,
|
||||
AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS,
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getVirtualViewAt(float x, float y) {
|
||||
final int day = getDayFromLocation(x, y);
|
||||
if (day >= 0) {
|
||||
return day;
|
||||
}
|
||||
return ExploreByTouchHelper.INVALID_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
|
||||
for (int day = 1; day <= mNumCells; day++) {
|
||||
virtualViewIds.add(day);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
|
||||
event.setContentDescription(getItemDescription(virtualViewId));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPopulateNodeForVirtualView(int virtualViewId,
|
||||
AccessibilityNodeInfoCompat node) {
|
||||
getItemBounds(virtualViewId, mTempRect);
|
||||
|
||||
node.setContentDescription(getItemDescription(virtualViewId));
|
||||
node.setBoundsInParent(mTempRect);
|
||||
node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
|
||||
|
||||
if (virtualViewId == mSelectedDay) {
|
||||
node.setSelected(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
|
||||
Bundle arguments) {
|
||||
switch (action) {
|
||||
case AccessibilityNodeInfo.ACTION_CLICK:
|
||||
onDayClick(virtualViewId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the bounding rectangle of a given time object.
|
||||
*
|
||||
* @param day The day to calculate bounds for
|
||||
* @param rect The rectangle in which to store the bounds
|
||||
*/
|
||||
private void getItemBounds(int day, Rect rect) {
|
||||
final int offsetX = mPadding;
|
||||
final int offsetY = MONTH_HEADER_SIZE;
|
||||
final int cellHeight = mRowHeight;
|
||||
final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays);
|
||||
final int index = ((day - 1) + findDayOffset());
|
||||
final int row = (index / mNumDays);
|
||||
final int column = (index % mNumDays);
|
||||
final int x = (offsetX + (column * cellWidth));
|
||||
final int y = (offsetY + (row * cellHeight));
|
||||
|
||||
rect.set(x, y, (x + cellWidth), (y + cellHeight));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a description for a given time object. Since this
|
||||
* description will be spoken, the components are ordered by descending
|
||||
* specificity as DAY MONTH YEAR.
|
||||
*
|
||||
* @param day The day to generate a description for
|
||||
* @return A description of the time object
|
||||
*/
|
||||
private CharSequence getItemDescription(int day) {
|
||||
mTempCalendar.set(mYear, mMonth, day);
|
||||
final CharSequence date = DateFormat.format(DATE_FORMAT,
|
||||
mTempCalendar.getTimeInMillis());
|
||||
|
||||
if (day == mSelectedDay) {
|
||||
return getContext().getString(R.string.item_is_selected, date);
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles callbacks when the user clicks on a time object.
|
||||
*/
|
||||
public interface OnDayClickListener {
|
||||
public void onDayClick(MonthView view, CalendarDay day);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* A DayPickerView customized for {@link SimpleMonthAdapter}
|
||||
*/
|
||||
public class SimpleDayPickerView extends DayPickerView {
|
||||
|
||||
public SimpleDayPickerView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public SimpleDayPickerView(Context context, DatePickerController controller) {
|
||||
super(context, controller);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MonthAdapter createMonthAdapter(Context context, DatePickerController controller) {
|
||||
return new SimpleMonthAdapter(context, controller);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* An adapter for a list of {@link SimpleMonthView} items.
|
||||
*/
|
||||
public class SimpleMonthAdapter extends MonthAdapter {
|
||||
|
||||
public SimpleMonthAdapter(Context context, DatePickerController controller) {
|
||||
super(context, controller);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MonthView createMonthView(Context context) {
|
||||
return new SimpleMonthView(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
|
||||
public class SimpleMonthView extends MonthView {
|
||||
|
||||
public SimpleMonthView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawMonthDay(Canvas canvas, int year, int month, int day,
|
||||
int x, int y, int startX, int stopX, int startY, int stopY) {
|
||||
if (mSelectedDay == day) {
|
||||
canvas.drawCircle(x , y - (MINI_DAY_NUMBER_TEXT_SIZE / 3), DAY_SELECTED_CIRCLE_SIZE,
|
||||
mSelectedCirclePaint);
|
||||
}
|
||||
|
||||
if (mHasToday && mToday == day) {
|
||||
mMonthNumPaint.setColor(mTodayNumberColor);
|
||||
} else {
|
||||
mMonthNumPaint.setColor(mDayTextColor);
|
||||
}
|
||||
canvas.drawText(String.format("%d", day), x, y, mMonthNumPaint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Align;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
/**
|
||||
* A text view which, when pressed or activated, displays a blue circle around the text.
|
||||
*/
|
||||
public class TextViewWithCircularIndicator extends TextView {
|
||||
|
||||
private static final int SELECTED_CIRCLE_ALPHA = 60;
|
||||
|
||||
Paint mCirclePaint = new Paint();
|
||||
|
||||
private final int mRadius;
|
||||
private final int mCircleColor;
|
||||
private final String mItemIsSelectedText;
|
||||
|
||||
private boolean mDrawCircle;
|
||||
|
||||
public TextViewWithCircularIndicator(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
Resources res = context.getResources();
|
||||
mCircleColor = res.getColor(R.color.blue);
|
||||
mRadius = res.getDimensionPixelOffset(R.dimen.month_select_circle_radius);
|
||||
mItemIsSelectedText = context.getResources().getString(R.string.item_is_selected);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
mCirclePaint.setFakeBoldText(true);
|
||||
mCirclePaint.setAntiAlias(true);
|
||||
mCirclePaint.setColor(mCircleColor);
|
||||
mCirclePaint.setTextAlign(Align.CENTER);
|
||||
mCirclePaint.setStyle(Style.FILL);
|
||||
mCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA);
|
||||
}
|
||||
|
||||
public void drawIndicator(boolean drawCircle) {
|
||||
mDrawCircle = drawCircle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
if (mDrawCircle) {
|
||||
final int width = getWidth();
|
||||
final int height = getHeight();
|
||||
int radius = Math.min(width, height) / 2;
|
||||
canvas.drawCircle(width / 2, height / 2, radius, mCirclePaint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getContentDescription() {
|
||||
CharSequence itemText = getText();
|
||||
if (mDrawCircle) {
|
||||
return String.format(mItemIsSelectedText, itemText);
|
||||
} else {
|
||||
return itemText;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.StateListDrawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.datetimepicker.date.DatePickerDialog.OnDateChangedListener;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Displays a selectable list of years.
|
||||
*/
|
||||
public class YearPickerView extends ListView implements OnItemClickListener, OnDateChangedListener {
|
||||
private static final String TAG = "YearPickerView";
|
||||
|
||||
private final DatePickerController mController;
|
||||
private YearAdapter mAdapter;
|
||||
private int mViewSize;
|
||||
private int mChildSize;
|
||||
private TextViewWithCircularIndicator mSelectedView;
|
||||
|
||||
/**
|
||||
* @param context
|
||||
*/
|
||||
public YearPickerView(Context context, DatePickerController controller) {
|
||||
super(context);
|
||||
mController = controller;
|
||||
mController.registerOnDateChangedListener(this);
|
||||
ViewGroup.LayoutParams frame = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.WRAP_CONTENT);
|
||||
setLayoutParams(frame);
|
||||
Resources res = context.getResources();
|
||||
mViewSize = res.getDimensionPixelOffset(R.dimen.date_picker_view_animator_height);
|
||||
mChildSize = res.getDimensionPixelOffset(R.dimen.year_label_height);
|
||||
setVerticalFadingEdgeEnabled(true);
|
||||
setFadingEdgeLength(mChildSize / 3);
|
||||
init(context);
|
||||
setOnItemClickListener(this);
|
||||
setSelector(new StateListDrawable());
|
||||
setDividerHeight(0);
|
||||
onDateChanged();
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
ArrayList<String> years = new ArrayList<String>();
|
||||
for (int year = mController.getMinYear(); year <= mController.getMaxYear(); year++) {
|
||||
years.add(String.format("%d", year));
|
||||
}
|
||||
mAdapter = new YearAdapter(context, R.layout.year_label_text_view, years);
|
||||
setAdapter(mAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
mController.tryVibrate();
|
||||
TextViewWithCircularIndicator clickedView = (TextViewWithCircularIndicator) view;
|
||||
if (clickedView != null) {
|
||||
if (clickedView != mSelectedView) {
|
||||
if (mSelectedView != null) {
|
||||
mSelectedView.drawIndicator(false);
|
||||
mSelectedView.requestLayout();
|
||||
}
|
||||
clickedView.drawIndicator(true);
|
||||
clickedView.requestLayout();
|
||||
mSelectedView = clickedView;
|
||||
}
|
||||
mController.onYearSelected(getYearFromTextView(clickedView));
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private static int getYearFromTextView(TextView view) {
|
||||
return Integer.valueOf(view.getText().toString());
|
||||
}
|
||||
|
||||
private class YearAdapter extends ArrayAdapter<String> {
|
||||
|
||||
public YearAdapter(Context context, int resource, List<String> objects) {
|
||||
super(context, resource, objects);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
TextViewWithCircularIndicator v = (TextViewWithCircularIndicator)
|
||||
super.getView(position, convertView, parent);
|
||||
v.requestLayout();
|
||||
int year = getYearFromTextView(v);
|
||||
boolean selected = mController.getSelectedDay().year == year;
|
||||
v.drawIndicator(selected);
|
||||
if (selected) {
|
||||
mSelectedView = v;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
public void postSetSelectionCentered(final int position) {
|
||||
postSetSelectionFromTop(position, mViewSize / 2 - mChildSize / 2);
|
||||
}
|
||||
|
||||
public void postSetSelectionFromTop(final int position, final int offset) {
|
||||
post(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
setSelectionFromTop(position, offset);
|
||||
requestLayout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public int getFirstPositionOffset() {
|
||||
final View firstChild = getChildAt(0);
|
||||
if (firstChild == null) {
|
||||
return 0;
|
||||
}
|
||||
return firstChild.getTop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDateChanged() {
|
||||
mAdapter.notifyDataSetChanged();
|
||||
postSetSelectionCentered(mController.getSelectedDay().year - mController.getMinYear());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(event);
|
||||
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
|
||||
event.setFromIndex(0);
|
||||
event.setToIndex(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* 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.time;
|
||||
|
||||
import android.content.*;
|
||||
import android.content.res.*;
|
||||
import android.graphics.*;
|
||||
import android.graphics.Paint.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
|
||||
import com.android.*;
|
||||
import com.android.datetimepicker.*;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
import java.text.*;
|
||||
|
||||
/**
|
||||
* Draw the two smaller AM and PM circles next to where the larger circle will be.
|
||||
*/
|
||||
public class AmPmCirclesView extends View {
|
||||
private static final String TAG = "AmPmCirclesView";
|
||||
|
||||
// Alpha level for selected circle.
|
||||
private static final int SELECTED_ALPHA = Utils.SELECTED_ALPHA;
|
||||
private static final int SELECTED_ALPHA_THEME_DARK = Utils.SELECTED_ALPHA_THEME_DARK;
|
||||
|
||||
private final Paint mPaint = new Paint();
|
||||
private int mSelectedAlpha;
|
||||
private int mUnselectedColor;
|
||||
protected int mAmPmTextColor = Color.WHITE;
|
||||
protected int mSelectedColor = Color.BLUE;
|
||||
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();
|
||||
mUnselectedColor = res.getColor(R.color.white);
|
||||
//mSelectedColor = res.getColor(R.color.blue);
|
||||
//mAmPmTextColor = res.getColor(R.color.ampm_text_color);
|
||||
mSelectedAlpha = SELECTED_ALPHA;
|
||||
String typefaceFamily = res.getString(R.string.sans_serif);
|
||||
Typeface tf = Typeface.create(typefaceFamily, 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));
|
||||
String[] amPmTexts = new DateFormatSymbols().getAmPmStrings();
|
||||
mAmText = amPmTexts[0];
|
||||
mPmText = amPmTexts[1];
|
||||
|
||||
setAmOrPm(amOrPm);
|
||||
mAmOrPmPressed = -1;
|
||||
|
||||
mIsInitialized = true;
|
||||
}
|
||||
|
||||
/* package */ void setTheme(Context context, boolean themeDark) {
|
||||
Resources res = context.getResources();
|
||||
if (themeDark) {
|
||||
mUnselectedColor = res.getColor(R.color.dark_gray);
|
||||
mSelectedColor = res.getColor(R.color.red);
|
||||
mAmPmTextColor = res.getColor(R.color.white);
|
||||
mSelectedAlpha = SELECTED_ALPHA_THEME_DARK;
|
||||
} else {
|
||||
mUnselectedColor = res.getColor(R.color.white);
|
||||
//mSelectedColor = res.getColor(R.color.blue);
|
||||
//mAmPmTextColor = res.getColor(R.color.ampm_text_color);
|
||||
mSelectedAlpha = SELECTED_ALPHA;
|
||||
}
|
||||
}
|
||||
|
||||
public void setAmOrPm(int amOrPm) {
|
||||
mAmOrPm = amOrPm;
|
||||
}
|
||||
|
||||
public void setAmOrPmPressed(int amOrPmPressed) {
|
||||
mAmOrPmPressed = amOrPmPressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate whether the coordinates are touching the AM or PM circle.
|
||||
*/
|
||||
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 * 3 / 4;
|
||||
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;
|
||||
}
|
||||
|
||||
// We'll need to draw either a lighter blue (for selection), a darker blue (for touching)
|
||||
// or white (for not selected).
|
||||
int amColor = mUnselectedColor;
|
||||
int amAlpha = 255;
|
||||
int pmColor = mUnselectedColor;
|
||||
int pmAlpha = 255;
|
||||
if (mAmOrPm == AM) {
|
||||
amColor = mSelectedColor;
|
||||
amAlpha = mSelectedAlpha;
|
||||
} else if (mAmOrPm == PM) {
|
||||
pmColor = mSelectedColor;
|
||||
pmAlpha = mSelectedAlpha;
|
||||
}
|
||||
if (mAmOrPmPressed == AM) {
|
||||
amColor = mSelectedColor;
|
||||
amAlpha = mSelectedAlpha;
|
||||
} else if (mAmOrPmPressed == PM) {
|
||||
pmColor = mSelectedColor;
|
||||
pmAlpha = mSelectedAlpha;
|
||||
}
|
||||
|
||||
// Draw the two circles.
|
||||
mPaint.setColor(amColor);
|
||||
mPaint.setAlpha(amAlpha);
|
||||
canvas.drawCircle(mAmXCenter, mAmPmYCenter, mAmPmCircleRadius, mPaint);
|
||||
mPaint.setColor(pmColor);
|
||||
mPaint.setAlpha(pmAlpha);
|
||||
canvas.drawCircle(mPmXCenter, mAmPmYCenter, mAmPmCircleRadius, mPaint);
|
||||
|
||||
// Draw the AM/PM texts on top.
|
||||
mPaint.setColor(mAmPmTextColor);
|
||||
int textYCenter = mAmPmYCenter - (int) (mPaint.descent() + mPaint.ascent()) / 2;
|
||||
canvas.drawText(mAmText, mAmXCenter, textYCenter, mPaint);
|
||||
canvas.drawText(mPmText, mPmXCenter, textYCenter, mPaint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.time;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
/**
|
||||
* Draws a simple white circle on which the numbers will be drawn.
|
||||
*/
|
||||
public class CircleView extends View {
|
||||
private static final String TAG = "CircleView";
|
||||
|
||||
private final Paint mPaint = new Paint();
|
||||
private boolean mIs24HourMode;
|
||||
private int mCircleColor;
|
||||
private int mDotColor;
|
||||
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();
|
||||
mCircleColor = res.getColor(R.color.white);
|
||||
mDotColor = res.getColor(R.color.numbers_text_color);
|
||||
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;
|
||||
}
|
||||
|
||||
/* package */ void setTheme(Context context, boolean dark) {
|
||||
Resources res = context.getResources();
|
||||
if (dark) {
|
||||
mCircleColor = res.getColor(R.color.dark_gray);
|
||||
mDotColor = res.getColor(R.color.light_gray);
|
||||
} else {
|
||||
mCircleColor = res.getColor(R.color.white);
|
||||
mDotColor = res.getColor(R.color.numbers_text_color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
// Draw the white circle.
|
||||
mPaint.setColor(mCircleColor);
|
||||
canvas.drawCircle(mXCenter, mYCenter, mCircleRadius, mPaint);
|
||||
|
||||
// Draw a small black circle in the center.
|
||||
mPaint.setColor(mDotColor);
|
||||
canvas.drawCircle(mXCenter, mYCenter, 2, mPaint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,830 @@
|
||||
/*
|
||||
* 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.time;
|
||||
|
||||
import android.animation.*;
|
||||
import android.annotation.*;
|
||||
import android.content.*;
|
||||
import android.content.res.*;
|
||||
import android.os.*;
|
||||
import android.text.format.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.view.View.*;
|
||||
import android.view.accessibility.*;
|
||||
import android.widget.*;
|
||||
|
||||
import com.android.*;
|
||||
import com.android.datetimepicker.*;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
/**
|
||||
* The primary layout to hold the circular picker, and the am/pm buttons. This view well measure
|
||||
* itself to end up as a square. It also handles touches to be passed in to views that need to know
|
||||
* when they'd been touched.
|
||||
*/
|
||||
public class RadialPickerLayout extends FrameLayout implements OnTouchListener {
|
||||
private static final String TAG = "RadialPickerLayout";
|
||||
|
||||
private final int TOUCH_SLOP;
|
||||
private final int TAP_TIMEOUT;
|
||||
|
||||
private static final int VISIBLE_DEGREES_STEP_SIZE = 30;
|
||||
private static final int HOUR_VALUE_TO_DEGREES_STEP_SIZE = VISIBLE_DEGREES_STEP_SIZE;
|
||||
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 ENABLE_PICKER_INDEX = TimePickerDialog.ENABLE_PICKER_INDEX;
|
||||
private static final int AM = TimePickerDialog.AM;
|
||||
private static final int PM = TimePickerDialog.PM;
|
||||
|
||||
private int mLastValueSelected;
|
||||
|
||||
private HapticFeedbackController mHapticFeedbackController;
|
||||
private OnValueSelectedListener mListener;
|
||||
private boolean mTimeInitialized;
|
||||
private int mCurrentHoursOfDay;
|
||||
private int mCurrentMinutes;
|
||||
private boolean mIs24HourMode;
|
||||
private boolean mHideAmPm;
|
||||
private int mCurrentItemShowing;
|
||||
|
||||
private CircleView mCircleView;
|
||||
private AmPmCirclesView mAmPmCirclesView;
|
||||
private RadialTextsView mHourRadialTextsView;
|
||||
private RadialTextsView mMinuteRadialTextsView;
|
||||
private RadialSelectorView mHourRadialSelectorView;
|
||||
private RadialSelectorView mMinuteRadialSelectorView;
|
||||
private View mGrayBox;
|
||||
|
||||
private int[] mSnapPrefer30sMap;
|
||||
private boolean mInputEnabled;
|
||||
private int mIsTouchingAmOrPm = -1;
|
||||
private boolean mDoingMove;
|
||||
private boolean mDoingTouch;
|
||||
private int mDownDegrees;
|
||||
private float mDownX;
|
||||
private float mDownY;
|
||||
private AccessibilityManager mAccessibilityManager;
|
||||
|
||||
private AnimatorSet mTransition;
|
||||
private Handler mHandler = new Handler();
|
||||
|
||||
public void setColor(int selectedColor)
|
||||
{
|
||||
mHourRadialSelectorView.mPaint.setColor(selectedColor);
|
||||
mMinuteRadialSelectorView.mPaint.setColor(selectedColor);
|
||||
mAmPmCirclesView.mSelectedColor = selectedColor;
|
||||
mAmPmCirclesView.mAmPmTextColor = selectedColor;
|
||||
}
|
||||
|
||||
public interface OnValueSelectedListener {
|
||||
void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance);
|
||||
}
|
||||
|
||||
public RadialPickerLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
setOnTouchListener(this);
|
||||
ViewConfiguration vc = ViewConfiguration.get(context);
|
||||
TOUCH_SLOP = vc.getScaledTouchSlop();
|
||||
TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
|
||||
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);
|
||||
|
||||
// Prepare mapping to snap touchable degrees to selectable degrees.
|
||||
preparePrefer30sMap();
|
||||
|
||||
mLastValueSelected = -1;
|
||||
|
||||
mInputEnabled = true;
|
||||
mGrayBox = new View(context);
|
||||
mGrayBox.setLayoutParams(new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
mGrayBox.setBackgroundColor(getResources().getColor(R.color.transparent_black));
|
||||
mGrayBox.setVisibility(View.INVISIBLE);
|
||||
addView(mGrayBox);
|
||||
|
||||
mAccessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
|
||||
|
||||
mTimeInitialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure the view to end up as a square, based on the minimum of the height and width.
|
||||
*/
|
||||
@Override
|
||||
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
|
||||
int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
|
||||
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
|
||||
int minDimension = Math.min(measuredWidth, measuredHeight);
|
||||
|
||||
super.onMeasure(MeasureSpec.makeMeasureSpec(minDimension, widthMode),
|
||||
MeasureSpec.makeMeasureSpec(minDimension, heightMode));
|
||||
}
|
||||
|
||||
public void setOnValueSelectedListener(OnValueSelectedListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Layout with starting values.
|
||||
* @param context
|
||||
* @param initialHoursOfDay
|
||||
* @param initialMinutes
|
||||
* @param is24HourMode
|
||||
*/
|
||||
public void initialize(Context context, HapticFeedbackController hapticFeedbackController,
|
||||
int initialHoursOfDay, int initialMinutes, boolean is24HourMode) {
|
||||
if (mTimeInitialized) {
|
||||
Log.e(TAG, "Time has already been initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
mHapticFeedbackController = hapticFeedbackController;
|
||||
mIs24HourMode = is24HourMode;
|
||||
mHideAmPm = mAccessibilityManager.isTouchExplorationEnabled()? true : mIs24HourMode;
|
||||
|
||||
// Initialize the circle and AM/PM circles if applicable.
|
||||
mCircleView.initialize(context, mHideAmPm);
|
||||
mCircleView.invalidate();
|
||||
if (!mHideAmPm) {
|
||||
mAmPmCirclesView.initialize(context, initialHoursOfDay < 12? AM : PM);
|
||||
mAmPmCirclesView.invalidate();
|
||||
}
|
||||
|
||||
// Initialize the hours and minutes numbers.
|
||||
Resources res = context.getResources();
|
||||
int[] hours = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
|
||||
int[] hours_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23};
|
||||
int[] minutes = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55};
|
||||
String[] hoursTexts = new String[12];
|
||||
String[] innerHoursTexts = new String[12];
|
||||
String[] minutesTexts = new String[12];
|
||||
for (int i = 0; i < 12; i++) {
|
||||
hoursTexts[i] = is24HourMode?
|
||||
String.format("%02d", hours_24[i]) : String.format("%d", hours[i]);
|
||||
innerHoursTexts[i] = String.format("%d", hours[i]);
|
||||
minutesTexts[i] = String.format("%02d", minutes[i]);
|
||||
}
|
||||
mHourRadialTextsView.initialize(res,
|
||||
hoursTexts, (is24HourMode? innerHoursTexts : null), mHideAmPm, true);
|
||||
mHourRadialTextsView.invalidate();
|
||||
mMinuteRadialTextsView.initialize(res, minutesTexts, null, mHideAmPm, false);
|
||||
mMinuteRadialTextsView.invalidate();
|
||||
|
||||
// Initialize the currently-selected hour and minute.
|
||||
setValueForItem(HOUR_INDEX, initialHoursOfDay);
|
||||
setValueForItem(MINUTE_INDEX, initialMinutes);
|
||||
int hourDegrees = (initialHoursOfDay % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE;
|
||||
mHourRadialSelectorView.initialize(context, mHideAmPm, is24HourMode, true,
|
||||
hourDegrees, isHourInnerCircle(initialHoursOfDay));
|
||||
int minuteDegrees = initialMinutes * MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
|
||||
mMinuteRadialSelectorView.initialize(context, mHideAmPm, false, false,
|
||||
minuteDegrees, false);
|
||||
|
||||
mTimeInitialized = true;
|
||||
}
|
||||
|
||||
/* package */ void setTheme(Context context, boolean themeDark) {
|
||||
mCircleView.setTheme(context, themeDark);
|
||||
mAmPmCirclesView.setTheme(context, themeDark);
|
||||
mHourRadialTextsView.setTheme(context, themeDark);
|
||||
mMinuteRadialTextsView.setTheme(context, themeDark);
|
||||
mHourRadialSelectorView.setTheme(context, themeDark);
|
||||
mMinuteRadialSelectorView.setTheme(context, themeDark);
|
||||
}
|
||||
|
||||
public void setTime(int hours, int minutes) {
|
||||
setItem(HOUR_INDEX, hours);
|
||||
setItem(MINUTE_INDEX, minutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set either the hour or the minute. Will set the internal value, and set the selection.
|
||||
*/
|
||||
private void setItem(int index, int value) {
|
||||
if (index == HOUR_INDEX) {
|
||||
setValueForItem(HOUR_INDEX, value);
|
||||
int hourDegrees = (value % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE;
|
||||
mHourRadialSelectorView.setSelection(hourDegrees, isHourInnerCircle(value), false);
|
||||
mHourRadialSelectorView.invalidate();
|
||||
} else if (index == MINUTE_INDEX) {
|
||||
setValueForItem(MINUTE_INDEX, value);
|
||||
int minuteDegrees = value * MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
|
||||
mMinuteRadialSelectorView.setSelection(minuteDegrees, false, false);
|
||||
mMinuteRadialSelectorView.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given hour appears in the outer circle or the inner circle
|
||||
* @return true if the hour is in the inner circle, false if it's in the outer circle.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the hours are showing, return the current hour. If the minutes are showing, return the
|
||||
* current minute.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the internal value for the hour, minute, or AM/PM.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the internal value as either AM or PM, and update the AM/PM circle displays.
|
||||
* @param amOrPm
|
||||
*/
|
||||
public void setAmOrPm(int amOrPm) {
|
||||
mAmPmCirclesView.setAmOrPm(amOrPm);
|
||||
mAmPmCirclesView.invalidate();
|
||||
setValueForItem(AMPM_INDEX, amOrPm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger
|
||||
* selectable area to each of the 12 visible values, such that the ratio of space apportioned
|
||||
* to a visible value : space apportioned to a non-visible value will be 14 : 4.
|
||||
* E.g. the output of 30 degrees should have a higher range of input associated with it than
|
||||
* the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock
|
||||
* circle (5 on the minutes, 1 or 13 on the hours).
|
||||
*/
|
||||
private void preparePrefer30sMap() {
|
||||
// We'll split up the visible output and the non-visible output such that each visible
|
||||
// output will correspond to a range of 14 associated input degrees, and each non-visible
|
||||
// output will correspond to a range of 4 associate input degrees, so visible numbers
|
||||
// are more than 3 times easier to get than non-visible numbers:
|
||||
// {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc.
|
||||
//
|
||||
// If an output of 30 degrees should correspond to a range of 14 associated degrees, then
|
||||
// we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should
|
||||
// snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you
|
||||
// can be touching 36 degrees but have the selection snapped to 30 degrees; however, this
|
||||
// inconsistency isn't noticeable at such fine-grained degrees, and it affords us the
|
||||
// ability to aggressively prefer the visible values by a factor of more than 3:1, which
|
||||
// greatly contributes to the selectability of these values.
|
||||
|
||||
// Our input will be 0 through 360.
|
||||
mSnapPrefer30sMap = new int[361];
|
||||
|
||||
// The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}.
|
||||
int snappedOutputDegrees = 0;
|
||||
// Count of how many inputs we've designated to the specified output.
|
||||
int count = 1;
|
||||
// How many input we expect for a specified output. This will be 14 for output divisible
|
||||
// by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so
|
||||
// the caller can decide which they need.
|
||||
int expectedCount = 8;
|
||||
// Iterate through the input.
|
||||
for (int degrees = 0; degrees < 361; degrees++) {
|
||||
// Save the input-output mapping.
|
||||
mSnapPrefer30sMap[degrees] = snappedOutputDegrees;
|
||||
// If this is the last input for the specified output, calculate the next output and
|
||||
// the next expected count.
|
||||
if (count == expectedCount) {
|
||||
snappedOutputDegrees += 6;
|
||||
if (snappedOutputDegrees == 360) {
|
||||
expectedCount = 7;
|
||||
} else if (snappedOutputDegrees % 30 == 0) {
|
||||
expectedCount = 14;
|
||||
} else {
|
||||
expectedCount = 4;
|
||||
}
|
||||
count = 1;
|
||||
} else {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees,
|
||||
* where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be
|
||||
* weighted heavier than the degrees corresponding to non-visible numbers.
|
||||
* See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the
|
||||
* mapping.
|
||||
*/
|
||||
private int snapPrefer30s(int degrees) {
|
||||
if (mSnapPrefer30sMap == null) {
|
||||
return -1;
|
||||
}
|
||||
return mSnapPrefer30sMap[degrees];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all
|
||||
* multiples of 30), where the input will be "snapped" to the closest visible degrees.
|
||||
* @param degrees The input degrees
|
||||
* @param forceAboveOrBelow The output may be forced to either the higher or lower step, or may
|
||||
* be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force
|
||||
* strictly lower, and 0 to snap to the closer one.
|
||||
* @return output degrees, will be a multiple of 30
|
||||
*/
|
||||
private static int snapOnly30s(int degrees, int forceHigherOrLower) {
|
||||
int stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
|
||||
int floor = (degrees / stepSize) * stepSize;
|
||||
int ceiling = floor + stepSize;
|
||||
if (forceHigherOrLower == 1) {
|
||||
degrees = ceiling;
|
||||
} else if (forceHigherOrLower == -1) {
|
||||
if (degrees == floor) {
|
||||
floor -= stepSize;
|
||||
}
|
||||
degrees = floor;
|
||||
} else {
|
||||
if ((degrees - floor) < (ceiling - degrees)) {
|
||||
degrees = floor;
|
||||
} else {
|
||||
degrees = ceiling;
|
||||
}
|
||||
}
|
||||
return degrees;
|
||||
}
|
||||
|
||||
/**
|
||||
* For the currently showing view (either hours or minutes), re-calculate the position for the
|
||||
* selector, and redraw it at that position. The input degrees will be snapped to a selectable
|
||||
* value.
|
||||
* @param degrees Degrees which should be selected.
|
||||
* @param isInnerCircle Whether the selection should be in the inner circle; will be ignored
|
||||
* if there is no inner circle.
|
||||
* @param forceToVisibleValue Even if the currently-showing circle allows for fine-grained
|
||||
* selection (i.e. minutes), force the selection to one of the visibly-showing values.
|
||||
* @param forceDrawDot The dot in the circle will generally only be shown when the selection
|
||||
* is on non-visible values, but use this to force the dot to be shown.
|
||||
* @return The value that was selected, i.e. 0-23 for hours, 0-59 for minutes.
|
||||
*/
|
||||
private int reselectSelector(int degrees, boolean isInnerCircle,
|
||||
boolean forceToVisibleValue, boolean forceDrawDot) {
|
||||
if (degrees == -1) {
|
||||
return -1;
|
||||
}
|
||||
int currentShowing = getCurrentItemShowing();
|
||||
|
||||
int stepSize;
|
||||
boolean allowFineGrained = !forceToVisibleValue && (currentShowing == MINUTE_INDEX);
|
||||
if (allowFineGrained) {
|
||||
degrees = snapPrefer30s(degrees);
|
||||
} else {
|
||||
degrees = snapOnly30s(degrees, 0);
|
||||
}
|
||||
|
||||
RadialSelectorView radialSelectorView;
|
||||
if (currentShowing == HOUR_INDEX) {
|
||||
radialSelectorView = mHourRadialSelectorView;
|
||||
stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
|
||||
} else {
|
||||
radialSelectorView = mMinuteRadialSelectorView;
|
||||
stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
|
||||
}
|
||||
radialSelectorView.setSelection(degrees, isInnerCircle, 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the degrees within the circle that corresponds to the specified coordinates, if
|
||||
* the coordinates are within the range that will trigger a selection.
|
||||
* @param pointX The x coordinate.
|
||||
* @param pointY The y coordinate.
|
||||
* @param forceLegal Force the selection to be legal, regardless of how far the coordinates are
|
||||
* from the actual numbers.
|
||||
* @param isInnerCircle If the selection may be in the inner circle, pass in a size-1 boolean
|
||||
* array here, inside which the value will be true if the selection is in the inner circle,
|
||||
* and false if in the outer circle.
|
||||
* @return Degrees from 0 to 360, if the selection was within the legal range. -1 if not.
|
||||
*/
|
||||
private int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal,
|
||||
final Boolean[] isInnerCircle) {
|
||||
int currentItem = getCurrentItemShowing();
|
||||
if (currentItem == HOUR_INDEX) {
|
||||
return mHourRadialSelectorView.getDegreesFromCoords(
|
||||
pointX, pointY, forceLegal, isInnerCircle);
|
||||
} else if (currentItem == MINUTE_INDEX) {
|
||||
return mMinuteRadialSelectorView.getDegreesFromCoords(
|
||||
pointX, pointY, forceLegal, isInnerCircle);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item (hours or minutes) that is currently showing.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set either minutes or hours as showing.
|
||||
* @param animate True to animate the transition, false to show with no animation.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
int lastIndex = getCurrentItemShowing();
|
||||
mCurrentItemShowing = index;
|
||||
|
||||
if (animate && (index != lastIndex)) {
|
||||
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();
|
||||
}
|
||||
|
||||
if (mTransition != null && mTransition.isRunning()) {
|
||||
mTransition.end();
|
||||
}
|
||||
mTransition = new AnimatorSet();
|
||||
mTransition.playTogether(anims);
|
||||
mTransition.start();
|
||||
} else {
|
||||
int hourAlpha = (index == HOUR_INDEX) ? 255 : 0;
|
||||
int minuteAlpha = (index == MINUTE_INDEX) ? 255 : 0;
|
||||
mHourRadialTextsView.setAlpha(hourAlpha);
|
||||
mHourRadialSelectorView.setAlpha(hourAlpha);
|
||||
mMinuteRadialTextsView.setAlpha(minuteAlpha);
|
||||
mMinuteRadialSelectorView.setAlpha(minuteAlpha);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
final float eventX = event.getX();
|
||||
final float eventY = event.getY();
|
||||
int degrees;
|
||||
int value;
|
||||
final Boolean[] isInnerCircle = new Boolean[1];
|
||||
isInnerCircle[0] = false;
|
||||
|
||||
switch(event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
if (!mInputEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
mDownX = eventX;
|
||||
mDownY = eventY;
|
||||
|
||||
mLastValueSelected = -1;
|
||||
mDoingMove = false;
|
||||
mDoingTouch = true;
|
||||
// If we're showing the AM/PM, check to see if the user is touching it.
|
||||
if (!mHideAmPm) {
|
||||
mIsTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
|
||||
} else {
|
||||
mIsTouchingAmOrPm = -1;
|
||||
}
|
||||
if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
|
||||
// If the touch is on AM or PM, set it as "touched" after the TAP_TIMEOUT
|
||||
// in case the user moves their finger quickly.
|
||||
mHapticFeedbackController.tryVibrate();
|
||||
mDownDegrees = -1;
|
||||
mHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mAmPmCirclesView.setAmOrPmPressed(mIsTouchingAmOrPm);
|
||||
mAmPmCirclesView.invalidate();
|
||||
}
|
||||
}, TAP_TIMEOUT);
|
||||
} else {
|
||||
// If we're in accessibility mode, force the touch to be legal. Otherwise,
|
||||
// it will only register within the given touch target zone.
|
||||
boolean forceLegal = mAccessibilityManager.isTouchExplorationEnabled();
|
||||
// Calculate the degrees that is currently being touched.
|
||||
mDownDegrees = getDegreesFromCoords(eventX, eventY, forceLegal, isInnerCircle);
|
||||
if (mDownDegrees != -1) {
|
||||
// If it's a legal touch, set that number as "selected" after the
|
||||
// TAP_TIMEOUT in case the user moves their finger quickly.
|
||||
mHapticFeedbackController.tryVibrate();
|
||||
mHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mDoingMove = true;
|
||||
int value = reselectSelector(mDownDegrees, isInnerCircle[0],
|
||||
false, true);
|
||||
mLastValueSelected = value;
|
||||
mListener.onValueSelected(getCurrentItemShowing(), value, false);
|
||||
}
|
||||
}, TAP_TIMEOUT);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (!mInputEnabled) {
|
||||
// We shouldn't be in this state, because input is disabled.
|
||||
Log.e(TAG, "Input was disabled, but received ACTION_MOVE.");
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// We're doing a move along the circle, so move the selection as appropriate.
|
||||
mDoingMove = true;
|
||||
mHandler.removeCallbacksAndMessages(null);
|
||||
degrees = getDegreesFromCoords(eventX, eventY, true, isInnerCircle);
|
||||
if (degrees != -1) {
|
||||
value = reselectSelector(degrees, isInnerCircle[0], false, true);
|
||||
if (value != mLastValueSelected) {
|
||||
mHapticFeedbackController.tryVibrate();
|
||||
mLastValueSelected = value;
|
||||
mListener.onValueSelected(getCurrentItemShowing(), value, false);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (!mInputEnabled) {
|
||||
// If our touch input was disabled, tell the listener to re-enable us.
|
||||
Log.d(TAG, "Input was disabled, but received ACTION_UP.");
|
||||
mListener.onValueSelected(ENABLE_PICKER_INDEX, 1, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
mHandler.removeCallbacksAndMessages(null);
|
||||
mDoingTouch = false;
|
||||
|
||||
// If we're touching AM or PM, set it as selected, and tell the listener.
|
||||
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 we have a legal degrees selected, set the value and tell the listener.
|
||||
if (mDownDegrees != -1) {
|
||||
degrees = getDegreesFromCoords(eventX, eventY, mDoingMove, isInnerCircle);
|
||||
if (degrees != -1) {
|
||||
value = reselectSelector(degrees, isInnerCircle[0], !mDoingMove, false);
|
||||
if (getCurrentItemShowing() == HOUR_INDEX && !mIs24HourMode) {
|
||||
int amOrPm = getIsCurrentlyAmOrPm();
|
||||
if (amOrPm == AM && value == 12) {
|
||||
value = 0;
|
||||
} else if (amOrPm == PM && value != 12) {
|
||||
value += 12;
|
||||
}
|
||||
}
|
||||
setValueForItem(getCurrentItemShowing(), value);
|
||||
mListener.onValueSelected(getCurrentItemShowing(), value, true);
|
||||
}
|
||||
}
|
||||
mDoingMove = false;
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set touch input as enabled or disabled, for use with keyboard mode.
|
||||
*/
|
||||
public boolean trySettingInputEnabled(boolean inputEnabled) {
|
||||
if (mDoingTouch && !inputEnabled) {
|
||||
// If we're trying to disable input, but we're in the middle of a touch event,
|
||||
// we'll allow the touch event to continue before disabling input.
|
||||
return false;
|
||||
}
|
||||
mInputEnabled = inputEnabled;
|
||||
mGrayBox.setVisibility(inputEnabled? View.INVISIBLE : View.VISIBLE);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Necessary for accessibility, to ensure we support "scrolling" forward and backward
|
||||
* in the circle.
|
||||
*/
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
||||
super.onInitializeAccessibilityNodeInfo(info);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Announce the currently-selected time when launched.
|
||||
*/
|
||||
@Override
|
||||
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
|
||||
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
|
||||
// Clear the event's current text so that only the current time will be spoken.
|
||||
event.getText().clear();
|
||||
Time time = new Time();
|
||||
time.hour = getHours();
|
||||
time.minute = getMinutes();
|
||||
long millis = time.normalize(true);
|
||||
int flags = DateUtils.FORMAT_SHOW_TIME;
|
||||
if (mIs24HourMode) {
|
||||
flags |= DateUtils.FORMAT_24HOUR;
|
||||
}
|
||||
String timeString = DateUtils.formatDateTime(getContext(), millis, flags);
|
||||
event.getText().add(timeString);
|
||||
return true;
|
||||
}
|
||||
return super.dispatchPopulateAccessibilityEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* When scroll forward/backward events are received, jump the time to the higher/lower
|
||||
* discrete, visible value on the circle.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
public boolean performAccessibilityAction(int action, Bundle arguments) {
|
||||
if (super.performAccessibilityAction(action, arguments)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int changeMultiplier = 0;
|
||||
if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
|
||||
changeMultiplier = 1;
|
||||
} else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
|
||||
changeMultiplier = -1;
|
||||
}
|
||||
if (changeMultiplier != 0) {
|
||||
int value = getCurrentlyShowingValue();
|
||||
int stepSize = 0;
|
||||
int currentItemShowing = getCurrentItemShowing();
|
||||
if (currentItemShowing == HOUR_INDEX) {
|
||||
stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
|
||||
value %= 12;
|
||||
} else if (currentItemShowing == MINUTE_INDEX) {
|
||||
stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
|
||||
}
|
||||
|
||||
int degrees = value * stepSize;
|
||||
degrees = snapOnly30s(degrees, changeMultiplier);
|
||||
value = degrees / stepSize;
|
||||
int maxValue = 0;
|
||||
int minValue = 0;
|
||||
if (currentItemShowing == HOUR_INDEX) {
|
||||
if (mIs24HourMode) {
|
||||
maxValue = 23;
|
||||
} else {
|
||||
maxValue = 12;
|
||||
minValue = 1;
|
||||
}
|
||||
} else {
|
||||
maxValue = 55;
|
||||
}
|
||||
if (value > maxValue) {
|
||||
// If we scrolled forward past the highest number, wrap around to the lowest.
|
||||
value = minValue;
|
||||
} else if (value < minValue) {
|
||||
// If we scrolled backward past the lowest number, wrap around to the highest.
|
||||
value = maxValue;
|
||||
}
|
||||
setItem(currentItemShowing, value);
|
||||
mListener.onValueSelected(currentItemShowing, value, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
/*
|
||||
* 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.time;
|
||||
|
||||
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.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.datetimepicker.Utils;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
/**
|
||||
* View to show what number is selected. This will draw a blue circle over the number, with a blue
|
||||
* line coming from the center of the main circle to the edge of the blue selection.
|
||||
*/
|
||||
public class RadialSelectorView extends View {
|
||||
private static final String TAG = "RadialSelectorView";
|
||||
|
||||
// Alpha level for selected circle.
|
||||
private static final int SELECTED_ALPHA = Utils.SELECTED_ALPHA;
|
||||
private static final int SELECTED_ALPHA_THEME_DARK = Utils.SELECTED_ALPHA_THEME_DARK;
|
||||
// Alpha level for the line.
|
||||
private static final int FULL_ALPHA = Utils.FULL_ALPHA;
|
||||
|
||||
protected 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 mSelectionAlpha;
|
||||
|
||||
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 mForceDrawDot;
|
||||
|
||||
public RadialSelectorView(Context context) {
|
||||
super(context);
|
||||
mIsInitialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize this selector with the state of the picker.
|
||||
* @param context Current context.
|
||||
* @param is24HourMode Whether the selector is in 24-hour mode, which will tell us
|
||||
* whether the circle's center is moved up slightly to make room for the AM/PM circles.
|
||||
* @param hasInnerCircle Whether we have both an inner and an outer circle of numbers
|
||||
* that may be selected. Should be true for 24-hour mode in the hours circle.
|
||||
* @param disappearsOut Whether the numbers' animation will have them disappearing out
|
||||
* or disappearing in.
|
||||
* @param selectionDegrees The initial degrees to be selected.
|
||||
* @param isInnerCircle Whether the initial selection is in the inner or outer circle.
|
||||
* Will be ignored when hasInnerCircle is false.
|
||||
*/
|
||||
public void initialize(Context context, boolean is24HourMode, boolean hasInnerCircle,
|
||||
boolean disappearsOut, int selectionDegrees, boolean isInnerCircle) {
|
||||
if (mIsInitialized) {
|
||||
Log.e(TAG, "This RadialSelectorView may only be initialized once.");
|
||||
return;
|
||||
}
|
||||
|
||||
Resources res = context.getResources();
|
||||
|
||||
mPaint.setAntiAlias(true);
|
||||
mSelectionAlpha = SELECTED_ALPHA;
|
||||
|
||||
// Calculate values for the circle radius size.
|
||||
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));
|
||||
}
|
||||
|
||||
// Calculate values for the radius size(s) of the numbers circle(s).
|
||||
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));
|
||||
|
||||
// Calculate values for the transition mid-way states.
|
||||
mAnimationRadiusMultiplier = 1;
|
||||
mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut? -1 : 1));
|
||||
mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut? 1 : -1));
|
||||
mInvalidateUpdateListener = new InvalidateUpdateListener();
|
||||
|
||||
setSelection(selectionDegrees, isInnerCircle, false);
|
||||
mIsInitialized = true;
|
||||
}
|
||||
|
||||
/* package */ void setTheme(Context context, boolean themeDark) {
|
||||
Resources res = context.getResources();
|
||||
if (themeDark) {
|
||||
mSelectionAlpha = SELECTED_ALPHA_THEME_DARK;
|
||||
} else {
|
||||
mSelectionAlpha = SELECTED_ALPHA;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selection.
|
||||
* @param selectionDegrees The degrees to be selected.
|
||||
* @param isInnerCircle Whether the selection should be in the inner circle or outer. Will be
|
||||
* ignored if hasInnerCircle was initialized to false.
|
||||
* @param forceDrawDot Whether to force the dot in the center of the selection circle to be
|
||||
* drawn. If false, the dot will be drawn only when the degrees is not a multiple of 30, i.e.
|
||||
* the selection is not on a visible number.
|
||||
*/
|
||||
public void setSelection(int selectionDegrees, boolean isInnerCircle, boolean forceDrawDot) {
|
||||
mSelectionDegrees = selectionDegrees;
|
||||
mSelectionRadians = selectionDegrees * Math.PI / 180;
|
||||
mForceDrawDot = forceDrawDot;
|
||||
|
||||
if (mHasInnerCircle) {
|
||||
if (isInnerCircle) {
|
||||
mNumbersRadiusMultiplier = mInnerNumbersRadiusMultiplier;
|
||||
} else {
|
||||
mNumbersRadiusMultiplier = mOuterNumbersRadiusMultiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows for smoother animations.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasOverlappingRendering() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the multiplier for the radius. Will be used during animations to move in/out.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// Calculate the current radius at which to place the selection circle.
|
||||
mLineLength = (int) (mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier);
|
||||
int pointX = mXCenter + (int) (mLineLength * Math.sin(mSelectionRadians));
|
||||
int pointY = mYCenter - (int) (mLineLength * Math.cos(mSelectionRadians));
|
||||
|
||||
// Draw the selection circle.
|
||||
mPaint.setAlpha(mSelectionAlpha);
|
||||
canvas.drawCircle(pointX, pointY, mSelectionRadius, mPaint);
|
||||
|
||||
if (mForceDrawDot | mSelectionDegrees % 30 != 0) {
|
||||
// We're not on a direct tick (or we've been told to draw the dot anyway).
|
||||
mPaint.setAlpha(FULL_ALPHA);
|
||||
canvas.drawCircle(pointX, pointY, (mSelectionRadius * 2 / 7), mPaint);
|
||||
} else {
|
||||
// We're not drawing the dot, so shorten the line to only go as far as the edge of the
|
||||
// selection circle.
|
||||
int lineLength = mLineLength;
|
||||
lineLength -= mSelectionRadius;
|
||||
pointX = mXCenter + (int) (lineLength * Math.sin(mSelectionRadians));
|
||||
pointY = mYCenter - (int) (lineLength * Math.cos(mSelectionRadians));
|
||||
}
|
||||
|
||||
// Draw the line from the center of the circle.
|
||||
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.25f;
|
||||
float transitionDurationMultiplier = 1f;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* We'll need to invalidate during the animation.
|
||||
*/
|
||||
private class InvalidateUpdateListener implements AnimatorUpdateListener {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
RadialSelectorView.this.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* 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.time;
|
||||
|
||||
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.Paint.Align;
|
||||
import android.graphics.Typeface;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
/**
|
||||
* A view to show a series of numbers in a circular pattern.
|
||||
*/
|
||||
public class RadialTextsView extends View {
|
||||
private final static String TAG = "RadialTextsView";
|
||||
|
||||
private final Paint mPaint = new Paint();
|
||||
|
||||
private boolean mDrawValuesReady;
|
||||
private boolean mIsInitialized;
|
||||
|
||||
private Typeface mTypefaceLight;
|
||||
private Typeface mTypefaceRegular;
|
||||
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;
|
||||
}
|
||||
|
||||
// Set up the paint.
|
||||
int numbersTextColor = res.getColor(R.color.numbers_text_color);
|
||||
mPaint.setColor(numbersTextColor);
|
||||
String typefaceFamily = res.getString(R.string.radial_numbers_typeface);
|
||||
mTypefaceLight = Typeface.create(typefaceFamily, Typeface.NORMAL);
|
||||
String typefaceFamilyRegular = res.getString(R.string.sans_serif);
|
||||
mTypefaceRegular = Typeface.create(typefaceFamilyRegular, Typeface.NORMAL);
|
||||
mPaint.setAntiAlias(true);
|
||||
mPaint.setTextAlign(Align.CENTER);
|
||||
|
||||
mTexts = texts;
|
||||
mInnerTexts = innerTexts;
|
||||
mIs24HourMode = is24HourMode;
|
||||
mHasInnerCircle = (innerTexts != null);
|
||||
|
||||
// Calculate the radius for the main circle.
|
||||
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));
|
||||
}
|
||||
|
||||
// Initialize the widths and heights of the grid, and calculate the values for the numbers.
|
||||
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;
|
||||
}
|
||||
|
||||
/* package */ void setTheme(Context context, boolean themeDark) {
|
||||
Resources res = context.getResources();
|
||||
int textColor;
|
||||
if (themeDark) {
|
||||
textColor = res.getColor(R.color.white);
|
||||
} else {
|
||||
textColor = res.getColor(R.color.numbers_text_color);
|
||||
}
|
||||
mPaint.setColor(textColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows for smoother animation.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasOverlappingRendering() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the animation to move the numbers in and out.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// Because the text positions will be static, pre-render the animations.
|
||||
renderAnimations();
|
||||
|
||||
mTextGridValuesDirty = true;
|
||||
mDrawValuesReady = true;
|
||||
}
|
||||
|
||||
// Calculate the text positions, but only if they've changed since the last onDraw.
|
||||
if (mTextGridValuesDirty) {
|
||||
float numbersRadius =
|
||||
mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier;
|
||||
|
||||
// Calculate the positions for the 12 numbers in the main circle.
|
||||
calculateGridSizes(numbersRadius, mXCenter, mYCenter,
|
||||
mTextSize, mTextGridHeights, mTextGridWidths);
|
||||
if (mHasInnerCircle) {
|
||||
// If we have an inner circle, calculate those positions too.
|
||||
float innerNumbersRadius =
|
||||
mCircleRadius * mInnerNumbersRadiusMultiplier * mAnimationRadiusMultiplier;
|
||||
calculateGridSizes(innerNumbersRadius, mXCenter, mYCenter,
|
||||
mInnerTextSize, mInnerTextGridHeights, mInnerTextGridWidths);
|
||||
}
|
||||
mTextGridValuesDirty = false;
|
||||
}
|
||||
|
||||
// Draw the texts in the pre-calculated positions.
|
||||
drawTexts(canvas, mTextSize, mTypefaceLight, mTexts, mTextGridWidths, mTextGridHeights);
|
||||
if (mHasInnerCircle) {
|
||||
drawTexts(canvas, mInnerTextSize, mTypefaceRegular, mInnerTexts,
|
||||
mInnerTextGridWidths, mInnerTextGridHeights);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the trigonometric Unit Circle, calculate the positions that the text will need to be
|
||||
* drawn at based on the specified circle radius. Place the values in the textGridHeights and
|
||||
* textGridWidths parameters.
|
||||
*/
|
||||
private void calculateGridSizes(float numbersRadius, float xCenter, float yCenter,
|
||||
float textSize, float[] textGridHeights, float[] textGridWidths) {
|
||||
/*
|
||||
* 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;
|
||||
mPaint.setTextSize(textSize);
|
||||
// We'll need yTextBase to be slightly lower to account for the text's baseline.
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the 12 text values at the positions specified by the textGrid parameters.
|
||||
*/
|
||||
private void drawTexts(Canvas canvas, float textSize, Typeface typeface, String[] texts,
|
||||
float[] textGridWidths, float[] textGridHeights) {
|
||||
mPaint.setTextSize(textSize);
|
||||
mPaint.setTypeface(typeface);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the animations for appearing and disappearing.
|
||||
*/
|
||||
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.25f;
|
||||
float transitionDurationMultiplier = 1f;
|
||||
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 || mDisappearAnimator == null) {
|
||||
Log.e(TAG, "RadialTextView was not ready for animation.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return mDisappearAnimator;
|
||||
}
|
||||
|
||||
public ObjectAnimator getReappearAnimator() {
|
||||
if (!mIsInitialized || !mDrawValuesReady || mReappearAnimator == null) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.gui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.Typeface
|
||||
import android.text.TextPaint
|
||||
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
|
||||
|
||||
class AndroidCanvas : Canvas {
|
||||
|
||||
lateinit var context: Context
|
||||
|
||||
lateinit var innerCanvas: android.graphics.Canvas
|
||||
var innerBitmap: Bitmap? = null
|
||||
var innerDensity = 1.0
|
||||
var innerWidth = 0
|
||||
var innerHeight = 0
|
||||
|
||||
var paint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
}
|
||||
var textPaint = TextPaint().apply {
|
||||
isAntiAlias = true
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
var textBounds = Rect()
|
||||
|
||||
private fun Double.toDp() = (this * innerDensity).toFloat()
|
||||
|
||||
override fun setColor(color: Color) {
|
||||
paint.color = color.toInt()
|
||||
textPaint.color = color.toInt()
|
||||
}
|
||||
|
||||
override fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) {
|
||||
innerCanvas.drawLine(
|
||||
x1.toDp(),
|
||||
y1.toDp(),
|
||||
x2.toDp(),
|
||||
y2.toDp(),
|
||||
paint,
|
||||
)
|
||||
}
|
||||
|
||||
override fun drawText(text: String, x: Double, y: Double) {
|
||||
textPaint.getTextBounds(text, 0, text.length, textBounds)
|
||||
innerCanvas.drawText(
|
||||
text,
|
||||
x.toDp(),
|
||||
y.toDp() - textBounds.exactCenterY(),
|
||||
textPaint,
|
||||
)
|
||||
}
|
||||
|
||||
override fun fillRect(x: Double, y: Double, width: Double, height: Double) {
|
||||
paint.style = Paint.Style.FILL
|
||||
rect(x, y, width, height)
|
||||
}
|
||||
|
||||
override fun fillRoundRect(
|
||||
x: Double,
|
||||
y: Double,
|
||||
width: Double,
|
||||
height: Double,
|
||||
cornerRadius: Double,
|
||||
) {
|
||||
paint.style = Paint.Style.FILL
|
||||
innerCanvas.drawRoundRect(
|
||||
x.toDp(),
|
||||
y.toDp(),
|
||||
(x + width).toDp(),
|
||||
(y + height).toDp(),
|
||||
cornerRadius.toDp(),
|
||||
cornerRadius.toDp(),
|
||||
paint,
|
||||
)
|
||||
}
|
||||
|
||||
override fun drawRect(x: Double, y: Double, width: Double, height: Double) {
|
||||
paint.style = Paint.Style.STROKE
|
||||
rect(x, y, width, height)
|
||||
}
|
||||
|
||||
private fun rect(x: Double, y: Double, width: Double, height: Double) {
|
||||
innerCanvas.drawRect(
|
||||
x.toDp(),
|
||||
y.toDp(),
|
||||
(x + width).toDp(),
|
||||
(y + height).toDp(),
|
||||
paint,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getHeight(): Double {
|
||||
return innerHeight / innerDensity
|
||||
}
|
||||
|
||||
override fun getWidth(): Double {
|
||||
return innerWidth / innerDensity
|
||||
}
|
||||
|
||||
override fun setFont(font: Font) {
|
||||
textPaint.typeface = when (font) {
|
||||
Font.REGULAR -> Typeface.DEFAULT
|
||||
Font.BOLD -> Typeface.DEFAULT_BOLD
|
||||
Font.FONT_AWESOME -> getFontAwesome(context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setFontSize(size: Double) {
|
||||
textPaint.textSize = size.toDp()
|
||||
}
|
||||
|
||||
override fun setStrokeWidth(size: Double) {
|
||||
paint.strokeWidth = size.toDp()
|
||||
}
|
||||
|
||||
override fun fillArc(
|
||||
centerX: Double,
|
||||
centerY: Double,
|
||||
radius: Double,
|
||||
startAngle: Double,
|
||||
swipeAngle: Double,
|
||||
) {
|
||||
paint.style = Paint.Style.FILL
|
||||
innerCanvas.drawArc(
|
||||
(centerX - radius).toDp(),
|
||||
(centerY - radius).toDp(),
|
||||
(centerX + radius).toDp(),
|
||||
(centerY + radius).toDp(),
|
||||
-startAngle.toFloat(),
|
||||
-swipeAngle.toFloat(),
|
||||
true,
|
||||
paint,
|
||||
)
|
||||
}
|
||||
|
||||
override fun fillCircle(
|
||||
centerX: Double,
|
||||
centerY: Double,
|
||||
radius: Double,
|
||||
) {
|
||||
paint.style = Paint.Style.FILL
|
||||
innerCanvas.drawCircle(centerX.toDp(), centerY.toDp(), radius.toDp(), paint)
|
||||
}
|
||||
|
||||
override fun setTextAlign(align: TextAlign) {
|
||||
textPaint.textAlign = when (align) {
|
||||
TextAlign.LEFT -> Paint.Align.LEFT
|
||||
TextAlign.CENTER -> Paint.Align.CENTER
|
||||
TextAlign.RIGHT -> Paint.Align.RIGHT
|
||||
}
|
||||
}
|
||||
|
||||
override fun toImage(): Image {
|
||||
val bmp = innerBitmap ?: throw UnsupportedOperationException()
|
||||
return AndroidImage(bmp)
|
||||
}
|
||||
|
||||
override fun measureText(text: String): Double {
|
||||
return textPaint.measureText(text) / innerDensity
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.gui
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.widget.Scroller
|
||||
|
||||
/**
|
||||
* An AndroidView that implements scrolling.
|
||||
*/
|
||||
class AndroidDataView(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
) : AndroidView<DataView>(context, attrs),
|
||||
GestureDetector.OnGestureListener,
|
||||
ValueAnimator.AnimatorUpdateListener {
|
||||
|
||||
private val detector = GestureDetector(context, this)
|
||||
private val scroller = Scroller(context, null, true)
|
||||
private val scrollAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
addUpdateListener(this@AndroidDataView)
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent?) = detector.onTouchEvent(event)
|
||||
override fun onDown(e: MotionEvent?) = true
|
||||
override fun onShowPress(e: MotionEvent?) = Unit
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent?): Boolean {
|
||||
val x: Float
|
||||
val y: Float
|
||||
try {
|
||||
val pointerId = e!!.getPointerId(0)
|
||||
x = e.getX(pointerId)
|
||||
y = e.getY(pointerId)
|
||||
} catch (ex: RuntimeException) {
|
||||
// Android often throws IllegalArgumentException here. Apparently,
|
||||
// the pointer id may become invalid shortly after calling
|
||||
// e.getPointerId.
|
||||
return false
|
||||
}
|
||||
view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onLongPress(e: MotionEvent?) = Unit
|
||||
|
||||
override fun onScroll(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent?,
|
||||
dx: Float,
|
||||
dy: Float,
|
||||
): Boolean {
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
val parent = parent
|
||||
parent?.requestDisallowInterceptTouchEvent(true)
|
||||
}
|
||||
scroller.startScroll(
|
||||
scroller.currX,
|
||||
scroller.currY,
|
||||
-dx.toInt(),
|
||||
dy.toInt(),
|
||||
0
|
||||
)
|
||||
scroller.computeScrollOffset()
|
||||
updateDataOffset()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onFling(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent?,
|
||||
velocityX: Float,
|
||||
velocityY: Float,
|
||||
): Boolean {
|
||||
scroller.fling(
|
||||
scroller.currX,
|
||||
scroller.currY,
|
||||
velocityX.toInt() / 2,
|
||||
0,
|
||||
0,
|
||||
Integer.MAX_VALUE,
|
||||
0,
|
||||
0
|
||||
)
|
||||
invalidate()
|
||||
scrollAnimator.duration = scroller.duration.toLong()
|
||||
scrollAnimator.start()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onAnimationUpdate(animation: ValueAnimator?) {
|
||||
if (!scroller.isFinished) {
|
||||
scroller.computeScrollOffset()
|
||||
updateDataOffset()
|
||||
} else {
|
||||
scrollAnimator.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fun resetDataOffset() {
|
||||
scroller.finalX = 0
|
||||
scroller.computeScrollOffset()
|
||||
updateDataOffset()
|
||||
}
|
||||
|
||||
private fun updateDataOffset() {
|
||||
view?.let { v ->
|
||||
var newDataOffset: Int =
|
||||
scroller.currX / (v.dataColumnWidth * canvas.innerDensity).toInt()
|
||||
newDataOffset = Math.max(0, newDataOffset)
|
||||
if (newDataOffset != v.dataOffset) {
|
||||
v.dataOffset = newDataOffset
|
||||
postInvalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.gui
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class AndroidImage(private val bmp: Bitmap) : Image {
|
||||
override val width: Int
|
||||
get() = bmp.width
|
||||
|
||||
override val height: Int
|
||||
get() = bmp.height
|
||||
|
||||
override fun getPixel(x: Int, y: Int): Color {
|
||||
return Color(bmp.getPixel(x, y))
|
||||
}
|
||||
|
||||
override fun setPixel(x: Int, y: Int, color: Color) {
|
||||
bmp.setPixel(x, y, color.toInt())
|
||||
}
|
||||
|
||||
override suspend fun export(path: String) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
||||
public fun Color.toInt(): Int {
|
||||
return android.graphics.Color.argb(
|
||||
(255 * this.alpha).roundToInt(),
|
||||
(255 * this.red).roundToInt(),
|
||||
(255 * this.green).roundToInt(),
|
||||
(255 * this.blue).roundToInt(),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.gui
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
|
||||
class AndroidTestView(context: Context, attrs: AttributeSet) : android.view.View(context, attrs) {
|
||||
val canvas = AndroidCanvas()
|
||||
|
||||
override fun onDraw(canvas: android.graphics.Canvas) {
|
||||
this.canvas.context = context
|
||||
this.canvas.innerCanvas = canvas
|
||||
this.canvas.innerDensity = resources.displayMetrics.density.toDouble()
|
||||
this.canvas.drawTestImage()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.gui
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
|
||||
open class AndroidView<T : View>(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
) : android.view.View(context, attrs) {
|
||||
|
||||
var view: T? = null
|
||||
val canvas = AndroidCanvas()
|
||||
|
||||
override fun onDraw(canvas: android.graphics.Canvas) {
|
||||
this.canvas.context = context
|
||||
this.canvas.innerCanvas = canvas
|
||||
this.canvas.innerWidth = width
|
||||
this.canvas.innerHeight = height
|
||||
this.canvas.innerDensity = resources.displayMetrics.density.toDouble()
|
||||
view?.draw(this.canvas)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.view.WindowManager
|
||||
import org.isoron.uhabits.inject.AppContext
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
open class AndroidBugReporter @Inject constructor(@AppContext private val context: Context) {
|
||||
|
||||
/**
|
||||
* Captures and returns a bug report. The bug report contains some device
|
||||
* information and the logcat.
|
||||
*
|
||||
* @return a String containing the bug report.
|
||||
* @throws IOException when any I/O error occur.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun getBugReport(): String {
|
||||
var log = "---------- BUG REPORT BEGINS ----------\n"
|
||||
log += "${getLogcat()}\n"
|
||||
log += "${getDeviceInfo()}\n"
|
||||
log += "---------- BUG REPORT ENDS ------------\n"
|
||||
return log
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getLogcat(): String {
|
||||
val maxLineCount = 250
|
||||
val builder = StringBuilder()
|
||||
val process = Runtime.getRuntime().exec(arrayOf("logcat", "-d"))
|
||||
val inputReader = InputStreamReader(process.inputStream)
|
||||
val bufferedReader = BufferedReader(inputReader)
|
||||
val log = LinkedList<String>()
|
||||
var line: String?
|
||||
while (true) {
|
||||
line = bufferedReader.readLine()
|
||||
if (line == null) break
|
||||
log.addLast(line)
|
||||
if (log.size > maxLineCount) log.removeFirst()
|
||||
}
|
||||
for (l in log) {
|
||||
builder.appendln(l)
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a bug report and saves it to a file in the SD card.
|
||||
*
|
||||
* The contents of the file are generated by the method [ ][.getBugReport]. The file is saved
|
||||
* in the apps's external private storage.
|
||||
*
|
||||
* @return the generated file.
|
||||
* @throws IOException when I/O errors occur.
|
||||
*/
|
||||
fun dumpBugReportToFile() {
|
||||
try {
|
||||
val date = SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US).format(Date())
|
||||
val dir = AndroidDirFinder(context).getFilesDir("Logs")
|
||||
?: throw IOException("log dir should not be null")
|
||||
val logFile = File(String.format("%s/Log %s.txt", dir.path, date))
|
||||
val output = FileWriter(logFile)
|
||||
output.write(getBugReport())
|
||||
output.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDeviceInfo(): String {
|
||||
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
return buildString {
|
||||
appendln("App Version Name: ${BuildConfig.VERSION_NAME}")
|
||||
appendln("App Version Code: ${BuildConfig.VERSION_CODE}")
|
||||
appendln("OS Version: ${System.getProperty("os.version")} (${Build.VERSION.INCREMENTAL})")
|
||||
appendln("OS API Level: ${Build.VERSION.SDK_INT}")
|
||||
appendln("Device: ${Build.DEVICE}")
|
||||
appendln("Model (Product): ${Build.MODEL} (${Build.PRODUCT})")
|
||||
appendln("Manufacturer: ${Build.MANUFACTURER}")
|
||||
appendln("Other tags: ${Build.TAGS}")
|
||||
appendln("Screen Width: ${wm.defaultDisplay.width}")
|
||||
appendln("Screen Height: ${wm.defaultDisplay.height}")
|
||||
appendln("External storage state: ${Environment.getExternalStorageState()}")
|
||||
appendln()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.isoron.uhabits.inject.AppContext
|
||||
import org.isoron.uhabits.utils.FileUtils
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class AndroidDirFinder @Inject constructor(@param:AppContext private val context: Context) {
|
||||
fun getFilesDir(relativePath: String): File? {
|
||||
return FileUtils.getDir(
|
||||
ContextCompat.getExternalFilesDirs(context, null),
|
||||
relativePath
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
class BaseExceptionHandler(private val activity: Activity) : Thread.UncaughtExceptionHandler {
|
||||
|
||||
private val originalHandler: Thread.UncaughtExceptionHandler? =
|
||||
Thread.getDefaultUncaughtExceptionHandler()
|
||||
|
||||
override fun uncaughtException(thread: Thread?, ex: Throwable?) {
|
||||
if (ex == null) return
|
||||
if (thread == null) return
|
||||
try {
|
||||
ex.printStackTrace()
|
||||
AndroidBugReporter(activity).dumpBugReportToFile()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
originalHandler?.uncaughtException(thread, ex)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import org.isoron.uhabits.core.database.UnsupportedDatabaseVersionException
|
||||
import org.isoron.uhabits.core.reminders.ReminderScheduler
|
||||
import org.isoron.uhabits.core.ui.NotificationTray
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
import org.isoron.uhabits.inject.AppContextModule
|
||||
import org.isoron.uhabits.inject.DaggerHabitsApplicationComponent
|
||||
import org.isoron.uhabits.inject.HabitsApplicationComponent
|
||||
import org.isoron.uhabits.inject.HabitsModule
|
||||
import org.isoron.uhabits.utils.DatabaseUtils
|
||||
import org.isoron.uhabits.widgets.WidgetUpdater
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* The Android application for Loop Habit Tracker.
|
||||
*/
|
||||
class HabitsApplication : Application() {
|
||||
|
||||
private lateinit var context: Context
|
||||
private lateinit var widgetUpdater: WidgetUpdater
|
||||
private lateinit var reminderScheduler: ReminderScheduler
|
||||
private lateinit var notificationTray: NotificationTray
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
context = this
|
||||
|
||||
if (isTestMode()) {
|
||||
val db = DatabaseUtils.getDatabaseFile(context)
|
||||
if (db.exists()) db.delete()
|
||||
}
|
||||
|
||||
try {
|
||||
DatabaseUtils.initializeDatabase(context)
|
||||
} catch (e: UnsupportedDatabaseVersionException) {
|
||||
val db = DatabaseUtils.getDatabaseFile(context)
|
||||
db.renameTo(File(db.absolutePath + ".invalid"))
|
||||
DatabaseUtils.initializeDatabase(context)
|
||||
}
|
||||
|
||||
val db = DatabaseUtils.getDatabaseFile(this)
|
||||
HabitsApplication.component = DaggerHabitsApplicationComponent
|
||||
.builder()
|
||||
.appContextModule(AppContextModule(context))
|
||||
.habitsModule(HabitsModule(db))
|
||||
.build()
|
||||
|
||||
DateUtils.setStartDayOffset(3, 0)
|
||||
|
||||
val habitList = component.habitList
|
||||
for (h in habitList) h.recompute()
|
||||
|
||||
widgetUpdater = component.widgetUpdater
|
||||
widgetUpdater.startListening()
|
||||
widgetUpdater.scheduleStartDayWidgetUpdate()
|
||||
|
||||
reminderScheduler = component.reminderScheduler
|
||||
reminderScheduler.startListening()
|
||||
|
||||
notificationTray = component.notificationTray
|
||||
notificationTray.startListening()
|
||||
|
||||
val prefs = component.preferences
|
||||
prefs.setLastAppVersion(BuildConfig.VERSION_CODE)
|
||||
|
||||
val taskRunner = component.taskRunner
|
||||
taskRunner.execute {
|
||||
reminderScheduler.scheduleAll()
|
||||
widgetUpdater.updateWidgets()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
reminderScheduler.stopListening()
|
||||
widgetUpdater.stopListening()
|
||||
notificationTray.stopListening()
|
||||
super.onTerminate()
|
||||
}
|
||||
|
||||
val component: HabitsApplicationComponent
|
||||
get() = HabitsApplication.component
|
||||
|
||||
companion object {
|
||||
lateinit var component: HabitsApplicationComponent
|
||||
|
||||
fun isTestMode(): Boolean {
|
||||
try {
|
||||
Class.forName("org.isoron.uhabits.BaseAndroidTest")
|
||||
return true
|
||||
} catch (e: ClassNotFoundException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits
|
||||
|
||||
import android.app.backup.BackupAgentHelper
|
||||
import android.app.backup.FileBackupHelper
|
||||
import android.app.backup.SharedPreferencesBackupHelper
|
||||
|
||||
/**
|
||||
* An Android BackupAgentHelper customized for this application.
|
||||
*/
|
||||
class HabitsBackupAgent : BackupAgentHelper() {
|
||||
override fun onCreate() {
|
||||
addHelper("preferences", SharedPreferencesBackupHelper(this, "preferences"))
|
||||
addHelper("database", FileBackupHelper(this, "../databases/uhabits.db"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import org.isoron.uhabits.core.database.MigrationHelper
|
||||
import org.isoron.uhabits.core.database.UnsupportedDatabaseVersionException
|
||||
import org.isoron.uhabits.database.AndroidDatabase
|
||||
import java.io.File
|
||||
|
||||
class HabitsDatabaseOpener(
|
||||
context: Context,
|
||||
private val databaseFilename: String,
|
||||
private val version: Int
|
||||
) : SQLiteOpenHelper(context, databaseFilename, null, version) {
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
db.disableWriteAheadLogging()
|
||||
db.version = 8
|
||||
onUpgrade(db, -1, version)
|
||||
}
|
||||
|
||||
override fun onOpen(db: SQLiteDatabase) {
|
||||
super.onOpen(db)
|
||||
db.disableWriteAheadLogging()
|
||||
}
|
||||
|
||||
override fun onUpgrade(
|
||||
db: SQLiteDatabase,
|
||||
oldVersion: Int,
|
||||
newVersion: Int
|
||||
) {
|
||||
db.disableWriteAheadLogging()
|
||||
if (db.version < 8) throw UnsupportedDatabaseVersionException()
|
||||
val helper = MigrationHelper(AndroidDatabase(db, File(databaseFilename)))
|
||||
helper.migrateTo(newVersion)
|
||||
}
|
||||
|
||||
override fun onDowngrade(
|
||||
db: SQLiteDatabase,
|
||||
oldVersion: Int,
|
||||
newVersion: Int
|
||||
) {
|
||||
throw UnsupportedDatabaseVersionException()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.ThemeSwitcher
|
||||
import org.isoron.uhabits.core.ui.views.DarkTheme
|
||||
import org.isoron.uhabits.core.ui.views.LightTheme
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.inject.ActivityScope
|
||||
|
||||
@ActivityScope
|
||||
class AndroidThemeSwitcher
|
||||
constructor(
|
||||
@ActivityContext val context: Context,
|
||||
preferences: Preferences,
|
||||
) : ThemeSwitcher(preferences) {
|
||||
|
||||
private var currentTheme: Theme = LightTheme()
|
||||
|
||||
override fun getSystemTheme(): Int {
|
||||
if (SDK_INT < 29) return THEME_LIGHT
|
||||
val uiMode = context.resources.configuration.uiMode
|
||||
return if ((uiMode and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) {
|
||||
THEME_DARK
|
||||
} else {
|
||||
THEME_LIGHT
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCurrentTheme(): Theme {
|
||||
return currentTheme
|
||||
}
|
||||
|
||||
override fun applyDarkTheme() {
|
||||
currentTheme = DarkTheme()
|
||||
context.setTheme(R.style.AppBaseThemeDark)
|
||||
(context as Activity).window.navigationBarColor =
|
||||
ContextCompat.getColor(context, R.color.grey_900)
|
||||
}
|
||||
|
||||
override fun applyLightTheme() {
|
||||
currentTheme = LightTheme()
|
||||
context.setTheme(R.style.AppBaseTheme)
|
||||
}
|
||||
|
||||
override fun applyPureBlackTheme() {
|
||||
currentTheme = DarkTheme()
|
||||
context.setTheme(R.style.AppBaseThemeDark_PureBlack)
|
||||
(context as Activity).window.navigationBarColor =
|
||||
ContextCompat.getColor(context, R.color.black)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities
|
||||
|
||||
import org.isoron.uhabits.AndroidDirFinder
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuPresenter
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class HabitsDirFinder @Inject
|
||||
constructor(
|
||||
private val androidDirFinder: AndroidDirFinder
|
||||
) : ShowHabitMenuPresenter.System, ListHabitsBehavior.DirFinder {
|
||||
|
||||
override fun getCSVOutputDir(): File {
|
||||
return androidDirFinder.getFilesDir("CSV")!!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities.about
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||
|
||||
/**
|
||||
* Activity that allows the user to see information about the app itself.
|
||||
* Display current version, link to Google Play and list of contributors.
|
||||
*/
|
||||
class AboutActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val app = application as HabitsApplication
|
||||
val screen = AboutScreen(
|
||||
this,
|
||||
app.component.intentFactory,
|
||||
app.component.preferences
|
||||
)
|
||||
AndroidThemeSwitcher(this, app.component.preferences).apply()
|
||||
setContentView(AboutView(this, screen))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities.about
|
||||
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.intents.IntentFactory
|
||||
import org.isoron.uhabits.utils.showMessage
|
||||
import org.isoron.uhabits.utils.startActivitySafely
|
||||
|
||||
class AboutScreen(
|
||||
private val activity: AboutActivity,
|
||||
private val intents: IntentFactory,
|
||||
private val prefs: Preferences,
|
||||
) {
|
||||
|
||||
private var developerCountdown = 5
|
||||
|
||||
fun showRateAppWebsite() =
|
||||
activity.startActivitySafely(intents.rateApp(activity))
|
||||
|
||||
fun showSendFeedbackScreen() =
|
||||
activity.startActivitySafely(intents.sendFeedback(activity))
|
||||
|
||||
fun showSourceCodeWebsite() =
|
||||
activity.startActivitySafely(intents.viewSourceCode(activity))
|
||||
|
||||
fun showTranslationWebsite() =
|
||||
activity.startActivitySafely(intents.helpTranslate(activity))
|
||||
|
||||
fun showPrivacyPolicyWebsite() =
|
||||
activity.startActivitySafely(intents.privacyPolicy(activity))
|
||||
|
||||
fun showCodeContributorsWebsite() =
|
||||
activity.startActivitySafely(intents.codeContributors(activity))
|
||||
|
||||
fun onPressDeveloperCountdown() {
|
||||
developerCountdown--
|
||||
if (developerCountdown == 0) {
|
||||
prefs.isDeveloper = true
|
||||
activity.showMessage(activity.resources.getString(R.string.you_are_now_a_developer))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities.about
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import org.isoron.uhabits.BuildConfig
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.databinding.AboutBinding
|
||||
import org.isoron.uhabits.utils.setupToolbar
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
class AboutView(
|
||||
context: Context,
|
||||
private val screen: AboutScreen,
|
||||
) : FrameLayout(context) {
|
||||
|
||||
private var binding = AboutBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
init {
|
||||
addView(binding.root)
|
||||
setupToolbar(
|
||||
toolbar = binding.toolbar,
|
||||
color = PaletteColor(11),
|
||||
title = resources.getString(R.string.about)
|
||||
)
|
||||
val version = resources.getString(R.string.version_n)
|
||||
binding.tvContributors.setOnClickListener { screen.showCodeContributorsWebsite() }
|
||||
binding.tvFeedback.setOnClickListener { screen.showSendFeedbackScreen() }
|
||||
binding.tvPrivacy.setOnClickListener { screen.showPrivacyPolicyWebsite() }
|
||||
binding.tvRate.setOnClickListener { screen.showRateAppWebsite() }
|
||||
binding.tvSource.setOnClickListener { screen.showSourceCodeWebsite() }
|
||||
binding.tvTranslate.setOnClickListener { screen.showTranslationWebsite() }
|
||||
binding.tvVersion.setOnClickListener { screen.onPressDeveloperCountdown() }
|
||||
binding.tvVersion.text = String.format(version, BuildConfig.VERSION_NAME)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs;
|
||||
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.ui.callbacks.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
/**
|
||||
* Dialog that allows the user to choose a color.
|
||||
*/
|
||||
public class ColorPickerDialog extends com.android.colorpicker.ColorPickerDialog
|
||||
{
|
||||
public void setListener(OnColorPickedCallback callback)
|
||||
{
|
||||
super.setOnColorSelectedListener(c ->
|
||||
{
|
||||
PaletteColor pc = PaletteUtilsKt.toPaletteColor(c, getContext());
|
||||
callback.onColorPicked(pc);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs;
|
||||
|
||||
import android.content.*;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.inject.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
@ActivityScope
|
||||
public class ColorPickerDialogFactory
|
||||
{
|
||||
private final Context context;
|
||||
|
||||
@Inject
|
||||
public ColorPickerDialogFactory(@ActivityContext Context context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public ColorPickerDialog create(PaletteColor color)
|
||||
{
|
||||
ColorPickerDialog dialog = new ColorPickerDialog();
|
||||
StyledResources res = new StyledResources(context);
|
||||
int androidColor = PaletteUtilsKt.toThemedAndroidColor(color, context);
|
||||
|
||||
dialog.initialize(R.string.color_picker_default_title, res.getPalette(),
|
||||
androidColor, 4, com.android.colorpicker.ColorPickerDialog.SIZE_SMALL);
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs;
|
||||
|
||||
import android.content.*;
|
||||
import android.content.res.*;
|
||||
|
||||
import androidx.annotation.*;
|
||||
import androidx.appcompat.app.*;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.core.ui.callbacks.*;
|
||||
import org.isoron.uhabits.inject.*;
|
||||
|
||||
/**
|
||||
* Dialog that asks the user confirmation before executing a delete operation.
|
||||
*/
|
||||
public class ConfirmDeleteDialog extends AlertDialog
|
||||
{
|
||||
public ConfirmDeleteDialog(@ActivityContext Context context,
|
||||
@NonNull OnConfirmedCallback callback,
|
||||
int quantity)
|
||||
{
|
||||
super(context);
|
||||
Resources res = context.getResources();
|
||||
setTitle(res.getQuantityString(R.plurals.delete_habits_title, quantity));
|
||||
setMessage(res.getQuantityString(R.plurals.delete_habits_message, quantity));
|
||||
setButton(BUTTON_POSITIVE,
|
||||
res.getString(R.string.yes),
|
||||
(dialog, which) -> callback.onConfirmed()
|
||||
);
|
||||
setButton(BUTTON_NEGATIVE,
|
||||
res.getString(R.string.no),
|
||||
(dialog, which) -> { }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs;
|
||||
|
||||
import android.content.*;
|
||||
import android.content.res.*;
|
||||
|
||||
import androidx.annotation.*;
|
||||
import androidx.appcompat.app.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.ui.callbacks.*;
|
||||
import org.isoron.uhabits.inject.*;
|
||||
|
||||
public class ConfirmSyncKeyDialog extends AlertDialog
|
||||
{
|
||||
public ConfirmSyncKeyDialog(@ActivityContext Context context,
|
||||
@NonNull OnConfirmedCallback callback)
|
||||
{
|
||||
super(context);
|
||||
setTitle(R.string.device_sync);
|
||||
Resources res = context.getResources();
|
||||
setMessage(res.getString(R.string.sync_confirm));
|
||||
setButton(BUTTON_POSITIVE,
|
||||
res.getString(R.string.yes),
|
||||
(dialog, which) -> callback.onConfirmed()
|
||||
);
|
||||
setButton(BUTTON_NEGATIVE,
|
||||
res.getString(R.string.no),
|
||||
(dialog, which) -> { }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.RadioButton
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import kotlinx.android.synthetic.main.frequency_picker_dialog.view.*
|
||||
import org.isoron.uhabits.R
|
||||
|
||||
class FrequencyPickerDialog(
|
||||
var freqNumerator: Int,
|
||||
var freqDenominator: Int
|
||||
) : AppCompatDialogFragment() {
|
||||
|
||||
lateinit var contentView: View
|
||||
var onFrequencyPicked: (num: Int, den: Int) -> Unit = { _, _ -> }
|
||||
|
||||
constructor() : this(1, 1)
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val inflater = LayoutInflater.from(activity!!)
|
||||
contentView = inflater.inflate(R.layout.frequency_picker_dialog, null)
|
||||
|
||||
contentView.everyDayRadioButton.setOnClickListener {
|
||||
check(contentView.everyDayRadioButton)
|
||||
unfocusAll()
|
||||
}
|
||||
|
||||
contentView.everyXDaysRadioButton.setOnClickListener {
|
||||
check(contentView.everyXDaysRadioButton)
|
||||
val everyXDaysTextView = contentView.everyXDaysTextView
|
||||
focus(everyXDaysTextView)
|
||||
}
|
||||
|
||||
contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||
if (hasFocus) check(contentView.everyXDaysRadioButton)
|
||||
}
|
||||
|
||||
contentView.xTimesPerWeekRadioButton.setOnClickListener {
|
||||
check(contentView.xTimesPerWeekRadioButton)
|
||||
focus(contentView.xTimesPerWeekTextView)
|
||||
}
|
||||
|
||||
contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||
if (hasFocus) check(contentView.xTimesPerWeekRadioButton)
|
||||
}
|
||||
|
||||
contentView.xTimesPerMonthRadioButton.setOnClickListener {
|
||||
check(contentView.xTimesPerMonthRadioButton)
|
||||
focus(contentView.xTimesPerMonthTextView)
|
||||
}
|
||||
|
||||
contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||
if (hasFocus) check(contentView.xTimesPerMonthRadioButton)
|
||||
}
|
||||
|
||||
return AlertDialog.Builder(activity!!)
|
||||
.setView(contentView)
|
||||
.setPositiveButton(R.string.save) { _, _ -> onSaveClicked() }
|
||||
.create()
|
||||
}
|
||||
|
||||
private fun onSaveClicked() {
|
||||
var numerator = 1
|
||||
var denominator = 1
|
||||
when {
|
||||
contentView.everyDayRadioButton.isChecked -> {
|
||||
// NOP
|
||||
}
|
||||
contentView.everyXDaysRadioButton.isChecked -> {
|
||||
if (contentView.everyXDaysTextView.text.isNotEmpty()) {
|
||||
denominator = Integer.parseInt(contentView.everyXDaysTextView.text.toString())
|
||||
}
|
||||
}
|
||||
contentView.xTimesPerWeekRadioButton.isChecked -> {
|
||||
if (contentView.xTimesPerWeekTextView.text.isNotEmpty()) {
|
||||
numerator = Integer.parseInt(contentView.xTimesPerWeekTextView.text.toString())
|
||||
denominator = 7
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (contentView.xTimesPerMonthTextView.text.isNotEmpty()) {
|
||||
numerator = Integer.parseInt(contentView.xTimesPerMonthTextView.text.toString())
|
||||
denominator = 30
|
||||
}
|
||||
}
|
||||
}
|
||||
if (numerator >= denominator || numerator < 1) {
|
||||
numerator = 1
|
||||
denominator = 1
|
||||
}
|
||||
onFrequencyPicked(numerator, denominator)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun check(view: RadioButton?) {
|
||||
uncheckAll()
|
||||
view?.isChecked = true
|
||||
view?.requestFocus()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
populateViews()
|
||||
}
|
||||
|
||||
private fun populateViews() {
|
||||
uncheckAll()
|
||||
if (freqNumerator == 1) {
|
||||
if (freqDenominator == 1) {
|
||||
contentView.everyDayRadioButton.isChecked = true
|
||||
} else {
|
||||
contentView.everyXDaysRadioButton.isChecked = true
|
||||
contentView.everyXDaysTextView.setText(freqDenominator.toString())
|
||||
focus(contentView.everyXDaysTextView)
|
||||
}
|
||||
} else {
|
||||
if (freqDenominator == 7) {
|
||||
contentView.xTimesPerWeekRadioButton.isChecked = true
|
||||
contentView.xTimesPerWeekTextView.setText(freqNumerator.toString())
|
||||
focus(contentView.xTimesPerWeekTextView)
|
||||
} else if (freqDenominator == 30 || freqDenominator == 31) {
|
||||
contentView.xTimesPerMonthRadioButton.isChecked = true
|
||||
contentView.xTimesPerMonthTextView.setText(freqNumerator.toString())
|
||||
focus(contentView.xTimesPerMonthTextView)
|
||||
} else {
|
||||
Log.w("FrequencyPickerDialog", "Unknown frequency: $freqNumerator/$freqDenominator")
|
||||
contentView.everyDayRadioButton.isChecked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun focus(view: EditText) {
|
||||
view.requestFocus()
|
||||
view.setSelection(view.text.length)
|
||||
}
|
||||
|
||||
private fun uncheckAll() {
|
||||
contentView.everyDayRadioButton.isChecked = false
|
||||
contentView.everyXDaysRadioButton.isChecked = false
|
||||
contentView.xTimesPerWeekRadioButton.isChecked = false
|
||||
contentView.xTimesPerMonthRadioButton.isChecked = false
|
||||
}
|
||||
|
||||
private fun unfocusAll() {
|
||||
contentView.everyXDaysTextView.clearFocus()
|
||||
contentView.xTimesPerWeekTextView.clearFocus()
|
||||
contentView.xTimesPerMonthTextView.clearFocus()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import org.isoron.platform.gui.AndroidDataView
|
||||
import org.isoron.platform.time.JavaLocalDateFormatter
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.commands.Command
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter
|
||||
import org.isoron.uhabits.core.ui.views.HistoryChart
|
||||
import org.isoron.uhabits.core.ui.views.LightTheme
|
||||
import org.isoron.uhabits.core.ui.views.OnDateClickedListener
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
import java.util.Locale
|
||||
import kotlin.math.min
|
||||
|
||||
class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
|
||||
|
||||
private lateinit var commandRunner: CommandRunner
|
||||
private lateinit var habit: Habit
|
||||
private lateinit var preferences: Preferences
|
||||
private lateinit var dataView: AndroidDataView
|
||||
|
||||
private var chart: HistoryChart? = null
|
||||
private var onDateClickedListener: OnDateClickedListener? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val component = (activity!!.application as HabitsApplication).component
|
||||
commandRunner = component.commandRunner
|
||||
habit = component.habitList.getById(arguments!!.getLong("habit"))!!
|
||||
preferences = component.preferences
|
||||
|
||||
chart = HistoryChart(
|
||||
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
|
||||
firstWeekday = preferences.firstWeekday,
|
||||
paletteColor = habit.color,
|
||||
series = emptyList(),
|
||||
theme = LightTheme(),
|
||||
today = DateUtils.getTodayWithOffset().toLocalDate(),
|
||||
onDateClickedListener = onDateClickedListener ?: OnDateClickedListener { },
|
||||
padding = 10.0,
|
||||
)
|
||||
dataView = AndroidDataView(context!!, null)
|
||||
dataView.view = chart!!
|
||||
|
||||
return Dialog(context!!).apply {
|
||||
val metrics = resources.displayMetrics
|
||||
val maxHeight = resources.getDimensionPixelSize(R.dimen.history_editor_max_height)
|
||||
setContentView(dataView)
|
||||
window!!.setLayout(metrics.widthPixels, min(metrics.heightPixels, maxHeight))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
commandRunner.addListener(this)
|
||||
refreshData()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
commandRunner.removeListener(this)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
fun setOnDateClickedListener(listener: OnDateClickedListener) {
|
||||
onDateClickedListener = listener
|
||||
chart?.onDateClickedListener = listener
|
||||
}
|
||||
|
||||
private fun refreshData() {
|
||||
val model = HistoryCardPresenter.buildState(
|
||||
habit,
|
||||
preferences.firstWeekday,
|
||||
theme = LightTheme()
|
||||
)
|
||||
chart?.series = model.series
|
||||
dataView.postInvalidate()
|
||||
}
|
||||
|
||||
override fun onCommandFinished(command: Command) {
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see .
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.text.InputFilter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.NumberPicker
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.utils.InterfaceUtils
|
||||
import javax.inject.Inject
|
||||
|
||||
class NumberPickerFactory
|
||||
@Inject constructor(
|
||||
@ActivityContext private val context: Context
|
||||
) {
|
||||
fun create(
|
||||
value: Double,
|
||||
unit: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback
|
||||
): AlertDialog {
|
||||
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val view = inflater.inflate(R.layout.number_picker_dialog, null)
|
||||
|
||||
val picker = view.findViewById<NumberPicker>(R.id.picker)
|
||||
val picker2 = view.findViewById<NumberPicker>(R.id.picker2)
|
||||
val tvUnit = view.findViewById<TextView>(R.id.tvUnit)
|
||||
|
||||
val intValue = Math.round(value * 100).toInt()
|
||||
|
||||
picker.minValue = 0
|
||||
picker.maxValue = Integer.MAX_VALUE / 100
|
||||
picker.value = intValue / 100
|
||||
picker.wrapSelectorWheel = false
|
||||
|
||||
picker2.minValue = 0
|
||||
picker2.maxValue = 19
|
||||
picker2.setFormatter { v -> String.format("%02d", 5 * v) }
|
||||
picker2.value = intValue % 100 / 5
|
||||
refreshInitialValue(picker2)
|
||||
|
||||
tvUnit.text = unit
|
||||
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setView(view)
|
||||
.setTitle(R.string.change_value)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
picker.clearFocus()
|
||||
val v = picker.value + 0.05 * picker2.value
|
||||
callback.onNumberPicked(v)
|
||||
}
|
||||
.setOnDismissListener {
|
||||
callback.onNumberPickerDismissed()
|
||||
}
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
picker.getChildAt(0)?.requestFocus()
|
||||
dialog.window?.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
}
|
||||
|
||||
InterfaceUtils.setupEditorAction(
|
||||
picker,
|
||||
TextView.OnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE)
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
|
||||
false
|
||||
}
|
||||
)
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun refreshInitialValue(picker: NumberPicker) {
|
||||
// Workaround for Android bug:
|
||||
// https://code.google.com/p/android/issues/detail?id=35482
|
||||
val f = NumberPicker::class.java.getDeclaredField("mInputText")
|
||||
f.isAccessible = true
|
||||
val inputText = f.get(picker) as EditText
|
||||
inputText.filters = arrayOfNulls<InputFilter>(0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDialogFragment;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.core.models.WeekdayList;
|
||||
import org.isoron.uhabits.core.utils.DateUtils;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
/**
|
||||
* Dialog that allows the user to pick one or more days of the week.
|
||||
*/
|
||||
public class WeekdayPickerDialog extends AppCompatDialogFragment implements
|
||||
DialogInterface.OnMultiChoiceClickListener,
|
||||
DialogInterface.OnClickListener
|
||||
{
|
||||
private static final String KEY_SELECTED_DAYS = "selectedDays";
|
||||
private boolean[] selectedDays;
|
||||
|
||||
private OnWeekdaysPickedListener listener;
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which, boolean isChecked)
|
||||
{
|
||||
selectedDays[which] = isChecked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if(savedInstanceState != null){
|
||||
selectedDays = savedInstanceState.getBooleanArray(KEY_SELECTED_DAYS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBooleanArray(KEY_SELECTED_DAYS, selectedDays);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which)
|
||||
{
|
||||
if (listener != null)
|
||||
listener.onWeekdaysSet(new WeekdayList(selectedDays));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState)
|
||||
{
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder
|
||||
.setTitle(R.string.select_weekdays)
|
||||
.setMultiChoiceItems(DateUtils.getLongWeekdayNames(Calendar.SATURDAY),
|
||||
selectedDays,
|
||||
this)
|
||||
.setPositiveButton(android.R.string.yes, this)
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
dismiss();
|
||||
});
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
public void setListener(OnWeekdaysPickedListener listener)
|
||||
{
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setSelectedDays(WeekdayList days)
|
||||
{
|
||||
this.selectedDays = days.toArray();
|
||||
}
|
||||
|
||||
public interface OnWeekdaysPickedListener
|
||||
{
|
||||
void onWeekdaysSet(WeekdayList days);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.os.*;
|
||||
|
||||
import androidx.customview.view.*;
|
||||
|
||||
public class BundleSavedState extends AbsSavedState
|
||||
{
|
||||
public static final Parcelable.Creator<BundleSavedState> CREATOR =
|
||||
new ClassLoaderCreator<BundleSavedState>()
|
||||
{
|
||||
@Override
|
||||
public BundleSavedState createFromParcel(Parcel source,
|
||||
ClassLoader loader)
|
||||
{
|
||||
return new BundleSavedState(source, loader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BundleSavedState createFromParcel(Parcel source)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BundleSavedState[] newArray(int size)
|
||||
{
|
||||
return new BundleSavedState[size];
|
||||
}
|
||||
};
|
||||
|
||||
public final Bundle bundle;
|
||||
|
||||
public BundleSavedState(Parcelable superState, Bundle bundle)
|
||||
{
|
||||
super(superState);
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
public BundleSavedState(Parcel source, ClassLoader loader)
|
||||
{
|
||||
super(source, loader);
|
||||
this.bundle = source.readBundle(loader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags)
|
||||
{
|
||||
super.writeToParcel(out, flags);
|
||||
out.writeBundle(bundle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.*;
|
||||
import android.util.*;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
public class FrequencyChart extends ScrollableChart
|
||||
{
|
||||
private Paint pGrid;
|
||||
|
||||
private float em;
|
||||
|
||||
private SimpleDateFormat dfMonth;
|
||||
|
||||
private SimpleDateFormat dfYear;
|
||||
|
||||
private Paint pText, pGraph;
|
||||
|
||||
private RectF rect, prevRect;
|
||||
|
||||
private int baseSize;
|
||||
|
||||
private int paddingTop;
|
||||
|
||||
private float columnWidth;
|
||||
|
||||
private int columnHeight;
|
||||
|
||||
private int nColumns;
|
||||
|
||||
private int textColor;
|
||||
|
||||
private int gridColor;
|
||||
|
||||
private int[] colors;
|
||||
|
||||
private int primaryColor;
|
||||
|
||||
private boolean isBackgroundTransparent;
|
||||
|
||||
@NonNull
|
||||
private HashMap<Timestamp, Integer[]> frequency;
|
||||
|
||||
private int maxFreq;
|
||||
|
||||
private int firstWeekday = Calendar.SUNDAY;
|
||||
|
||||
public FrequencyChart(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public FrequencyChart(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
this.frequency = new HashMap<>();
|
||||
init();
|
||||
}
|
||||
|
||||
public void setColor(int color)
|
||||
{
|
||||
this.primaryColor = color;
|
||||
initColors();
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setFrequency(HashMap<Timestamp, Integer[]> frequency)
|
||||
{
|
||||
this.frequency = frequency;
|
||||
maxFreq = getMaxFreq(frequency);
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setFirstWeekday(int firstWeekday)
|
||||
{
|
||||
this.firstWeekday = firstWeekday;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
private int getMaxFreq(HashMap<Timestamp, Integer[]> frequency)
|
||||
{
|
||||
int maxValue = 1;
|
||||
|
||||
for (Integer[] values : frequency.values())
|
||||
for (Integer value : values)
|
||||
maxValue = Math.max(value, maxValue);
|
||||
|
||||
return maxValue;
|
||||
}
|
||||
|
||||
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
|
||||
{
|
||||
this.isBackgroundTransparent = isBackgroundTransparent;
|
||||
initColors();
|
||||
}
|
||||
|
||||
protected void initPaints()
|
||||
{
|
||||
pText = new Paint();
|
||||
pText.setAntiAlias(true);
|
||||
|
||||
pGraph = new Paint();
|
||||
pGraph.setTextAlign(Paint.Align.CENTER);
|
||||
pGraph.setAntiAlias(true);
|
||||
|
||||
pGrid = new Paint();
|
||||
pGrid.setAntiAlias(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
|
||||
rect.set(0, 0, nColumns * columnWidth, columnHeight);
|
||||
rect.offset(0, paddingTop);
|
||||
|
||||
drawGrid(canvas, rect);
|
||||
|
||||
pText.setTextAlign(Paint.Align.CENTER);
|
||||
pText.setColor(textColor);
|
||||
pGraph.setColor(primaryColor);
|
||||
prevRect.setEmpty();
|
||||
|
||||
GregorianCalendar currentDate = DateUtils.getStartOfTodayCalendarWithOffset();
|
||||
currentDate.set(Calendar.DAY_OF_MONTH, 1);
|
||||
currentDate.add(Calendar.MONTH, -nColumns + 2 - getDataOffset());
|
||||
|
||||
for (int i = 0; i < nColumns - 1; i++)
|
||||
{
|
||||
rect.set(0, 0, columnWidth, columnHeight);
|
||||
rect.offset(i * columnWidth, 0);
|
||||
|
||||
drawColumn(canvas, rect, currentDate);
|
||||
currentDate.add(Calendar.MONTH, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int width,
|
||||
int height,
|
||||
int oldWidth,
|
||||
int oldHeight)
|
||||
{
|
||||
if (height < 9) height = 200;
|
||||
|
||||
baseSize = height / 8;
|
||||
setScrollerBucketSize(baseSize);
|
||||
|
||||
pText.setTextSize(baseSize * 0.4f);
|
||||
pGraph.setTextSize(baseSize * 0.4f);
|
||||
pGraph.setStrokeWidth(baseSize * 0.1f);
|
||||
pGrid.setStrokeWidth(baseSize * 0.05f);
|
||||
em = pText.getFontSpacing();
|
||||
|
||||
columnWidth = baseSize;
|
||||
columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
|
||||
|
||||
columnHeight = 8 * baseSize;
|
||||
nColumns = (int) (width / columnWidth);
|
||||
paddingTop = 0;
|
||||
}
|
||||
|
||||
private void drawColumn(Canvas canvas, RectF rect, GregorianCalendar date)
|
||||
{
|
||||
Integer[] values = frequency.get(new Timestamp(date));
|
||||
float rowHeight = rect.height() / 8.0f;
|
||||
prevRect.set(rect);
|
||||
|
||||
int[] localeWeekdayList = DateUtils.getWeekdaySequence(firstWeekday);
|
||||
for (int j = 0; j < localeWeekdayList.length; j++)
|
||||
{
|
||||
rect.set(0, 0, baseSize, baseSize);
|
||||
rect.offset(prevRect.left, prevRect.top + baseSize * j);
|
||||
|
||||
int i = localeWeekdayList[j] % 7;
|
||||
if (values != null) drawMarker(canvas, rect, values[i]);
|
||||
|
||||
rect.offset(0, rowHeight);
|
||||
}
|
||||
|
||||
drawFooter(canvas, rect, date);
|
||||
}
|
||||
|
||||
private void drawFooter(Canvas canvas, RectF rect, GregorianCalendar date)
|
||||
{
|
||||
Date time = date.getTime();
|
||||
|
||||
canvas.drawText(dfMonth.format(time), rect.centerX(),
|
||||
rect.centerY() - 0.1f * em, pText);
|
||||
|
||||
if (date.get(Calendar.MONTH) == 1)
|
||||
canvas.drawText(dfYear.format(time), rect.centerX(),
|
||||
rect.centerY() + 0.9f * em, pText);
|
||||
}
|
||||
|
||||
private void drawGrid(Canvas canvas, RectF rGrid)
|
||||
{
|
||||
int nRows = 7;
|
||||
float rowHeight = rGrid.height() / (nRows + 1);
|
||||
|
||||
pText.setTextAlign(Paint.Align.LEFT);
|
||||
pText.setColor(textColor);
|
||||
pGrid.setColor(gridColor);
|
||||
|
||||
for (String day : DateUtils.getShortWeekdayNames(firstWeekday))
|
||||
{
|
||||
canvas.drawText(day, rGrid.right - columnWidth,
|
||||
rGrid.top + rowHeight / 2 + 0.25f * em, pText);
|
||||
|
||||
pGrid.setStrokeWidth(1f);
|
||||
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top,
|
||||
pGrid);
|
||||
|
||||
rGrid.offset(0, rowHeight);
|
||||
}
|
||||
|
||||
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
|
||||
}
|
||||
|
||||
private void drawMarker(Canvas canvas, RectF rect, Integer value)
|
||||
{
|
||||
float padding = rect.height() * 0.2f;
|
||||
// maximal allowed mark radius
|
||||
float maxRadius = (rect.height() - 2 * padding) / 2.0f;
|
||||
// the real mark radius is scaled down by a factor depending on the maximal frequency
|
||||
float scale = 1.0f/maxFreq * value;
|
||||
float radius = maxRadius * scale;
|
||||
|
||||
int colorIndex = Math.min(colors.length - 1, Math.round((colors.length - 1) * scale));
|
||||
pGraph.setColor(colors[colorIndex]);
|
||||
canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph);
|
||||
}
|
||||
|
||||
private float getMaxMonthWidth()
|
||||
{
|
||||
float maxMonthWidth = 0;
|
||||
GregorianCalendar day = DateUtils.getStartOfTodayCalendarWithOffset();
|
||||
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
day.set(Calendar.MONTH, i);
|
||||
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
|
||||
maxMonthWidth = Math.max(maxMonthWidth, monthWidth);
|
||||
}
|
||||
|
||||
return maxMonthWidth;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
initPaints();
|
||||
initColors();
|
||||
initDateFormats();
|
||||
initRects();
|
||||
}
|
||||
|
||||
private void initColors()
|
||||
{
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
textColor = res.getColor(R.attr.mediumContrastTextColor);
|
||||
gridColor = res.getColor(R.attr.lowContrastTextColor);
|
||||
|
||||
colors = new int[4];
|
||||
colors[0] = gridColor;
|
||||
colors[3] = primaryColor;
|
||||
colors[1] = ColorUtils.mixColors(colors[0], colors[3], 0.66f);
|
||||
colors[2] = ColorUtils.mixColors(colors[0], colors[3], 0.33f);
|
||||
}
|
||||
|
||||
private void initDateFormats()
|
||||
{
|
||||
if (isInEditMode())
|
||||
{
|
||||
dfMonth = new SimpleDateFormat("MMM", Locale.getDefault());
|
||||
dfYear = new SimpleDateFormat("yyyy", Locale.getDefault());
|
||||
}
|
||||
else
|
||||
{
|
||||
dfMonth = DateExtensionsKt.toSimpleDataFormat("MMM");
|
||||
dfYear = DateExtensionsKt.toSimpleDataFormat("yyyy");
|
||||
}
|
||||
}
|
||||
|
||||
private void initRects()
|
||||
{
|
||||
rect = new RectF();
|
||||
prevRect = new RectF();
|
||||
}
|
||||
|
||||
public void populateWithRandomData()
|
||||
{
|
||||
GregorianCalendar date = DateUtils.getStartOfTodayCalendar();
|
||||
date.set(Calendar.DAY_OF_MONTH, 1);
|
||||
Random rand = new Random();
|
||||
frequency.clear();
|
||||
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
Integer values[] = new Integer[7];
|
||||
for (int j = 0; j < 7; j++)
|
||||
values[j] = rand.nextInt(5);
|
||||
|
||||
frequency.put(new Timestamp(date), values);
|
||||
date.add(Calendar.MONTH, -1);
|
||||
}
|
||||
maxFreq = getMaxFreq(frequency);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import org.isoron.uhabits.core.models.Habit;
|
||||
|
||||
public interface HabitChart
|
||||
{
|
||||
void setHabit(Habit habit);
|
||||
|
||||
void refreshData();
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.*;
|
||||
import android.text.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import static org.isoron.uhabits.utils.AttributeSetUtils.*;
|
||||
import static org.isoron.uhabits.utils.InterfaceUtils.*;
|
||||
|
||||
public class RingView extends View
|
||||
{
|
||||
public static final PorterDuffXfermode XFERMODE_CLEAR =
|
||||
new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
|
||||
|
||||
private int color;
|
||||
|
||||
private float precision;
|
||||
|
||||
private float percentage;
|
||||
|
||||
private int diameter;
|
||||
|
||||
private float thickness;
|
||||
|
||||
private RectF rect;
|
||||
|
||||
private TextPaint pRing;
|
||||
|
||||
private Integer backgroundColor;
|
||||
|
||||
private Integer inactiveColor;
|
||||
|
||||
private float em;
|
||||
|
||||
private String text;
|
||||
|
||||
private float textSize;
|
||||
|
||||
private boolean enableFontAwesome;
|
||||
|
||||
@Nullable
|
||||
private Bitmap drawingCache;
|
||||
|
||||
private Canvas cacheCanvas;
|
||||
|
||||
private boolean isTransparencyEnabled;
|
||||
|
||||
public RingView(Context context)
|
||||
{
|
||||
super(context);
|
||||
|
||||
percentage = 0.0f;
|
||||
precision = 0.01f;
|
||||
color = PaletteUtils.getAndroidTestColor(0);
|
||||
thickness = dpToPixels(getContext(), 2);
|
||||
text = "";
|
||||
textSize = getDimension(context, R.dimen.smallTextSize);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
public RingView(Context ctx, AttributeSet attrs)
|
||||
{
|
||||
super(ctx, attrs);
|
||||
|
||||
percentage = getFloatAttribute(ctx, attrs, "percentage", 0);
|
||||
precision = getFloatAttribute(ctx, attrs, "precision", 0.01f);
|
||||
|
||||
color = getColorAttribute(ctx, attrs, "color", 0);
|
||||
backgroundColor = getColorAttribute(ctx, attrs, "backgroundColor", null);
|
||||
inactiveColor = getColorAttribute(ctx, attrs, "inactiveColor", null);
|
||||
|
||||
thickness = getFloatAttribute(ctx, attrs, "thickness", 0);
|
||||
thickness = dpToPixels(ctx, thickness);
|
||||
|
||||
float defaultTextSize = getDimension(ctx, R.dimen.smallTextSize);
|
||||
textSize = getFloatAttribute(ctx, attrs, "textSize", defaultTextSize);
|
||||
textSize = spToPixels(ctx, textSize);
|
||||
text = getAttribute(ctx, attrs, "text", "");
|
||||
|
||||
enableFontAwesome =
|
||||
getBooleanAttribute(ctx, attrs, "enableFontAwesome", false);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBackgroundColor(int backgroundColor)
|
||||
{
|
||||
this.backgroundColor = backgroundColor;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setColor(int color)
|
||||
{
|
||||
this.color = color;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public int getColor()
|
||||
{
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setIsTransparencyEnabled(boolean isTransparencyEnabled)
|
||||
{
|
||||
this.isTransparencyEnabled = isTransparencyEnabled;
|
||||
}
|
||||
|
||||
public void setPercentage(float percentage)
|
||||
{
|
||||
this.percentage = percentage;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setPrecision(float precision)
|
||||
{
|
||||
this.precision = precision;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setText(String text)
|
||||
{
|
||||
this.text = text;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setTextSize(float textSize)
|
||||
{
|
||||
this.textSize = textSize;
|
||||
}
|
||||
|
||||
public void setThickness(float thickness)
|
||||
{
|
||||
this.thickness = thickness;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
Canvas activeCanvas;
|
||||
|
||||
if (isTransparencyEnabled)
|
||||
{
|
||||
if (drawingCache == null) reallocateCache();
|
||||
activeCanvas = cacheCanvas;
|
||||
drawingCache.eraseColor(Color.TRANSPARENT);
|
||||
}
|
||||
else
|
||||
{
|
||||
activeCanvas = canvas;
|
||||
}
|
||||
|
||||
pRing.setColor(color);
|
||||
rect.set(0, 0, diameter, diameter);
|
||||
|
||||
float angle = 360 * Math.round(percentage / precision) * precision;
|
||||
|
||||
activeCanvas.drawArc(rect, -90, angle, true, pRing);
|
||||
|
||||
pRing.setColor(inactiveColor);
|
||||
activeCanvas.drawArc(rect, angle - 90, 360 - angle, true, pRing);
|
||||
|
||||
if (thickness > 0)
|
||||
{
|
||||
if (isTransparencyEnabled) pRing.setXfermode(XFERMODE_CLEAR);
|
||||
else pRing.setColor(backgroundColor);
|
||||
|
||||
rect.inset(thickness, thickness);
|
||||
activeCanvas.drawArc(rect, 0, 360, true, pRing);
|
||||
pRing.setXfermode(null);
|
||||
|
||||
pRing.setColor(color);
|
||||
pRing.setTextSize(textSize);
|
||||
if (enableFontAwesome)
|
||||
pRing.setTypeface(getFontAwesome(getContext()));
|
||||
activeCanvas.drawText(text, rect.centerX(),
|
||||
rect.centerY() + 0.4f * em, pRing);
|
||||
}
|
||||
|
||||
if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
diameter = Math.min(height, width);
|
||||
|
||||
pRing.setTextSize(textSize);
|
||||
em = pRing.measureText("M");
|
||||
|
||||
setMeasuredDimension(diameter, diameter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh)
|
||||
{
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
|
||||
if (isTransparencyEnabled) reallocateCache();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
pRing = new TextPaint();
|
||||
pRing.setAntiAlias(true);
|
||||
pRing.setColor(color);
|
||||
pRing.setTextAlign(Paint.Align.CENTER);
|
||||
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
|
||||
if (backgroundColor == null)
|
||||
backgroundColor = res.getColor(R.attr.cardBgColor);
|
||||
|
||||
if (inactiveColor == null)
|
||||
inactiveColor = res.getColor(R.attr.highContrastTextColor);
|
||||
|
||||
inactiveColor = ColorUtils.setAlpha(inactiveColor, 0.1f);
|
||||
|
||||
rect = new RectF();
|
||||
}
|
||||
|
||||
private void reallocateCache()
|
||||
{
|
||||
if (drawingCache != null) drawingCache.recycle();
|
||||
drawingCache =
|
||||
Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888);
|
||||
cacheCanvas = new Canvas(drawingCache);
|
||||
}
|
||||
|
||||
public float getPercentage()
|
||||
{
|
||||
return percentage;
|
||||
}
|
||||
|
||||
public float getPrecision()
|
||||
{
|
||||
return precision;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.*;
|
||||
import android.util.*;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
import static org.isoron.uhabits.utils.InterfaceUtils.*;
|
||||
|
||||
public class ScoreChart extends ScrollableChart
|
||||
{
|
||||
private static final PorterDuffXfermode XFERMODE_CLEAR =
|
||||
new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
|
||||
|
||||
private static final PorterDuffXfermode XFERMODE_SRC =
|
||||
new PorterDuffXfermode(PorterDuff.Mode.SRC);
|
||||
|
||||
private Paint pGrid;
|
||||
|
||||
private float em;
|
||||
|
||||
private SimpleDateFormat dfMonth;
|
||||
|
||||
private SimpleDateFormat dfDay;
|
||||
|
||||
private SimpleDateFormat dfYear;
|
||||
|
||||
private Paint pText, pGraph;
|
||||
|
||||
private RectF rect, prevRect;
|
||||
|
||||
private int baseSize;
|
||||
|
||||
private int paddingTop;
|
||||
|
||||
private float columnWidth;
|
||||
|
||||
private int columnHeight;
|
||||
|
||||
private int nColumns;
|
||||
|
||||
private int textColor;
|
||||
|
||||
private int gridColor;
|
||||
|
||||
@Nullable
|
||||
private List<Score> scores;
|
||||
|
||||
private int primaryColor;
|
||||
|
||||
@Deprecated
|
||||
private int bucketSize = 7;
|
||||
|
||||
private int backgroundColor;
|
||||
|
||||
private Bitmap drawingCache;
|
||||
|
||||
private Canvas cacheCanvas;
|
||||
|
||||
private boolean isTransparencyEnabled;
|
||||
|
||||
private int skipYear = 0;
|
||||
|
||||
private String previousYearText;
|
||||
|
||||
private String previousMonthText;
|
||||
|
||||
public ScoreChart(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public ScoreChart(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public void populateWithRandomData()
|
||||
{
|
||||
Random random = new Random();
|
||||
scores = new LinkedList<>();
|
||||
|
||||
double previous = 0.5f;
|
||||
Timestamp timestamp = DateUtils.getToday();
|
||||
|
||||
for (int i = 1; i < 100; i++)
|
||||
{
|
||||
double step = 0.1f;
|
||||
double current = previous + random.nextDouble() * step * 2 - step;
|
||||
current = Math.max(0, Math.min(1.0f, current));
|
||||
scores.add(new Score(timestamp.minus(i), current));
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
|
||||
public void setBucketSize(int bucketSize)
|
||||
{
|
||||
this.bucketSize = bucketSize;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setIsTransparencyEnabled(boolean enabled)
|
||||
{
|
||||
this.isTransparencyEnabled = enabled;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setColor(int primaryColor)
|
||||
{
|
||||
this.primaryColor = primaryColor;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setScores(@NonNull List<Score> scores)
|
||||
{
|
||||
this.scores = scores;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
Canvas activeCanvas;
|
||||
|
||||
if (isTransparencyEnabled)
|
||||
{
|
||||
if (drawingCache == null) initCache(getWidth(), getHeight());
|
||||
|
||||
activeCanvas = cacheCanvas;
|
||||
drawingCache.eraseColor(Color.TRANSPARENT);
|
||||
}
|
||||
else
|
||||
{
|
||||
activeCanvas = canvas;
|
||||
}
|
||||
|
||||
if (scores == null) return;
|
||||
|
||||
rect.set(0, 0, nColumns * columnWidth, columnHeight);
|
||||
rect.offset(0, paddingTop);
|
||||
|
||||
drawGrid(activeCanvas, rect);
|
||||
|
||||
pText.setColor(textColor);
|
||||
pGraph.setColor(primaryColor);
|
||||
prevRect.setEmpty();
|
||||
|
||||
previousMonthText = "";
|
||||
previousYearText = "";
|
||||
skipYear = 0;
|
||||
|
||||
for (int k = 0; k < nColumns; k++)
|
||||
{
|
||||
int offset = nColumns - k - 1 + getDataOffset();
|
||||
if (offset >= scores.size()) continue;
|
||||
|
||||
double score = scores.get(offset).getValue();
|
||||
Timestamp timestamp = scores.get(offset).getTimestamp();
|
||||
|
||||
int height = (int) (columnHeight * score);
|
||||
|
||||
rect.set(0, 0, baseSize, baseSize);
|
||||
rect.offset(k * columnWidth + (columnWidth - baseSize) / 2,
|
||||
paddingTop + columnHeight - height - baseSize / 2);
|
||||
|
||||
if (!prevRect.isEmpty())
|
||||
{
|
||||
drawLine(activeCanvas, prevRect, rect);
|
||||
drawMarker(activeCanvas, prevRect);
|
||||
}
|
||||
|
||||
if (k == nColumns - 1) drawMarker(activeCanvas, rect);
|
||||
|
||||
prevRect.set(rect);
|
||||
rect.set(0, 0, columnWidth, columnHeight);
|
||||
rect.offset(k * columnWidth, paddingTop);
|
||||
|
||||
drawFooter(activeCanvas, rect, timestamp);
|
||||
}
|
||||
|
||||
if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int width,
|
||||
int height,
|
||||
int oldWidth,
|
||||
int oldHeight)
|
||||
{
|
||||
if (height < 9) height = 200;
|
||||
|
||||
float maxTextSize = getDimension(getContext(), R.dimen.tinyTextSize);
|
||||
float textSize = height * 0.06f;
|
||||
pText.setTextSize(Math.min(textSize, maxTextSize));
|
||||
em = pText.getFontSpacing();
|
||||
|
||||
int footerHeight = (int) (3 * em);
|
||||
paddingTop = (int) (em);
|
||||
|
||||
baseSize = (height - footerHeight - paddingTop) / 8;
|
||||
columnWidth = baseSize;
|
||||
columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f);
|
||||
columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
|
||||
|
||||
nColumns = (int) (width / columnWidth);
|
||||
columnWidth = (float) width / nColumns;
|
||||
setScrollerBucketSize((int) columnWidth);
|
||||
|
||||
columnHeight = 8 * baseSize;
|
||||
|
||||
float minStrokeWidth = dpToPixels(getContext(), 1);
|
||||
pGraph.setTextSize(baseSize * 0.5f);
|
||||
pGraph.setStrokeWidth(baseSize * 0.1f);
|
||||
pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f));
|
||||
|
||||
if (isTransparencyEnabled) initCache(width, height);
|
||||
}
|
||||
|
||||
private void drawFooter(Canvas canvas, RectF rect, Timestamp currentDate)
|
||||
{
|
||||
String yearText = dfYear.format(currentDate.toJavaDate());
|
||||
String monthText = dfMonth.format(currentDate.toJavaDate());
|
||||
String dayText = dfDay.format(currentDate.toJavaDate());
|
||||
|
||||
GregorianCalendar calendar = currentDate.toCalendar();
|
||||
|
||||
String text;
|
||||
int year = calendar.get(Calendar.YEAR);
|
||||
|
||||
boolean shouldPrintYear = true;
|
||||
if (yearText.equals(previousYearText)) shouldPrintYear = false;
|
||||
if (bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false;
|
||||
|
||||
if (skipYear > 0)
|
||||
{
|
||||
skipYear--;
|
||||
shouldPrintYear = false;
|
||||
}
|
||||
|
||||
if (shouldPrintYear)
|
||||
{
|
||||
previousYearText = yearText;
|
||||
previousMonthText = "";
|
||||
|
||||
pText.setTextAlign(Paint.Align.CENTER);
|
||||
canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f,
|
||||
pText);
|
||||
|
||||
skipYear = 1;
|
||||
}
|
||||
|
||||
if (bucketSize < 365)
|
||||
{
|
||||
if (!monthText.equals(previousMonthText))
|
||||
{
|
||||
previousMonthText = monthText;
|
||||
text = monthText;
|
||||
}
|
||||
else
|
||||
{
|
||||
text = dayText;
|
||||
}
|
||||
|
||||
pText.setTextAlign(Paint.Align.CENTER);
|
||||
canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f,
|
||||
pText);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawGrid(Canvas canvas, RectF rGrid)
|
||||
{
|
||||
int nRows = 5;
|
||||
float rowHeight = rGrid.height() / nRows;
|
||||
|
||||
pText.setTextAlign(Paint.Align.LEFT);
|
||||
pText.setColor(textColor);
|
||||
pGrid.setColor(gridColor);
|
||||
|
||||
for (int i = 0; i < nRows; i++)
|
||||
{
|
||||
canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)),
|
||||
rGrid.left + 0.5f * em, rGrid.top + 1f * em, pText);
|
||||
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top,
|
||||
pGrid);
|
||||
rGrid.offset(0, rowHeight);
|
||||
}
|
||||
|
||||
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
|
||||
}
|
||||
|
||||
private void drawLine(Canvas canvas, RectF rectFrom, RectF rectTo)
|
||||
{
|
||||
pGraph.setColor(primaryColor);
|
||||
canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(),
|
||||
rectTo.centerX(), rectTo.centerY(), pGraph);
|
||||
}
|
||||
|
||||
private void drawMarker(Canvas canvas, RectF rect)
|
||||
{
|
||||
rect.inset(baseSize * 0.225f, baseSize * 0.225f);
|
||||
setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
|
||||
canvas.drawOval(rect, pGraph);
|
||||
|
||||
rect.inset(baseSize * 0.1f, baseSize * 0.1f);
|
||||
setModeOrColor(pGraph, XFERMODE_SRC, primaryColor);
|
||||
canvas.drawOval(rect, pGraph);
|
||||
|
||||
// rect.inset(baseSize * 0.1f, baseSize * 0.1f);
|
||||
// setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
|
||||
// canvas.drawOval(rect, pGraph);
|
||||
|
||||
if (isTransparencyEnabled) pGraph.setXfermode(XFERMODE_SRC);
|
||||
}
|
||||
|
||||
private float getMaxDayWidth()
|
||||
{
|
||||
float maxDayWidth = 0;
|
||||
GregorianCalendar day = DateUtils.getStartOfTodayCalendarWithOffset();
|
||||
|
||||
for (int i = 0; i < 28; i++)
|
||||
{
|
||||
day.set(Calendar.DAY_OF_MONTH, i);
|
||||
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
|
||||
maxDayWidth = Math.max(maxDayWidth, monthWidth);
|
||||
}
|
||||
|
||||
return maxDayWidth;
|
||||
}
|
||||
|
||||
private float getMaxMonthWidth()
|
||||
{
|
||||
float maxMonthWidth = 0;
|
||||
GregorianCalendar day = DateUtils.getStartOfTodayCalendarWithOffset();
|
||||
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
day.set(Calendar.MONTH, i);
|
||||
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
|
||||
maxMonthWidth = Math.max(maxMonthWidth, monthWidth);
|
||||
}
|
||||
|
||||
return maxMonthWidth;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
initPaints();
|
||||
initColors();
|
||||
initDateFormats();
|
||||
initRects();
|
||||
}
|
||||
|
||||
private void initCache(int width, int height)
|
||||
{
|
||||
if (drawingCache != null) drawingCache.recycle();
|
||||
drawingCache =
|
||||
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
cacheCanvas = new Canvas(drawingCache);
|
||||
}
|
||||
|
||||
private void initColors()
|
||||
{
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
|
||||
primaryColor = Color.BLACK;
|
||||
textColor = res.getColor(R.attr.mediumContrastTextColor);
|
||||
gridColor = res.getColor(R.attr.lowContrastTextColor);
|
||||
backgroundColor = res.getColor(R.attr.cardBgColor);
|
||||
}
|
||||
|
||||
private void initDateFormats()
|
||||
{
|
||||
if (isInEditMode())
|
||||
{
|
||||
dfMonth = new SimpleDateFormat("MMM", Locale.getDefault());
|
||||
dfYear = new SimpleDateFormat("yyyy", Locale.getDefault());
|
||||
dfDay = new SimpleDateFormat("d", Locale.getDefault());
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
dfMonth = DateExtensionsKt.toSimpleDataFormat("MMM");
|
||||
dfYear = DateExtensionsKt.toSimpleDataFormat("yyyy");
|
||||
dfDay = DateExtensionsKt.toSimpleDataFormat("d");
|
||||
}
|
||||
}
|
||||
|
||||
private void initPaints()
|
||||
{
|
||||
pText = new Paint();
|
||||
pText.setAntiAlias(true);
|
||||
|
||||
pGraph = new Paint();
|
||||
pGraph.setTextAlign(Paint.Align.CENTER);
|
||||
pGraph.setAntiAlias(true);
|
||||
|
||||
pGrid = new Paint();
|
||||
pGrid.setAntiAlias(true);
|
||||
}
|
||||
|
||||
private void initRects()
|
||||
{
|
||||
rect = new RectF();
|
||||
prevRect = new RectF();
|
||||
}
|
||||
|
||||
private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color)
|
||||
{
|
||||
if (isTransparencyEnabled) p.setXfermode(mode);
|
||||
else p.setColor(color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.animation.*;
|
||||
import android.content.*;
|
||||
import android.os.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
public abstract class ScrollableChart extends View
|
||||
implements GestureDetector.OnGestureListener,
|
||||
ValueAnimator.AnimatorUpdateListener
|
||||
{
|
||||
|
||||
private int dataOffset;
|
||||
|
||||
private int scrollerBucketSize = 1;
|
||||
|
||||
private int direction = 1;
|
||||
|
||||
private GestureDetector detector;
|
||||
|
||||
private Scroller scroller;
|
||||
|
||||
private ValueAnimator scrollAnimator;
|
||||
|
||||
private ScrollController scrollController;
|
||||
|
||||
private int maxDataOffset = 10000;
|
||||
|
||||
public ScrollableChart(Context context)
|
||||
{
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public ScrollableChart(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public int getDataOffset()
|
||||
{
|
||||
return dataOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation)
|
||||
{
|
||||
if (!scroller.isFinished())
|
||||
{
|
||||
scroller.computeScrollOffset();
|
||||
updateDataOffset();
|
||||
}
|
||||
else
|
||||
{
|
||||
scrollAnimator.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1,
|
||||
MotionEvent e2,
|
||||
float velocityX,
|
||||
float velocityY)
|
||||
{
|
||||
scroller.fling(scroller.getCurrX(), scroller.getCurrY(),
|
||||
direction * ((int) velocityX) / 2, 0, 0, getMaxX(), 0, 0);
|
||||
invalidate();
|
||||
|
||||
scrollAnimator.setDuration(scroller.getDuration());
|
||||
scrollAnimator.start();
|
||||
return false;
|
||||
}
|
||||
|
||||
private int getMaxX()
|
||||
{
|
||||
return maxDataOffset * scrollerBucketSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Parcelable state)
|
||||
{
|
||||
if(!(state instanceof BundleSavedState))
|
||||
{
|
||||
super.onRestoreInstanceState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
BundleSavedState bss = (BundleSavedState) state;
|
||||
int x = bss.bundle.getInt("x");
|
||||
int y = bss.bundle.getInt("y");
|
||||
direction = bss.bundle.getInt("direction");
|
||||
dataOffset = bss.bundle.getInt("dataOffset");
|
||||
maxDataOffset = bss.bundle.getInt("maxDataOffset");
|
||||
scroller.startScroll(0, 0, x, y, 0);
|
||||
scroller.computeScrollOffset();
|
||||
super.onRestoreInstanceState(bss.getSuperState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState()
|
||||
{
|
||||
Parcelable superState = super.onSaveInstanceState();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt("x", scroller.getCurrX());
|
||||
bundle.putInt("y", scroller.getCurrY());
|
||||
bundle.putInt("dataOffset", dataOffset);
|
||||
bundle.putInt("direction", direction);
|
||||
bundle.putInt("maxDataOffset", maxDataOffset);
|
||||
return new BundleSavedState(superState, bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy)
|
||||
{
|
||||
if (scrollerBucketSize == 0) return false;
|
||||
|
||||
if (Math.abs(dx) > Math.abs(dy))
|
||||
{
|
||||
ViewParent parent = getParent();
|
||||
if (parent != null) parent.requestDisallowInterceptTouchEvent(true);
|
||||
}
|
||||
|
||||
|
||||
dx = - direction * dx;
|
||||
dx = Math.min(dx, getMaxX() - scroller.getCurrX());
|
||||
scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), (int) dx,
|
||||
(int) dy, 0);
|
||||
|
||||
scroller.computeScrollOffset();
|
||||
updateDataOffset();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowPress(MotionEvent e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event)
|
||||
{
|
||||
return detector.onTouchEvent(event);
|
||||
}
|
||||
|
||||
public void setScrollDirection(int direction)
|
||||
{
|
||||
if (direction != 1 && direction != -1)
|
||||
throw new IllegalArgumentException();
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void setMaxDataOffset(int maxDataOffset)
|
||||
{
|
||||
this.maxDataOffset = maxDataOffset;
|
||||
this.dataOffset = Math.min(dataOffset, maxDataOffset);
|
||||
scrollController.onDataOffsetChanged(this.dataOffset);
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setScrollController(ScrollController scrollController)
|
||||
{
|
||||
this.scrollController = scrollController;
|
||||
}
|
||||
|
||||
public void setScrollerBucketSize(int scrollerBucketSize)
|
||||
{
|
||||
this.scrollerBucketSize = scrollerBucketSize;
|
||||
}
|
||||
|
||||
private void init(Context context)
|
||||
{
|
||||
detector = new GestureDetector(context, this);
|
||||
scroller = new Scroller(context, null, true);
|
||||
scrollAnimator = ValueAnimator.ofFloat(0, 1);
|
||||
scrollAnimator.addUpdateListener(this);
|
||||
scrollController = new ScrollController() {};
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
scroller.setFinalX(0);
|
||||
scroller.computeScrollOffset();
|
||||
updateDataOffset();
|
||||
}
|
||||
|
||||
private void updateDataOffset()
|
||||
{
|
||||
int newDataOffset = scroller.getCurrX() / scrollerBucketSize;
|
||||
newDataOffset = Math.max(0, newDataOffset);
|
||||
newDataOffset = Math.min(maxDataOffset, newDataOffset);
|
||||
|
||||
if (newDataOffset != dataOffset)
|
||||
{
|
||||
dataOffset = newDataOffset;
|
||||
scrollController.onDataOffsetChanged(dataOffset);
|
||||
postInvalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public interface ScrollController
|
||||
{
|
||||
default void onDataOffsetChanged(int newDataOffset) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.view.ViewGroup.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
import static android.view.View.MeasureSpec.*;
|
||||
import static org.isoron.uhabits.utils.InterfaceUtils.*;
|
||||
|
||||
public class StreakChart extends View
|
||||
{
|
||||
private Paint paint;
|
||||
|
||||
private long minLength;
|
||||
|
||||
private long maxLength;
|
||||
|
||||
private int[] colors;
|
||||
|
||||
private int[] textColors;
|
||||
|
||||
private RectF rect;
|
||||
|
||||
private int baseSize;
|
||||
|
||||
private int primaryColor;
|
||||
|
||||
private List<Streak> streaks;
|
||||
|
||||
private boolean isBackgroundTransparent;
|
||||
|
||||
private DateFormat dateFormat;
|
||||
|
||||
private int width;
|
||||
|
||||
private float em;
|
||||
|
||||
private float maxLabelWidth;
|
||||
|
||||
private float textMargin;
|
||||
|
||||
private boolean shouldShowLabels;
|
||||
|
||||
private int textColor;
|
||||
|
||||
public StreakChart(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public StreakChart(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of streaks this view is able to show, given
|
||||
* its current size.
|
||||
*
|
||||
* @return max number of visible streaks
|
||||
*/
|
||||
public int getMaxStreakCount()
|
||||
{
|
||||
return (int) Math.floor(getMeasuredHeight() / baseSize);
|
||||
}
|
||||
|
||||
public void populateWithRandomData()
|
||||
{
|
||||
Timestamp start = DateUtils.getToday();
|
||||
LinkedList<Streak> streaks = new LinkedList<>();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
int length = new Random().nextInt(100);
|
||||
Timestamp end = start.plus(length);
|
||||
streaks.add(new Streak(start, end));
|
||||
start = end.plus(1);
|
||||
}
|
||||
|
||||
setStreaks(streaks);
|
||||
}
|
||||
|
||||
public void setColor(int color)
|
||||
{
|
||||
this.primaryColor = color;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
|
||||
{
|
||||
this.isBackgroundTransparent = isBackgroundTransparent;
|
||||
initColors();
|
||||
}
|
||||
|
||||
public void setStreaks(List<Streak> streaks)
|
||||
{
|
||||
this.streaks = streaks;
|
||||
initColors();
|
||||
updateMaxMinLengths();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
if (streaks.size() == 0) return;
|
||||
|
||||
rect.set(0, 0, width, baseSize);
|
||||
|
||||
for (Streak s : streaks)
|
||||
{
|
||||
drawRow(canvas, s, rect);
|
||||
rect.offset(0, baseSize);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthSpec, int heightSpec)
|
||||
{
|
||||
LayoutParams params = getLayoutParams();
|
||||
|
||||
if (params != null && params.height == LayoutParams.WRAP_CONTENT)
|
||||
{
|
||||
int width = getSize(widthSpec);
|
||||
int height = streaks.size() * baseSize;
|
||||
|
||||
heightSpec = makeMeasureSpec(height, EXACTLY);
|
||||
widthSpec = makeMeasureSpec(width, EXACTLY);
|
||||
}
|
||||
|
||||
setMeasuredDimension(widthSpec, heightSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int width,
|
||||
int height,
|
||||
int oldWidth,
|
||||
int oldHeight)
|
||||
{
|
||||
this.width = width;
|
||||
|
||||
Context context = getContext();
|
||||
float minTextSize = getDimension(context, R.dimen.tinyTextSize);
|
||||
float maxTextSize = getDimension(context, R.dimen.regularTextSize);
|
||||
float textSize = baseSize * 0.5f;
|
||||
|
||||
paint.setTextSize(
|
||||
Math.max(Math.min(textSize, maxTextSize), minTextSize));
|
||||
em = paint.getFontSpacing();
|
||||
textMargin = 0.5f * em;
|
||||
|
||||
updateMaxMinLengths();
|
||||
}
|
||||
|
||||
private void drawRow(Canvas canvas, Streak streak, RectF rect)
|
||||
{
|
||||
if (maxLength == 0) return;
|
||||
|
||||
float percentage = (float) streak.getLength() / maxLength;
|
||||
float availableWidth = width - 2 * maxLabelWidth;
|
||||
if (shouldShowLabels) availableWidth -= 2 * textMargin;
|
||||
|
||||
float barWidth = percentage * availableWidth;
|
||||
float minBarWidth =
|
||||
paint.measureText(Long.toString(streak.getLength())) + em;
|
||||
barWidth = Math.max(barWidth, minBarWidth);
|
||||
|
||||
float gap = (width - barWidth) / 2;
|
||||
float paddingTopBottom = baseSize * 0.05f;
|
||||
|
||||
paint.setColor(percentageToColor(percentage));
|
||||
|
||||
float round = dpToPixels(getContext(), 2);
|
||||
canvas.drawRoundRect(rect.left + gap,
|
||||
rect.top + paddingTopBottom,
|
||||
rect.right - gap,
|
||||
rect.bottom - paddingTopBottom,
|
||||
round,
|
||||
round,
|
||||
paint);
|
||||
|
||||
float yOffset = rect.centerY() + 0.3f * em;
|
||||
|
||||
paint.setColor(percentageToTextColor(percentage));
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
canvas.drawText(Long.toString(streak.getLength()), rect.centerX(),
|
||||
yOffset, paint);
|
||||
|
||||
if (shouldShowLabels)
|
||||
{
|
||||
String startLabel = dateFormat.format(streak.getStart().toJavaDate());
|
||||
String endLabel = dateFormat.format(streak.getEnd().toJavaDate());
|
||||
|
||||
paint.setColor(textColors[1]);
|
||||
paint.setTextAlign(Paint.Align.RIGHT);
|
||||
canvas.drawText(startLabel, gap - textMargin, yOffset, paint);
|
||||
|
||||
paint.setTextAlign(Paint.Align.LEFT);
|
||||
canvas.drawText(endLabel, width - gap + textMargin, yOffset, paint);
|
||||
}
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
initPaints();
|
||||
initColors();
|
||||
|
||||
streaks = Collections.emptyList();
|
||||
|
||||
dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM);
|
||||
if (!isInEditMode()) dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
rect = new RectF();
|
||||
baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize);
|
||||
}
|
||||
|
||||
private void initColors()
|
||||
{
|
||||
int red = Color.red(primaryColor);
|
||||
int green = Color.green(primaryColor);
|
||||
int blue = Color.blue(primaryColor);
|
||||
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
|
||||
colors = new int[4];
|
||||
colors[3] = primaryColor;
|
||||
colors[2] = Color.argb(192, red, green, blue);
|
||||
colors[1] = Color.argb(96, red, green, blue);
|
||||
colors[0] = res.getColor(R.attr.lowContrastTextColor);
|
||||
|
||||
textColors = new int[3];
|
||||
textColors[2] = res.getColor(R.attr.highContrastReverseTextColor);
|
||||
textColors[1] = res.getColor(R.attr.mediumContrastTextColor);
|
||||
textColors[0] = res.getColor(R.attr.lowContrastReverseTextColor);
|
||||
}
|
||||
|
||||
private void initPaints()
|
||||
{
|
||||
paint = new Paint();
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
private int percentageToColor(float percentage)
|
||||
{
|
||||
if (percentage >= 1.0f) return colors[3];
|
||||
if (percentage >= 0.8f) return colors[2];
|
||||
if (percentage >= 0.5f) return colors[1];
|
||||
return colors[0];
|
||||
}
|
||||
|
||||
private int percentageToTextColor(float percentage)
|
||||
{
|
||||
if (percentage >= 0.5f) return textColors[2];
|
||||
return textColors[1];
|
||||
}
|
||||
|
||||
private void updateMaxMinLengths()
|
||||
{
|
||||
maxLength = 0;
|
||||
minLength = Long.MAX_VALUE;
|
||||
shouldShowLabels = true;
|
||||
|
||||
for (Streak s : streaks)
|
||||
{
|
||||
maxLength = Math.max(maxLength, s.getLength());
|
||||
minLength = Math.min(minLength, s.getLength());
|
||||
|
||||
float lw1 =
|
||||
paint.measureText(dateFormat.format(s.getStart().toJavaDate()));
|
||||
float lw2 =
|
||||
paint.measureText(dateFormat.format(s.getEnd().toJavaDate()));
|
||||
maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2));
|
||||
}
|
||||
|
||||
if (width - 2 * maxLabelWidth < width * 0.25f)
|
||||
{
|
||||
maxLabelWidth = 0;
|
||||
shouldShowLabels = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.activities.habits.list.views.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static android.view.View.MeasureSpec.*;
|
||||
import static org.isoron.uhabits.utils.InterfaceUtils.*;
|
||||
|
||||
public class TargetChart extends View
|
||||
{
|
||||
private Paint paint;
|
||||
private int baseSize;
|
||||
private int primaryColor;
|
||||
private int mediumContrastTextColor;
|
||||
private int highContrastReverseTextColor;
|
||||
private int lowContrastTextColor;
|
||||
private RectF rect = new RectF();
|
||||
private RectF barRect = new RectF();
|
||||
private List<Double> values = Collections.emptyList();
|
||||
private List<String> labels = Collections.emptyList();
|
||||
private List<Double> targets = Collections.emptyList();
|
||||
private float maxLabelSize;
|
||||
private float tinyTextSize;
|
||||
|
||||
public TargetChart(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public TargetChart(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public void populateWithRandomData()
|
||||
{
|
||||
labels = new ArrayList<>();
|
||||
values = new ArrayList<>();
|
||||
targets = new ArrayList<>();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
double percentage = new Random().nextDouble();
|
||||
targets.add(new Random().nextDouble() * 1000.0);
|
||||
values.add(targets.get(i) * percentage * 1.2);
|
||||
labels.add(String.format(Locale.US, "Label %d", i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
public void setColor(int color)
|
||||
{
|
||||
this.primaryColor = color;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
if (labels.size() == 0) return;
|
||||
|
||||
maxLabelSize = 0;
|
||||
for (String label : labels) {
|
||||
paint.setTextSize(tinyTextSize);
|
||||
float len = paint.measureText(label);
|
||||
maxLabelSize = Math.max(maxLabelSize, len);
|
||||
}
|
||||
|
||||
float marginTop = (getHeight() - baseSize * labels.size()) / 2.0f;
|
||||
rect.set(0, marginTop, getWidth(), marginTop + baseSize);
|
||||
for (int i = 0; i < labels.size(); i++) {
|
||||
drawRow(canvas, i, rect);
|
||||
rect.offset(0, baseSize);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthSpec, int heightSpec)
|
||||
{
|
||||
baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize);
|
||||
|
||||
int width = getSize(widthSpec);
|
||||
int height = labels.size() * baseSize;
|
||||
|
||||
ViewGroup.LayoutParams params = getLayoutParams();
|
||||
if (params != null && params.height == ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||
height = getSize(heightSpec);
|
||||
if (labels.size() > 0) baseSize = height / labels.size();
|
||||
}
|
||||
|
||||
heightSpec = makeMeasureSpec(height, EXACTLY);
|
||||
widthSpec = makeMeasureSpec(width, EXACTLY);
|
||||
setMeasuredDimension(widthSpec, heightSpec);
|
||||
}
|
||||
|
||||
private void drawRow(Canvas canvas, int row, RectF rect)
|
||||
{
|
||||
float padding = dpToPixels(getContext(), 4);
|
||||
float round = dpToPixels(getContext(), 2);
|
||||
float stop = maxLabelSize + padding * 2;
|
||||
|
||||
paint.setColor(mediumContrastTextColor);
|
||||
|
||||
// Draw label
|
||||
paint.setTextSize(tinyTextSize);
|
||||
paint.setTextAlign(Paint.Align.RIGHT);
|
||||
float yTextAdjust = (paint.descent() + paint.ascent()) / 2.0f;
|
||||
canvas.drawText(labels.get(row),
|
||||
rect.left + stop - padding,
|
||||
rect.centerY() - yTextAdjust,
|
||||
paint);
|
||||
|
||||
// Draw background box
|
||||
paint.setColor(lowContrastTextColor);
|
||||
barRect.set(rect.left + stop + padding,
|
||||
rect.top + baseSize * 0.05f,
|
||||
rect.right - padding,
|
||||
rect.bottom - baseSize * 0.05f);
|
||||
canvas.drawRoundRect(barRect, round, round, paint);
|
||||
|
||||
float percentage = (float) (values.get(row) / targets.get(row));
|
||||
percentage = Math.min(1.0f, percentage);
|
||||
|
||||
// Draw completed box
|
||||
float completedWidth = percentage * barRect.width();
|
||||
if (completedWidth > 0 && completedWidth < 2 * round) {
|
||||
completedWidth = 2 * round;
|
||||
}
|
||||
float remainingWidth = barRect.width() - completedWidth;
|
||||
|
||||
paint.setColor(primaryColor);
|
||||
barRect.set(barRect.left,
|
||||
barRect.top,
|
||||
barRect.left + completedWidth,
|
||||
barRect.bottom);
|
||||
canvas.drawRoundRect(barRect, round, round, paint);
|
||||
|
||||
// Draw values
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setTextSize(tinyTextSize);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
yTextAdjust = (paint.descent() + paint.ascent()) / 2.0f;
|
||||
|
||||
double remaining = targets.get(row) - values.get(row);
|
||||
String completedText = NumberButtonViewKt.toShortString(values.get(row));
|
||||
String remainingText = NumberButtonViewKt.toShortString(remaining);
|
||||
|
||||
if (completedWidth > paint.measureText(completedText) + 2 * padding) {
|
||||
paint.setColor(highContrastReverseTextColor);
|
||||
canvas.drawText(completedText,
|
||||
barRect.centerX(),
|
||||
barRect.centerY() - yTextAdjust,
|
||||
paint);
|
||||
}
|
||||
|
||||
if (remainingWidth > paint.measureText(remainingText) + 2 * padding) {
|
||||
paint.setColor(mediumContrastTextColor);
|
||||
barRect.set(rect.left + stop + padding + completedWidth,
|
||||
barRect.top,
|
||||
rect.right - padding,
|
||||
barRect.bottom);
|
||||
canvas.drawText(remainingText,
|
||||
barRect.centerX(),
|
||||
barRect.centerY() - yTextAdjust,
|
||||
paint);
|
||||
}
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
paint = new Paint();
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setAntiAlias(true);
|
||||
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
lowContrastTextColor = res.getColor(R.attr.lowContrastTextColor);
|
||||
mediumContrastTextColor = res.getColor(R.attr.mediumContrastTextColor);
|
||||
highContrastReverseTextColor = res.getColor(R.attr.highContrastReverseTextColor);
|
||||
tinyTextSize = getDimension(getContext(), R.dimen.tinyTextSize);
|
||||
}
|
||||
|
||||
public void setValues(List<Double> values)
|
||||
{
|
||||
this.values = values;
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
public void setLabels(List<String> labels)
|
||||
{
|
||||
this.labels = labels;
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
public void setTargets(List<Double> targets)
|
||||
{
|
||||
this.targets = targets;
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import org.isoron.uhabits.core.tasks.Task
|
||||
import org.isoron.uhabits.core.tasks.TaskRunner
|
||||
|
||||
class TaskProgressBar(
|
||||
context: Context,
|
||||
private val runner: TaskRunner
|
||||
) : ProgressBar(
|
||||
context,
|
||||
null,
|
||||
android.R.attr.progressBarStyleHorizontal
|
||||
),
|
||||
TaskRunner.Listener {
|
||||
|
||||
init {
|
||||
visibility = View.GONE
|
||||
isIndeterminate = true
|
||||
}
|
||||
|
||||
override fun onTaskStarted(task: Task?) = update()
|
||||
override fun onTaskFinished(task: Task?) = update()
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
runner.addListener(this)
|
||||
update()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
runner.removeListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
fun update() {
|
||||
val callback = {
|
||||
val activeTaskCount = runner.activeTaskCount
|
||||
val newVisibility = when (activeTaskCount) {
|
||||
0 -> GONE
|
||||
else -> VISIBLE
|
||||
}
|
||||
if (visibility != newVisibility) visibility = newVisibility
|
||||
}
|
||||
postDelayed(callback, 500)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.edit
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateFormat
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.android.datetimepicker.time.RadialPickerLayout
|
||||
import com.android.datetimepicker.time.TimePickerDialog
|
||||
import kotlinx.android.synthetic.main.activity_edit_habit.*
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
|
||||
import org.isoron.uhabits.activities.common.dialogs.FrequencyPickerDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.WeekdayPickerDialog
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.commands.CreateHabitCommand
|
||||
import org.isoron.uhabits.core.commands.EditHabitCommand
|
||||
import org.isoron.uhabits.core.models.Frequency
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.models.Reminder
|
||||
import org.isoron.uhabits.core.models.WeekdayList
|
||||
import org.isoron.uhabits.databinding.ActivityEditHabitBinding
|
||||
import org.isoron.uhabits.utils.ColorUtils
|
||||
import org.isoron.uhabits.utils.formatTime
|
||||
import org.isoron.uhabits.utils.toFormattedString
|
||||
import org.isoron.uhabits.utils.toThemedAndroidColor
|
||||
|
||||
class EditHabitActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var themeSwitcher: AndroidThemeSwitcher
|
||||
private lateinit var binding: ActivityEditHabitBinding
|
||||
private lateinit var commandRunner: CommandRunner
|
||||
|
||||
var habitId = -1L
|
||||
var habitType = -1
|
||||
var unit = ""
|
||||
var color = PaletteColor(11)
|
||||
var androidColor = 0
|
||||
var freqNum = 1
|
||||
var freqDen = 1
|
||||
var reminderHour = -1
|
||||
var reminderMin = -1
|
||||
var reminderDays: WeekdayList = WeekdayList.EVERY_DAY
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
|
||||
val component = (application as HabitsApplication).component
|
||||
themeSwitcher = AndroidThemeSwitcher(this, component.preferences)
|
||||
themeSwitcher.apply()
|
||||
|
||||
binding = ActivityEditHabitBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
if (intent.hasExtra("habitId")) {
|
||||
binding.toolbar.title = getString(R.string.edit_habit)
|
||||
habitId = intent.getLongExtra("habitId", -1)
|
||||
val habit = component.habitList.getById(habitId)!!
|
||||
habitType = habit.type
|
||||
color = habit.color
|
||||
freqNum = habit.frequency.numerator
|
||||
freqDen = habit.frequency.denominator
|
||||
habit.reminder?.let {
|
||||
reminderHour = it.hour
|
||||
reminderMin = it.minute
|
||||
reminderDays = it.days
|
||||
}
|
||||
binding.nameInput.setText(habit.name)
|
||||
binding.questionInput.setText(habit.question)
|
||||
binding.notesInput.setText(habit.description)
|
||||
binding.unitInput.setText(habit.unit)
|
||||
binding.targetInput.setText(habit.targetValue.toString())
|
||||
} else {
|
||||
habitType = intent.getIntExtra("habitType", Habit.YES_NO_HABIT)
|
||||
}
|
||||
|
||||
if (state != null) {
|
||||
habitId = state.getLong("habitId")
|
||||
habitType = state.getInt("habitType")
|
||||
color = PaletteColor(state.getInt("paletteColor"))
|
||||
freqNum = state.getInt("freqNum")
|
||||
freqDen = state.getInt("freqDen")
|
||||
reminderHour = state.getInt("reminderHour")
|
||||
reminderMin = state.getInt("reminderMin")
|
||||
reminderDays = WeekdayList(state.getInt("reminderDays"))
|
||||
}
|
||||
|
||||
updateColors()
|
||||
|
||||
if (habitType == Habit.YES_NO_HABIT) {
|
||||
binding.unitOuterBox.visibility = View.GONE
|
||||
binding.targetOuterBox.visibility = View.GONE
|
||||
} else {
|
||||
binding.nameInput.hint = getString(R.string.measurable_short_example)
|
||||
binding.questionInput.hint = getString(R.string.measurable_question_example)
|
||||
binding.frequencyOuterBox.visibility = View.GONE
|
||||
}
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
supportActionBar?.elevation = 10.0f
|
||||
|
||||
val colorPickerDialogFactory = ColorPickerDialogFactory(this)
|
||||
binding.colorButton.setOnClickListener {
|
||||
val dialog = colorPickerDialogFactory.create(color)
|
||||
dialog.setListener { paletteColor ->
|
||||
this.color = paletteColor
|
||||
updateColors()
|
||||
}
|
||||
dialog.show(supportFragmentManager, "colorPicker")
|
||||
}
|
||||
|
||||
populateFrequency()
|
||||
binding.booleanFrequencyPicker.setOnClickListener {
|
||||
val dialog = FrequencyPickerDialog(freqNum, freqDen)
|
||||
dialog.onFrequencyPicked = { num, den ->
|
||||
freqNum = num
|
||||
freqDen = den
|
||||
populateFrequency()
|
||||
}
|
||||
dialog.show(supportFragmentManager, "frequencyPicker")
|
||||
}
|
||||
|
||||
binding.numericalFrequencyPicker.setOnClickListener {
|
||||
val builder = AlertDialog.Builder(this)
|
||||
val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item)
|
||||
arrayAdapter.add(getString(R.string.every_day))
|
||||
arrayAdapter.add(getString(R.string.every_week))
|
||||
arrayAdapter.add(getString(R.string.every_month))
|
||||
builder.setAdapter(arrayAdapter) { dialog, which ->
|
||||
freqDen = when (which) {
|
||||
1 -> 7
|
||||
2 -> 30
|
||||
else -> 1
|
||||
}
|
||||
populateFrequency()
|
||||
dialog.dismiss()
|
||||
}
|
||||
builder.show()
|
||||
}
|
||||
|
||||
populateReminder()
|
||||
binding.reminderTimePicker.setOnClickListener {
|
||||
val currentHour = if (reminderHour >= 0) reminderHour else 8
|
||||
val currentMin = if (reminderMin >= 0) reminderMin else 0
|
||||
val is24HourMode = DateFormat.is24HourFormat(this)
|
||||
val dialog = TimePickerDialog.newInstance(
|
||||
object : TimePickerDialog.OnTimeSetListener {
|
||||
override fun onTimeSet(view: RadialPickerLayout?, hourOfDay: Int, minute: Int) {
|
||||
reminderHour = hourOfDay
|
||||
reminderMin = minute
|
||||
populateReminder()
|
||||
}
|
||||
|
||||
override fun onTimeCleared(view: RadialPickerLayout?) {
|
||||
reminderHour = -1
|
||||
reminderMin = -1
|
||||
reminderDays = WeekdayList.EVERY_DAY
|
||||
populateReminder()
|
||||
}
|
||||
},
|
||||
currentHour,
|
||||
currentMin,
|
||||
is24HourMode,
|
||||
androidColor
|
||||
)
|
||||
dialog.show(supportFragmentManager, "timePicker")
|
||||
}
|
||||
|
||||
binding.reminderDatePicker.setOnClickListener {
|
||||
val dialog = WeekdayPickerDialog()
|
||||
dialog.setListener { days ->
|
||||
reminderDays = days
|
||||
if (reminderDays.isEmpty) reminderDays = WeekdayList.EVERY_DAY
|
||||
populateReminder()
|
||||
}
|
||||
dialog.setSelectedDays(reminderDays)
|
||||
dialog.show(supportFragmentManager, "dayPicker")
|
||||
}
|
||||
|
||||
binding.buttonSave.setOnClickListener {
|
||||
if (validate()) save()
|
||||
}
|
||||
|
||||
for (fragment in supportFragmentManager.fragments) {
|
||||
(fragment as DialogFragment).dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun save() {
|
||||
val component = (application as HabitsApplication).component
|
||||
val habit = component.modelFactory.buildHabit()
|
||||
|
||||
var original: Habit? = null
|
||||
if (habitId >= 0) {
|
||||
original = component.habitList.getById(habitId)!!
|
||||
habit.copyFrom(original)
|
||||
}
|
||||
|
||||
habit.name = nameInput.text.trim().toString()
|
||||
habit.question = questionInput.text.trim().toString()
|
||||
habit.description = notesInput.text.trim().toString()
|
||||
habit.color = color
|
||||
if (reminderHour >= 0) {
|
||||
habit.reminder = Reminder(reminderHour, reminderMin, reminderDays)
|
||||
} else {
|
||||
habit.reminder = null
|
||||
}
|
||||
|
||||
habit.frequency = Frequency(freqNum, freqDen)
|
||||
if (habitType == Habit.NUMBER_HABIT) {
|
||||
habit.targetValue = targetInput.text.toString().toDouble()
|
||||
habit.targetType = Habit.AT_LEAST
|
||||
habit.unit = unitInput.text.trim().toString()
|
||||
}
|
||||
habit.type = habitType
|
||||
|
||||
val command = if (habitId >= 0) {
|
||||
EditHabitCommand(
|
||||
component.habitList,
|
||||
habitId,
|
||||
habit
|
||||
)
|
||||
} else {
|
||||
CreateHabitCommand(
|
||||
component.modelFactory,
|
||||
component.habitList,
|
||||
habit
|
||||
)
|
||||
}
|
||||
component.commandRunner.run(command)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun validate(): Boolean {
|
||||
var isValid = true
|
||||
if (nameInput.text.isEmpty()) {
|
||||
nameInput.error = getString(R.string.validation_cannot_be_blank)
|
||||
isValid = false
|
||||
}
|
||||
if (habitType == Habit.NUMBER_HABIT) {
|
||||
if (unitInput.text.isEmpty()) {
|
||||
unitInput.error = getString(R.string.validation_cannot_be_blank)
|
||||
isValid = false
|
||||
}
|
||||
if (targetInput.text.isEmpty()) {
|
||||
targetInput.error = getString(R.string.validation_cannot_be_blank)
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
return isValid
|
||||
}
|
||||
|
||||
private fun populateReminder() {
|
||||
if (reminderHour < 0) {
|
||||
binding.reminderTimePicker.text = getString(R.string.reminder_off)
|
||||
binding.reminderDatePicker.visibility = View.GONE
|
||||
binding.reminderDivider.visibility = View.GONE
|
||||
} else {
|
||||
val time = formatTime(this, reminderHour, reminderMin)
|
||||
binding.reminderTimePicker.text = time
|
||||
binding.reminderDatePicker.visibility = View.VISIBLE
|
||||
binding.reminderDivider.visibility = View.VISIBLE
|
||||
binding.reminderDatePicker.text = reminderDays.toFormattedString(this)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
private fun populateFrequency() {
|
||||
binding.booleanFrequencyPicker.text = when {
|
||||
freqNum == 1 && freqDen == 1 -> getString(R.string.every_day)
|
||||
freqNum == 1 && freqDen == 7 -> getString(R.string.every_week)
|
||||
freqNum == 1 && freqDen > 1 -> getString(R.string.every_x_days, freqDen)
|
||||
freqDen == 7 -> getString(R.string.x_times_per_week, freqNum)
|
||||
freqDen == 31 -> getString(R.string.x_times_per_month, freqNum)
|
||||
else -> "Unknown"
|
||||
}
|
||||
binding.numericalFrequencyPicker.text = when (freqDen) {
|
||||
1 -> getString(R.string.every_day)
|
||||
7 -> getString(R.string.every_week)
|
||||
30 -> getString(R.string.every_month)
|
||||
else -> "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateColors() {
|
||||
androidColor = color.toThemedAndroidColor(this)
|
||||
binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor)
|
||||
if (!themeSwitcher.isNightMode) {
|
||||
val darkerAndroidColor = ColorUtils.mixColors(Color.BLACK, androidColor, 0.15f)
|
||||
window.statusBarColor = darkerAndroidColor
|
||||
binding.toolbar.setBackgroundColor(androidColor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(state: Bundle) {
|
||||
super.onSaveInstanceState(state)
|
||||
with(state) {
|
||||
putLong("habitId", habitId)
|
||||
putInt("habitType", habitType)
|
||||
putInt("paletteColor", color.paletteIndex)
|
||||
putInt("androidColor", androidColor)
|
||||
putInt("freqNum", freqNum)
|
||||
putInt("freqDen", freqDen)
|
||||
putInt("reminderHour", reminderHour)
|
||||
putInt("reminderMin", reminderMin)
|
||||
putInt("reminderDays", reminderDays.toInteger())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.edit
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.databinding.SelectHabitTypeBinding
|
||||
import org.isoron.uhabits.intents.IntentFactory
|
||||
|
||||
class HabitTypeDialog : AppCompatDialogFragment() {
|
||||
override fun getTheme() = R.style.Translucent
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val binding = SelectHabitTypeBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.buttonYesNo.setOnClickListener {
|
||||
val intent = IntentFactory().startEditActivity(activity!!, Habit.YES_NO_HABIT)
|
||||
startActivity(intent)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.buttonMeasurable.setOnClickListener {
|
||||
val intent = IntentFactory().startEditActivity(activity!!, Habit.NUMBER_HABIT)
|
||||
startActivity(intent)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.background.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.isoron.uhabits.BaseExceptionHandler
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.sync.SyncManager
|
||||
import org.isoron.uhabits.core.tasks.TaskRunner
|
||||
import org.isoron.uhabits.core.ui.ThemeSwitcher.THEME_DARK
|
||||
import org.isoron.uhabits.core.utils.MidnightTimer
|
||||
import org.isoron.uhabits.database.AutoBackup
|
||||
import org.isoron.uhabits.inject.ActivityContextModule
|
||||
import org.isoron.uhabits.inject.DaggerHabitsActivityComponent
|
||||
import org.isoron.uhabits.utils.restartWithFade
|
||||
|
||||
class ListHabitsActivity : AppCompatActivity() {
|
||||
|
||||
var pureBlack: Boolean = false
|
||||
lateinit var taskRunner: TaskRunner
|
||||
lateinit var adapter: HabitCardListAdapter
|
||||
lateinit var rootView: ListHabitsRootView
|
||||
lateinit var screen: ListHabitsScreen
|
||||
lateinit var prefs: Preferences
|
||||
lateinit var midnightTimer: MidnightTimer
|
||||
lateinit var syncManager: SyncManager
|
||||
private val scope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
private lateinit var menu: ListHabitsMenu
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val appComponent = (applicationContext as HabitsApplication).component
|
||||
val component = DaggerHabitsActivityComponent
|
||||
.builder()
|
||||
.activityContextModule(ActivityContextModule(this))
|
||||
.habitsApplicationComponent(appComponent)
|
||||
.build()
|
||||
component.themeSwitcher.apply()
|
||||
|
||||
prefs = appComponent.preferences
|
||||
syncManager = appComponent.syncManager
|
||||
pureBlack = prefs.isPureBlackEnabled
|
||||
midnightTimer = appComponent.midnightTimer
|
||||
rootView = component.listHabitsRootView
|
||||
screen = component.listHabitsScreen
|
||||
adapter = component.habitCardListAdapter
|
||||
taskRunner = appComponent.taskRunner
|
||||
menu = component.listHabitsMenu
|
||||
Thread.setDefaultUncaughtExceptionHandler(BaseExceptionHandler(this))
|
||||
component.listHabitsBehavior.onStartup()
|
||||
setContentView(rootView)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
midnightTimer.onPause()
|
||||
screen.onDettached()
|
||||
adapter.cancelRefresh()
|
||||
scope.launch {
|
||||
syncManager.onPause()
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
adapter.refresh()
|
||||
screen.onAttached()
|
||||
rootView.postInvalidate()
|
||||
midnightTimer.onResume()
|
||||
scope.launch {
|
||||
syncManager.onResume()
|
||||
}
|
||||
taskRunner.run {
|
||||
AutoBackup(this@ListHabitsActivity).run()
|
||||
}
|
||||
if (prefs.theme == THEME_DARK && prefs.isPureBlackEnabled != pureBlack) {
|
||||
restartWithFade(ListHabitsActivity::class.java)
|
||||
}
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(m: Menu): Boolean {
|
||||
menu.onCreate(menuInflater, m)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return menu.onItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onActivityResult(request: Int, result: Int, data: Intent?) {
|
||||
super.onActivityResult(request, result, data)
|
||||
screen.onResult(request, result, data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list
|
||||
|
||||
import android.content.Context
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.ThemeSwitcher
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsMenuBehavior
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.inject.ActivityScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ActivityScope
|
||||
class ListHabitsMenu @Inject constructor(
|
||||
@ActivityContext context: Context,
|
||||
private val preferences: Preferences,
|
||||
private val themeSwitcher: ThemeSwitcher,
|
||||
private val behavior: ListHabitsMenuBehavior
|
||||
) {
|
||||
val activity = (context as AppCompatActivity)
|
||||
|
||||
fun onCreate(inflater: MenuInflater, menu: Menu) {
|
||||
menu.clear()
|
||||
inflater.inflate(R.menu.list_habits, menu)
|
||||
val nightModeItem = menu.findItem(R.id.actionToggleNightMode)
|
||||
val hideArchivedItem = menu.findItem(R.id.actionHideArchived)
|
||||
val hideCompletedItem = menu.findItem(R.id.actionHideCompleted)
|
||||
nightModeItem.isChecked = themeSwitcher.isNightMode
|
||||
hideArchivedItem.isChecked = !preferences.showArchived
|
||||
hideCompletedItem.isChecked = !preferences.showCompleted
|
||||
}
|
||||
|
||||
fun onItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.actionToggleNightMode -> {
|
||||
behavior.onToggleNightMode()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionCreateHabit -> {
|
||||
behavior.onCreateHabit()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionFAQ -> {
|
||||
behavior.onViewFAQ()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionAbout -> {
|
||||
behavior.onViewAbout()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionSettings -> {
|
||||
behavior.onViewSettings()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionHideArchived -> {
|
||||
behavior.onToggleShowArchived()
|
||||
activity.invalidateOptionsMenu()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionHideCompleted -> {
|
||||
behavior.onToggleShowCompleted()
|
||||
activity.invalidateOptionsMenu()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionSortColor -> {
|
||||
behavior.onSortByColor()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionSortManual -> {
|
||||
behavior.onSortByManually()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionSortName -> {
|
||||
behavior.onSortByName()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionSortScore -> {
|
||||
behavior.onSortByScore()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionSortStatus -> {
|
||||
behavior.onSortByStatus()
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import org.isoron.uhabits.AndroidBugReporter
|
||||
import org.isoron.uhabits.activities.HabitsDirFinder
|
||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsMenuBehavior
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBehavior
|
||||
import org.isoron.uhabits.inject.AppContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class BugReporterProxy
|
||||
@Inject constructor(
|
||||
@AppContext context: Context
|
||||
) : AndroidBugReporter(context), ListHabitsBehavior.BugReporter
|
||||
|
||||
@Module
|
||||
abstract class ListHabitsModule {
|
||||
|
||||
@Binds
|
||||
abstract fun getAdapter(adapter: HabitCardListAdapter): ListHabitsMenuBehavior.Adapter
|
||||
|
||||
@Binds
|
||||
abstract fun getBugReporter(proxy: BugReporterProxy): ListHabitsBehavior.BugReporter
|
||||
|
||||
@Binds
|
||||
abstract fun getMenuScreen(screen: ListHabitsScreen): ListHabitsMenuBehavior.Screen
|
||||
|
||||
@Binds
|
||||
abstract fun getScreen(screen: ListHabitsScreen): ListHabitsBehavior.Screen
|
||||
|
||||
@Binds
|
||||
abstract fun getSelMenuAdapter(adapter: HabitCardListAdapter): ListHabitsSelectionMenuBehavior.Adapter
|
||||
|
||||
@Binds
|
||||
abstract fun getSelMenuScreen(screen: ListHabitsScreen): ListHabitsSelectionMenuBehavior.Screen
|
||||
|
||||
@Binds
|
||||
abstract fun getSystem(system: HabitsDirFinder): ListHabitsBehavior.DirFinder
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.RelativeLayout
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.common.views.ScrollableChart
|
||||
import org.isoron.uhabits.activities.common.views.TaskProgressBar
|
||||
import org.isoron.uhabits.activities.habits.list.views.EmptyListView
|
||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardListView
|
||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardListViewFactory
|
||||
import org.isoron.uhabits.activities.habits.list.views.HeaderView
|
||||
import org.isoron.uhabits.activities.habits.list.views.HintView
|
||||
import org.isoron.uhabits.core.models.ModelObservable
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.tasks.TaskRunner
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.HintListFactory
|
||||
import org.isoron.uhabits.core.utils.MidnightTimer
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.inject.ActivityScope
|
||||
import org.isoron.uhabits.utils.addAtBottom
|
||||
import org.isoron.uhabits.utils.addAtTop
|
||||
import org.isoron.uhabits.utils.addBelow
|
||||
import org.isoron.uhabits.utils.buildToolbar
|
||||
import org.isoron.uhabits.utils.dim
|
||||
import org.isoron.uhabits.utils.dp
|
||||
import org.isoron.uhabits.utils.setupToolbar
|
||||
import org.isoron.uhabits.utils.sres
|
||||
import java.lang.Math.max
|
||||
import java.lang.Math.min
|
||||
import javax.inject.Inject
|
||||
|
||||
const val MAX_CHECKMARK_COUNT = 60
|
||||
|
||||
@ActivityScope
|
||||
class ListHabitsRootView @Inject constructor(
|
||||
@ActivityContext context: Context,
|
||||
hintListFactory: HintListFactory,
|
||||
preferences: Preferences,
|
||||
midnightTimer: MidnightTimer,
|
||||
runner: TaskRunner,
|
||||
private val listAdapter: HabitCardListAdapter,
|
||||
habitCardListViewFactory: HabitCardListViewFactory
|
||||
) : FrameLayout(context), ModelObservable.Listener {
|
||||
|
||||
val listView: HabitCardListView = habitCardListViewFactory.create()
|
||||
val llEmpty = EmptyListView(context)
|
||||
val tbar = buildToolbar()
|
||||
val progressBar = TaskProgressBar(context, runner)
|
||||
val hintView: HintView
|
||||
val header = HeaderView(context, preferences, midnightTimer)
|
||||
|
||||
init {
|
||||
val hints = resources.getStringArray(R.array.hints)
|
||||
val hintList = hintListFactory.create(hints)
|
||||
hintView = HintView(context, hintList)
|
||||
|
||||
val rootView = RelativeLayout(context).apply {
|
||||
background = sres.getDrawable(R.attr.windowBackgroundColor)
|
||||
addAtTop(tbar)
|
||||
addBelow(header, tbar)
|
||||
addBelow(listView, header, height = MATCH_PARENT)
|
||||
addBelow(llEmpty, header, height = MATCH_PARENT)
|
||||
addBelow(progressBar, header) {
|
||||
it.topMargin = dp(-6.0f).toInt()
|
||||
}
|
||||
addAtBottom(hintView)
|
||||
}
|
||||
rootView.setupToolbar(
|
||||
toolbar = tbar,
|
||||
title = resources.getString(R.string.main_activity_title),
|
||||
color = PaletteColor(17),
|
||||
displayHomeAsUpEnabled = false,
|
||||
)
|
||||
addView(rootView, MATCH_PARENT, MATCH_PARENT)
|
||||
listAdapter.setListView(listView)
|
||||
}
|
||||
|
||||
override fun onModelChange() {
|
||||
updateEmptyView()
|
||||
}
|
||||
|
||||
private fun setupControllers() {
|
||||
header.setScrollController(
|
||||
object : ScrollableChart.ScrollController {
|
||||
override fun onDataOffsetChanged(newDataOffset: Int) {
|
||||
listView.dataOffset = newDataOffset
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
setupControllers()
|
||||
listAdapter.observable.addListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
listAdapter.observable.removeListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
val count = getCheckmarkCount()
|
||||
header.buttonCount = count
|
||||
header.setMaxDataOffset(max(MAX_CHECKMARK_COUNT - count, 0))
|
||||
listView.checkmarkCount = count
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
}
|
||||
|
||||
private fun getCheckmarkCount(): Int {
|
||||
val nameWidth = dim(R.dimen.habitNameWidth)
|
||||
val buttonWidth = dim(R.dimen.checkmarkWidth)
|
||||
val labelWidth = max((measuredWidth / 3).toFloat(), nameWidth)
|
||||
val buttonCount = ((measuredWidth - labelWidth) / buttonWidth).toInt()
|
||||
return min(MAX_CHECKMARK_COUNT, max(0, buttonCount))
|
||||
}
|
||||
|
||||
private fun updateEmptyView() {
|
||||
llEmpty.visibility = when (listAdapter.itemCount) {
|
||||
0 -> VISIBLE
|
||||
else -> GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import dagger.Lazy
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
|
||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmSyncKeyDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
|
||||
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
|
||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
||||
import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
|
||||
import org.isoron.uhabits.core.commands.ChangeHabitColorCommand
|
||||
import org.isoron.uhabits.core.commands.Command
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.commands.CreateHabitCommand
|
||||
import org.isoron.uhabits.core.commands.DeleteHabitsCommand
|
||||
import org.isoron.uhabits.core.commands.EditHabitCommand
|
||||
import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.tasks.TaskRunner
|
||||
import org.isoron.uhabits.core.ui.ThemeSwitcher
|
||||
import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback
|
||||
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.COULD_NOT_EXPORT
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.COULD_NOT_GENERATE_BUG_REPORT
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.DATABASE_REPAIRED
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.FILE_NOT_RECOGNIZED
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.IMPORT_FAILED
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.IMPORT_SUCCESSFUL
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.SYNC_ENABLED
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.SYNC_KEY_ALREADY_INSTALLED
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsMenuBehavior
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBehavior
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.inject.ActivityScope
|
||||
import org.isoron.uhabits.intents.IntentFactory
|
||||
import org.isoron.uhabits.tasks.ExportDBTaskFactory
|
||||
import org.isoron.uhabits.tasks.ImportDataTask
|
||||
import org.isoron.uhabits.tasks.ImportDataTaskFactory
|
||||
import org.isoron.uhabits.utils.copyTo
|
||||
import org.isoron.uhabits.utils.restartWithFade
|
||||
import org.isoron.uhabits.utils.showMessage
|
||||
import org.isoron.uhabits.utils.showSendEmailScreen
|
||||
import org.isoron.uhabits.utils.showSendFileScreen
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
const val RESULT_IMPORT_DATA = 101
|
||||
const val RESULT_EXPORT_CSV = 102
|
||||
const val RESULT_EXPORT_DB = 103
|
||||
const val RESULT_BUG_REPORT = 104
|
||||
const val RESULT_REPAIR_DB = 105
|
||||
const val REQUEST_OPEN_DOCUMENT = 106
|
||||
const val REQUEST_SETTINGS = 107
|
||||
|
||||
@ActivityScope
|
||||
class ListHabitsScreen
|
||||
@Inject constructor(
|
||||
@ActivityContext val context: Context,
|
||||
private val commandRunner: CommandRunner,
|
||||
private val intentFactory: IntentFactory,
|
||||
private val themeSwitcher: ThemeSwitcher,
|
||||
private val adapter: HabitCardListAdapter,
|
||||
private val taskRunner: TaskRunner,
|
||||
private val exportDBFactory: ExportDBTaskFactory,
|
||||
private val importTaskFactory: ImportDataTaskFactory,
|
||||
private val colorPickerFactory: ColorPickerDialogFactory,
|
||||
private val numberPickerFactory: NumberPickerFactory,
|
||||
private val behavior: Lazy<ListHabitsBehavior>
|
||||
) : CommandRunner.Listener,
|
||||
ListHabitsBehavior.Screen,
|
||||
ListHabitsMenuBehavior.Screen,
|
||||
ListHabitsSelectionMenuBehavior.Screen {
|
||||
|
||||
val activity = (context as AppCompatActivity)
|
||||
|
||||
fun onAttached() {
|
||||
commandRunner.addListener(this)
|
||||
if (activity.intent.action == "android.intent.action.VIEW") {
|
||||
val uri = activity.intent.data!!.toString()
|
||||
val parts = uri.replace(Regex("^.*sync/"), "").split("#")
|
||||
val syncKey = parts[0]
|
||||
val encKey = parts[1]
|
||||
Log.i("ListHabitsScreen", "sync: $syncKey enc: $encKey")
|
||||
behavior.get().onSyncKeyOffer(syncKey, encKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDettached() {
|
||||
commandRunner.removeListener(this)
|
||||
}
|
||||
|
||||
override fun onCommandFinished(command: Command) {
|
||||
val msg = getExecuteString(command)
|
||||
if (msg != null) activity.showMessage(msg)
|
||||
}
|
||||
|
||||
fun onResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_OPEN_DOCUMENT -> onOpenDocumentResult(resultCode, data)
|
||||
REQUEST_SETTINGS -> onSettingsResult(resultCode)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOpenDocumentResult(resultCode: Int, data: Intent?) {
|
||||
if (data == null) return
|
||||
if (resultCode != Activity.RESULT_OK) return
|
||||
try {
|
||||
val inStream = activity.contentResolver.openInputStream(data.data!!)!!
|
||||
val cacheDir = activity.externalCacheDir
|
||||
val tempFile = File.createTempFile("import", "", cacheDir)
|
||||
inStream.copyTo(tempFile)
|
||||
onImportData(tempFile) { tempFile.delete() }
|
||||
} catch (e: IOException) {
|
||||
activity.showMessage(activity.resources.getString(R.string.could_not_import))
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSettingsResult(resultCode: Int) {
|
||||
when (resultCode) {
|
||||
RESULT_IMPORT_DATA -> showImportScreen()
|
||||
RESULT_EXPORT_CSV -> behavior.get().onExportCSV()
|
||||
RESULT_EXPORT_DB -> onExportDB()
|
||||
RESULT_BUG_REPORT -> behavior.get().onSendBugReport()
|
||||
RESULT_REPAIR_DB -> behavior.get().onRepairDB()
|
||||
}
|
||||
}
|
||||
|
||||
override fun applyTheme() {
|
||||
themeSwitcher.apply()
|
||||
activity.restartWithFade(ListHabitsActivity::class.java)
|
||||
}
|
||||
|
||||
override fun showAboutScreen() {
|
||||
val intent = intentFactory.startAboutActivity(activity)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun showSelectHabitTypeDialog() {
|
||||
val dialog = HabitTypeDialog()
|
||||
dialog.show(activity.supportFragmentManager, "habitType")
|
||||
}
|
||||
|
||||
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback, quantity: Int) {
|
||||
ConfirmDeleteDialog(activity, callback, quantity).show()
|
||||
}
|
||||
|
||||
override fun showEditHabitsScreen(habits: List<Habit>) {
|
||||
val intent = intentFactory.startEditActivity(activity, habits[0])
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun showFAQScreen() {
|
||||
val intent = intentFactory.viewFAQ(activity)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun showHabitScreen(habit: Habit) {
|
||||
val intent = intentFactory.startShowHabitActivity(activity, habit)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
fun showImportScreen() {
|
||||
val intent = intentFactory.openDocument()
|
||||
activity.startActivityForResult(intent, REQUEST_OPEN_DOCUMENT)
|
||||
}
|
||||
|
||||
override fun showIntroScreen() {
|
||||
val intent = intentFactory.startIntroActivity(activity)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun showMessage(m: ListHabitsBehavior.Message) {
|
||||
activity.showMessage(
|
||||
activity.resources.getString(
|
||||
when (m) {
|
||||
COULD_NOT_EXPORT -> R.string.could_not_export
|
||||
IMPORT_SUCCESSFUL -> R.string.habits_imported
|
||||
IMPORT_FAILED -> R.string.could_not_import
|
||||
DATABASE_REPAIRED -> R.string.database_repaired
|
||||
COULD_NOT_GENERATE_BUG_REPORT -> R.string.bug_report_failed
|
||||
FILE_NOT_RECOGNIZED -> R.string.file_not_recognized
|
||||
SYNC_ENABLED -> R.string.sync_enabled
|
||||
SYNC_KEY_ALREADY_INSTALLED -> R.string.sync_key_already_installed
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun showSendBugReportToDeveloperScreen(log: String) {
|
||||
val to = R.string.bugReportTo
|
||||
val subject = R.string.bugReportSubject
|
||||
activity.showSendEmailScreen(to, subject, log)
|
||||
}
|
||||
|
||||
override fun showSendFileScreen(filename: String) {
|
||||
activity.showSendFileScreen(filename)
|
||||
}
|
||||
|
||||
override fun showSettingsScreen() {
|
||||
val intent = intentFactory.startSettingsActivity(activity)
|
||||
activity.startActivityForResult(intent, REQUEST_SETTINGS)
|
||||
}
|
||||
|
||||
override fun showColorPicker(
|
||||
defaultColor: PaletteColor,
|
||||
callback: OnColorPickedCallback
|
||||
) {
|
||||
val picker = colorPickerFactory.create(defaultColor)
|
||||
picker.setListener(callback)
|
||||
picker.show(activity.supportFragmentManager, "picker")
|
||||
}
|
||||
|
||||
override fun showNumberPicker(
|
||||
value: Double,
|
||||
unit: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback
|
||||
) {
|
||||
numberPickerFactory.create(value, unit, callback).show()
|
||||
}
|
||||
|
||||
override fun showConfirmInstallSyncKey(callback: OnConfirmedCallback) {
|
||||
ConfirmSyncKeyDialog(activity, callback).show()
|
||||
}
|
||||
|
||||
private fun getExecuteString(command: Command): String? {
|
||||
when (command) {
|
||||
is ArchiveHabitsCommand -> {
|
||||
return activity.resources.getQuantityString(
|
||||
R.plurals.toast_habits_archived,
|
||||
command.selected.size
|
||||
)
|
||||
}
|
||||
is ChangeHabitColorCommand -> {
|
||||
return activity.resources.getQuantityString(
|
||||
R.plurals.toast_habits_changed,
|
||||
command.selected.size
|
||||
)
|
||||
}
|
||||
is CreateHabitCommand -> {
|
||||
return activity.resources.getString(R.string.toast_habit_created)
|
||||
}
|
||||
is DeleteHabitsCommand -> {
|
||||
return activity.resources.getQuantityString(
|
||||
R.plurals.toast_habits_deleted,
|
||||
command.selected.size
|
||||
)
|
||||
}
|
||||
is EditHabitCommand -> {
|
||||
return activity.resources.getQuantityString(R.plurals.toast_habits_changed, 1)
|
||||
}
|
||||
is UnarchiveHabitsCommand -> {
|
||||
return activity.resources.getQuantityString(
|
||||
R.plurals.toast_habits_unarchived,
|
||||
command.selected.size
|
||||
)
|
||||
}
|
||||
else -> return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun onImportData(file: File, onFinished: () -> Unit) {
|
||||
taskRunner.execute(
|
||||
importTaskFactory.create(file) { result ->
|
||||
if (result == ImportDataTask.SUCCESS) {
|
||||
adapter.refresh()
|
||||
activity.showMessage(activity.resources.getString(R.string.habits_imported))
|
||||
} else if (result == ImportDataTask.NOT_RECOGNIZED) {
|
||||
activity.showMessage(activity.resources.getString(R.string.file_not_recognized))
|
||||
} else {
|
||||
activity.showMessage(activity.resources.getString(R.string.could_not_import))
|
||||
}
|
||||
onFinished()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onExportDB() {
|
||||
taskRunner.execute(
|
||||
exportDBFactory.create { filename ->
|
||||
if (filename != null) activity.showSendFileScreen(filename)
|
||||
else activity.showMessage(activity.resources.getString(R.string.could_not_export))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list
|
||||
|
||||
import android.content.Context
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import dagger.Lazy
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardListController
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.NotificationTray
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBehavior
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.inject.ActivityScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ActivityScope
|
||||
class ListHabitsSelectionMenu @Inject constructor(
|
||||
@ActivityContext context: Context,
|
||||
private val listAdapter: HabitCardListAdapter,
|
||||
var commandRunner: CommandRunner,
|
||||
private val prefs: Preferences,
|
||||
private val behavior: ListHabitsSelectionMenuBehavior,
|
||||
private val listController: Lazy<HabitCardListController>,
|
||||
private val notificationTray: NotificationTray
|
||||
) : ActionMode.Callback {
|
||||
|
||||
val activity = (context as AppCompatActivity)
|
||||
|
||||
var activeActionMode: ActionMode? = null
|
||||
|
||||
fun onSelectionStart() {
|
||||
activity.startSupportActionMode(this)
|
||||
}
|
||||
|
||||
fun onSelectionChange() {
|
||||
activeActionMode?.invalidate()
|
||||
}
|
||||
|
||||
fun onSelectionFinish() {
|
||||
activeActionMode?.finish()
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
activeActionMode = mode
|
||||
activity.menuInflater.inflate(R.menu.list_habits_selection, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val itemEdit = menu.findItem(R.id.action_edit_habit)
|
||||
val itemColor = menu.findItem(R.id.action_color)
|
||||
val itemArchive = menu.findItem(R.id.action_archive_habit)
|
||||
val itemUnarchive = menu.findItem(R.id.action_unarchive_habit)
|
||||
val itemNotify = menu.findItem(R.id.action_notify)
|
||||
|
||||
itemColor.isVisible = true
|
||||
itemEdit.isVisible = behavior.canEdit()
|
||||
itemArchive.isVisible = behavior.canArchive()
|
||||
itemUnarchive.isVisible = behavior.canUnarchive()
|
||||
itemNotify.isVisible = prefs.isDeveloper
|
||||
activeActionMode?.title = listAdapter.selected.size.toString()
|
||||
return true
|
||||
}
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
listController.get().onSelectionFinished()
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_edit_habit -> {
|
||||
behavior.onEditHabits()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.action_archive_habit -> {
|
||||
behavior.onArchiveHabits()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.action_unarchive_habit -> {
|
||||
behavior.onUnarchiveHabits()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.action_delete -> {
|
||||
behavior.onDeleteHabits()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.action_color -> {
|
||||
behavior.onChangeColor()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.action_notify -> {
|
||||
for (h in listAdapter.selected)
|
||||
notificationTray.show(h, DateUtils.getToday(), 0)
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.View.MeasureSpec.EXACTLY
|
||||
import android.widget.LinearLayout
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.utils.dim
|
||||
import org.isoron.uhabits.utils.toMeasureSpec
|
||||
|
||||
abstract class ButtonPanelView<T : View>(
|
||||
context: Context,
|
||||
val preferences: Preferences
|
||||
) : LinearLayout(context),
|
||||
Preferences.Listener {
|
||||
|
||||
var buttonCount = 0
|
||||
set(value) {
|
||||
field = value
|
||||
inflateButtons()
|
||||
}
|
||||
|
||||
var dataOffset = 0
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var buttons = mutableListOf<T>()
|
||||
|
||||
override fun onCheckmarkSequenceChanged() {
|
||||
inflateButtons()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
protected fun inflateButtons() {
|
||||
val reverse = preferences.isCheckmarkSequenceReversed
|
||||
|
||||
buttons.clear()
|
||||
repeat(buttonCount) { buttons.add(createButton()) }
|
||||
|
||||
removeAllViews()
|
||||
if (reverse) buttons.reversed().forEach { addView(it) }
|
||||
else buttons.forEach { addView(it) }
|
||||
setupButtons()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
public override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
preferences.addListener(this)
|
||||
}
|
||||
|
||||
public override fun onDetachedFromWindow() {
|
||||
preferences.removeListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
||||
val buttonWidth = dim(R.dimen.checkmarkWidth)
|
||||
val buttonHeight = dim(R.dimen.checkmarkHeight)
|
||||
val width = (buttonWidth * buttonCount)
|
||||
super.onMeasure(
|
||||
width.toMeasureSpec(EXACTLY),
|
||||
buttonHeight.toMeasureSpec(EXACTLY)
|
||||
)
|
||||
}
|
||||
|
||||
protected abstract fun setupButtons()
|
||||
protected abstract fun createButton(): T
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.text.TextPaint
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.View
|
||||
import android.view.View.MeasureSpec.EXACTLY
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.NO
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.utils.dim
|
||||
import org.isoron.uhabits.utils.getFontAwesome
|
||||
import org.isoron.uhabits.utils.showMessage
|
||||
import org.isoron.uhabits.utils.sres
|
||||
import org.isoron.uhabits.utils.toMeasureSpec
|
||||
import javax.inject.Inject
|
||||
|
||||
class CheckmarkButtonViewFactory
|
||||
@Inject constructor(
|
||||
@ActivityContext val context: Context,
|
||||
val preferences: Preferences
|
||||
) {
|
||||
fun create() = CheckmarkButtonView(context, preferences)
|
||||
}
|
||||
|
||||
class CheckmarkButtonView(
|
||||
context: Context,
|
||||
val preferences: Preferences
|
||||
) : View(context),
|
||||
View.OnClickListener,
|
||||
View.OnLongClickListener {
|
||||
|
||||
var color: Int = Color.BLACK
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var value: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var onToggle: (Int) -> Unit = {}
|
||||
private var drawer = Drawer()
|
||||
|
||||
init {
|
||||
isFocusable = false
|
||||
setOnClickListener(this)
|
||||
setOnLongClickListener(this)
|
||||
}
|
||||
|
||||
fun performToggle() {
|
||||
value = if (preferences.isSkipEnabled) {
|
||||
Entry.nextToggleValueWithSkip(value)
|
||||
} else {
|
||||
Entry.nextToggleValueWithoutSkip(value)
|
||||
}
|
||||
onToggle(value)
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (preferences.isShortToggleEnabled) performToggle()
|
||||
else showMessage(resources.getString(R.string.long_press_to_toggle))
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
performToggle()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
drawer.draw(canvas)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val height = resources.getDimensionPixelSize(R.dimen.checkmarkHeight)
|
||||
val width = resources.getDimensionPixelSize(R.dimen.checkmarkWidth)
|
||||
super.onMeasure(
|
||||
width.toMeasureSpec(EXACTLY),
|
||||
height.toMeasureSpec(EXACTLY)
|
||||
)
|
||||
}
|
||||
|
||||
private inner class Drawer {
|
||||
private val rect = RectF()
|
||||
private val lowContrastColor = sres.getColor(R.attr.lowContrastTextColor)
|
||||
|
||||
private val paint = TextPaint().apply {
|
||||
typeface = getFontAwesome()
|
||||
isAntiAlias = true
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = dim(R.dimen.smallTextSize)
|
||||
}
|
||||
|
||||
fun draw(canvas: Canvas) {
|
||||
paint.color = when (value) {
|
||||
YES_MANUAL -> color
|
||||
SKIP -> color
|
||||
else -> lowContrastColor
|
||||
}
|
||||
val id = when (value) {
|
||||
SKIP -> R.string.fa_skipped
|
||||
NO -> R.string.fa_times
|
||||
UNKNOWN -> {
|
||||
if (preferences.areQuestionMarksEnabled()) R.string.fa_question
|
||||
else R.string.fa_times
|
||||
}
|
||||
else -> R.string.fa_check
|
||||
}
|
||||
val label = resources.getString(id)
|
||||
val em = paint.measureText("m")
|
||||
|
||||
rect.set(0f, 0f, width.toFloat(), height.toFloat())
|
||||
rect.offset(0f, 0.4f * em)
|
||||
canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.Context
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class CheckmarkPanelViewFactory
|
||||
@Inject constructor(
|
||||
@ActivityContext val context: Context,
|
||||
val preferences: Preferences,
|
||||
private val buttonFactory: CheckmarkButtonViewFactory
|
||||
) {
|
||||
fun create() = CheckmarkPanelView(context, preferences, buttonFactory)
|
||||
}
|
||||
|
||||
class CheckmarkPanelView(
|
||||
context: Context,
|
||||
preferences: Preferences,
|
||||
private val buttonFactory: CheckmarkButtonViewFactory
|
||||
) : ButtonPanelView<CheckmarkButtonView>(context, preferences) {
|
||||
|
||||
var values = IntArray(0)
|
||||
set(values) {
|
||||
field = values
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var color = 0
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onToggle: (Timestamp, Int) -> Unit = { _, _ -> }
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
override fun createButton(): CheckmarkButtonView = buttonFactory.create()
|
||||
|
||||
@Synchronized
|
||||
override fun setupButtons() {
|
||||
val today = DateUtils.getTodayWithOffset()
|
||||
|
||||
buttons.forEachIndexed { index, button ->
|
||||
val timestamp = today.minus(index + dataOffset)
|
||||
button.value = when {
|
||||
index + dataOffset < values.size -> values[index + dataOffset]
|
||||
else -> UNKNOWN
|
||||
}
|
||||
button.color = color
|
||||
button.onToggle = { value -> onToggle(timestamp, value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.Context
|
||||
import android.view.Gravity.CENTER
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.utils.dp
|
||||
import org.isoron.uhabits.utils.getFontAwesome
|
||||
import org.isoron.uhabits.utils.sp
|
||||
import org.isoron.uhabits.utils.sres
|
||||
import org.isoron.uhabits.utils.str
|
||||
|
||||
class EmptyListView(context: Context) : LinearLayout(context) {
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
gravity = CENTER
|
||||
visibility = View.GONE
|
||||
|
||||
addView(
|
||||
TextView(context).apply {
|
||||
text = str(R.string.fa_star_half_o)
|
||||
typeface = getFontAwesome()
|
||||
textSize = sp(40.0f)
|
||||
gravity = CENTER
|
||||
setTextColor(sres.getColor(R.attr.mediumContrastTextColor))
|
||||
},
|
||||
MATCH_PARENT,
|
||||
WRAP_CONTENT
|
||||
)
|
||||
|
||||
addView(
|
||||
TextView(context).apply {
|
||||
text = str(R.string.no_habits_found)
|
||||
gravity = CENTER
|
||||
setPadding(0, dp(20.0f).toInt(), 0, 0)
|
||||
setTextColor(sres.getColor(R.attr.mediumContrastTextColor))
|
||||
},
|
||||
MATCH_PARENT,
|
||||
WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import android.view.*;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.isoron.uhabits.activities.habits.list.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.inject.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
/**
|
||||
* Provides data that backs a {@link HabitCardListView}.
|
||||
* <p>
|
||||
* The data if fetched and cached by a {@link HabitCardListCache}. This adapter
|
||||
* also holds a list of items that have been selected.
|
||||
*/
|
||||
@ActivityScope
|
||||
public class HabitCardListAdapter
|
||||
extends RecyclerView.Adapter<HabitCardViewHolder> implements
|
||||
HabitCardListCache.Listener,
|
||||
MidnightTimer.MidnightListener,
|
||||
ListHabitsMenuBehavior.Adapter,
|
||||
ListHabitsSelectionMenuBehavior.Adapter
|
||||
{
|
||||
@NonNull
|
||||
private ModelObservable observable;
|
||||
|
||||
@Nullable
|
||||
private HabitCardListView listView;
|
||||
|
||||
@NonNull
|
||||
private final LinkedList<Habit> selected;
|
||||
|
||||
@NonNull
|
||||
private final HabitCardListCache cache;
|
||||
|
||||
@NonNull
|
||||
private Preferences preferences;
|
||||
|
||||
private final MidnightTimer midnightTimer;
|
||||
|
||||
@Inject
|
||||
public HabitCardListAdapter(@NonNull HabitCardListCache cache,
|
||||
@NonNull Preferences preferences,
|
||||
@NonNull MidnightTimer midnightTimer)
|
||||
{
|
||||
this.preferences = preferences;
|
||||
this.selected = new LinkedList<>();
|
||||
this.observable = new ModelObservable();
|
||||
this.cache = cache;
|
||||
|
||||
this.midnightTimer = midnightTimer;
|
||||
|
||||
cache.setListener(this);
|
||||
cache.setCheckmarkCount(
|
||||
ListHabitsRootViewKt.MAX_CHECKMARK_COUNT);
|
||||
cache.setSecondaryOrder(preferences.getDefaultSecondaryOrder());
|
||||
cache.setPrimaryOrder(preferences.getDefaultPrimaryOrder());
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void atMidnight()
|
||||
{
|
||||
cache.refreshAllHabits();
|
||||
}
|
||||
|
||||
public void cancelRefresh()
|
||||
{
|
||||
cache.cancelTasks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all items as not selected.
|
||||
*/
|
||||
@Override
|
||||
public void clearSelection()
|
||||
{
|
||||
selected.clear();
|
||||
notifyDataSetChanged();
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item that occupies a certain position on the list
|
||||
*
|
||||
* @param position position of the item
|
||||
* @return the item at given position or null if position is invalid
|
||||
*/
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public Habit getItem(int position)
|
||||
{
|
||||
return cache.getHabitByPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount()
|
||||
{
|
||||
return cache.getHabitCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position)
|
||||
{
|
||||
return getItem(position).getId();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public ModelObservable getObservable()
|
||||
{
|
||||
return observable;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public List<Habit> getSelected()
|
||||
{
|
||||
return new LinkedList<>(selected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether list of selected items is empty.
|
||||
*
|
||||
* @return true if selection is empty, false otherwise
|
||||
*/
|
||||
public boolean isSelectionEmpty()
|
||||
{
|
||||
return selected.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isSortable()
|
||||
{
|
||||
return cache.getPrimaryOrder() == HabitList.Order.BY_POSITION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the adapter that it has been attached to a ListView.
|
||||
*/
|
||||
public void onAttached()
|
||||
{
|
||||
cache.onAttached();
|
||||
midnightTimer.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@Nullable HabitCardViewHolder holder,
|
||||
int position)
|
||||
{
|
||||
if (holder == null) return;
|
||||
if (listView == null) return;
|
||||
|
||||
Habit habit = cache.getHabitByPosition(position);
|
||||
double score = cache.getScore(habit.getId());
|
||||
int checkmarks[] = cache.getCheckmarks(habit.getId());
|
||||
boolean selected = this.selected.contains(habit);
|
||||
|
||||
listView.bindCardView(holder, habit, score, checkmarks, selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@Nullable HabitCardViewHolder holder)
|
||||
{
|
||||
if (listView == null) return;
|
||||
listView.attachCardView(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@Nullable HabitCardViewHolder holder)
|
||||
{
|
||||
if (listView == null) return;
|
||||
listView.detachCardView(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HabitCardViewHolder onCreateViewHolder(ViewGroup parent,
|
||||
int viewType)
|
||||
{
|
||||
if (listView == null) return null;
|
||||
View view = listView.createHabitCardView();
|
||||
return new HabitCardViewHolder(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the adapter that it has been detached from a ListView.
|
||||
*/
|
||||
public void onDetached()
|
||||
{
|
||||
cache.onDetached();
|
||||
midnightTimer.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemChanged(int position)
|
||||
{
|
||||
notifyItemChanged(position);
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemInserted(int position)
|
||||
{
|
||||
notifyItemInserted(position);
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemMoved(int fromPosition, int toPosition)
|
||||
{
|
||||
notifyItemMoved(fromPosition, toPosition);
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRemoved(int position)
|
||||
{
|
||||
notifyItemRemoved(position);
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefreshFinished()
|
||||
{
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a list of habits from the adapter.
|
||||
* <p>
|
||||
* Note that this only has effect on the adapter cache. The database is not
|
||||
* modified, and the change is lost when the cache is refreshed. This method
|
||||
* is useful for making the ListView more responsive: while we wait for the
|
||||
* database operation to finish, the cache can be modified to reflect the
|
||||
* changes immediately.
|
||||
*
|
||||
* @param habits list of habits to be removed
|
||||
*/
|
||||
@Override
|
||||
public void performRemove(List<Habit> habits)
|
||||
{
|
||||
for (Habit h : habits)
|
||||
cache.remove(h.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the order of habits on the adapter.
|
||||
* <p>
|
||||
* Note that this only has effect on the adapter cache. The database is not
|
||||
* modified, and the change is lost when the cache is refreshed. This method
|
||||
* is useful for making the ListView more responsive: while we wait for the
|
||||
* database operation to finish, the cache can be modified to reflect the
|
||||
* changes immediately.
|
||||
*
|
||||
* @param from the habit that should be moved
|
||||
* @param to the habit that currently occupies the desired position
|
||||
*/
|
||||
public void performReorder(int from, int to)
|
||||
{
|
||||
cache.reorder(from, to);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh()
|
||||
{
|
||||
cache.refreshAllHabits();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFilter(HabitMatcher matcher)
|
||||
{
|
||||
cache.setFilter(matcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HabitCardListView that this adapter will provide data for.
|
||||
* <p>
|
||||
* This object will be used to generated new HabitCardViews, upon demand.
|
||||
*
|
||||
* @param listView the HabitCardListView associated with this adapter
|
||||
*/
|
||||
public void setListView(@Nullable HabitCardListView listView)
|
||||
{
|
||||
this.listView = listView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPrimaryOrder(HabitList.Order order)
|
||||
{
|
||||
cache.setPrimaryOrder(order);
|
||||
preferences.setDefaultPrimaryOrder(order);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSecondaryOrder(HabitList.Order order) {
|
||||
cache.setSecondaryOrder(order);
|
||||
preferences.setDefaultSecondaryOrder(order);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HabitList.Order getPrimaryOrder()
|
||||
{
|
||||
return cache.getPrimaryOrder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects or deselects the item at a given position.
|
||||
*
|
||||
* @param position position of the item to be toggled
|
||||
*/
|
||||
public void toggleSelection(int position)
|
||||
{
|
||||
Habit h = getItem(position);
|
||||
if (h == null) return;
|
||||
|
||||
int k = selected.indexOf(h);
|
||||
if (k < 0) selected.add(h);
|
||||
else selected.remove(h);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import dagger.Lazy
|
||||
import org.isoron.uhabits.activities.habits.list.ListHabitsSelectionMenu
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.ModelObservable
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
import org.isoron.uhabits.inject.ActivityScope
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Controller responsible for receiving and processing the events generated by a
|
||||
* HabitListView. These include selecting and reordering items, toggling
|
||||
* checkmarks and clicking habits.
|
||||
*/
|
||||
@ActivityScope
|
||||
class HabitCardListController @Inject constructor(
|
||||
private val adapter: HabitCardListAdapter,
|
||||
private val behavior: ListHabitsBehavior,
|
||||
private val selectionMenu: Lazy<ListHabitsSelectionMenu>
|
||||
) : HabitCardListView.Controller, ModelObservable.Listener {
|
||||
|
||||
private val NORMAL_MODE = NormalMode()
|
||||
private val SELECTION_MODE = SelectionMode()
|
||||
private var activeMode: Mode
|
||||
|
||||
init {
|
||||
this.activeMode = NORMAL_MODE
|
||||
adapter.observable.addListener(this)
|
||||
}
|
||||
|
||||
override fun drop(from: Int, to: Int) {
|
||||
if (from == to) return
|
||||
cancelSelection()
|
||||
|
||||
val habitFrom = adapter.getItem(from)
|
||||
val habitTo = adapter.getItem(to)
|
||||
if (habitFrom == null || habitTo == null) return
|
||||
|
||||
adapter.performReorder(from, to)
|
||||
behavior.onReorderHabit(habitFrom, habitTo)
|
||||
}
|
||||
|
||||
override fun onItemClick(position: Int) {
|
||||
activeMode.onItemClick(position)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(position: Int) {
|
||||
activeMode.onItemLongClick(position)
|
||||
}
|
||||
|
||||
override fun onModelChange() {
|
||||
if (adapter.isSelectionEmpty) {
|
||||
activeMode = NormalMode()
|
||||
selectionMenu.get().onSelectionFinish()
|
||||
}
|
||||
}
|
||||
|
||||
fun onSelectionFinished() {
|
||||
cancelSelection()
|
||||
}
|
||||
|
||||
override fun startDrag(position: Int) {
|
||||
activeMode.startDrag(position)
|
||||
}
|
||||
|
||||
protected fun toggleSelection(position: Int) {
|
||||
adapter.toggleSelection(position)
|
||||
activeMode = if (adapter.isSelectionEmpty) NORMAL_MODE else SELECTION_MODE
|
||||
}
|
||||
|
||||
private fun cancelSelection() {
|
||||
adapter.clearSelection()
|
||||
activeMode = NormalMode()
|
||||
selectionMenu.get().onSelectionFinish()
|
||||
}
|
||||
|
||||
interface HabitListener {
|
||||
fun onHabitClick(habit: Habit)
|
||||
fun onHabitReorder(from: Habit, to: Habit)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Mode describes the behavior of the list upon clicking, long clicking
|
||||
* and dragging an item. This depends on whether some items are already
|
||||
* selected or not.
|
||||
*/
|
||||
private interface Mode {
|
||||
fun onItemClick(position: Int)
|
||||
fun onItemLongClick(position: Int): Boolean
|
||||
fun startDrag(position: Int)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode activated when there are no items selected. Clicks trigger habit
|
||||
* click. Long clicks start selection.
|
||||
*/
|
||||
internal inner class NormalMode : Mode {
|
||||
override fun onItemClick(position: Int) {
|
||||
val habit = adapter.getItem(position)
|
||||
if (habit == null) return
|
||||
behavior.onClickHabit(habit)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(position: Int): Boolean {
|
||||
startSelection(position)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun startDrag(position: Int) {
|
||||
startSelection(position)
|
||||
}
|
||||
|
||||
protected fun startSelection(position: Int) {
|
||||
toggleSelection(position)
|
||||
activeMode = SELECTION_MODE
|
||||
selectionMenu.get().onSelectionStart()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode activated when some items are already selected. Clicks toggle
|
||||
* item selection. Long clicks select more items.
|
||||
*/
|
||||
internal inner class SelectionMode : Mode {
|
||||
override fun onItemClick(position: Int) {
|
||||
toggleSelection(position)
|
||||
notifyListener()
|
||||
}
|
||||
|
||||
override fun onItemLongClick(position: Int): Boolean {
|
||||
toggleSelection(position)
|
||||
notifyListener()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun startDrag(position: Int) {
|
||||
toggleSelection(position)
|
||||
notifyListener()
|
||||
}
|
||||
|
||||
protected fun notifyListener() {
|
||||
if (activeMode === SELECTION_MODE)
|
||||
selectionMenu.get().onSelectionChange()
|
||||
else
|
||||
selectionMenu.get().onSelectionFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.END
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.START
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.UP
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dagger.Lazy
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.common.views.BundleSavedState
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class HabitCardListViewFactory
|
||||
@Inject constructor(
|
||||
@ActivityContext val context: Context,
|
||||
val adapter: HabitCardListAdapter,
|
||||
val cardViewFactory: HabitCardViewFactory,
|
||||
val controller: Lazy<HabitCardListController>
|
||||
) {
|
||||
fun create() = HabitCardListView(context, adapter, cardViewFactory, controller)
|
||||
}
|
||||
|
||||
class HabitCardListView(
|
||||
@ActivityContext context: Context,
|
||||
private val adapter: HabitCardListAdapter,
|
||||
private val cardViewFactory: HabitCardViewFactory,
|
||||
private val controller: Lazy<HabitCardListController>
|
||||
) : RecyclerView(context, null, R.attr.scrollableRecyclerViewStyle) {
|
||||
|
||||
var checkmarkCount: Int = 0
|
||||
|
||||
var dataOffset: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
attachedHolders
|
||||
.map { it.itemView as HabitCardView }
|
||||
.forEach { it.dataOffset = value }
|
||||
}
|
||||
|
||||
private val attachedHolders = mutableListOf<HabitCardViewHolder>()
|
||||
private val touchHelper = ItemTouchHelper(TouchHelperCallback()).apply {
|
||||
attachToRecyclerView(this@HabitCardListView)
|
||||
}
|
||||
|
||||
init {
|
||||
setHasFixedSize(true)
|
||||
isLongClickable = true
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
super.setAdapter(adapter)
|
||||
}
|
||||
|
||||
fun createHabitCardView(): View {
|
||||
return cardViewFactory.create()
|
||||
}
|
||||
|
||||
fun bindCardView(
|
||||
holder: HabitCardViewHolder,
|
||||
habit: Habit,
|
||||
score: Double,
|
||||
checkmarks: IntArray,
|
||||
selected: Boolean
|
||||
): View {
|
||||
val cardView = holder.itemView as HabitCardView
|
||||
cardView.habit = habit
|
||||
cardView.isSelected = selected
|
||||
cardView.values = checkmarks
|
||||
cardView.buttonCount = checkmarkCount
|
||||
cardView.dataOffset = dataOffset
|
||||
cardView.score = score
|
||||
cardView.unit = habit.unit
|
||||
cardView.threshold = habit.targetValue / habit.frequency.denominator
|
||||
|
||||
val detector = GestureDetector(context, CardViewGestureDetector(holder))
|
||||
cardView.setOnTouchListener { _, ev ->
|
||||
detector.onTouchEvent(ev)
|
||||
true
|
||||
}
|
||||
|
||||
return cardView
|
||||
}
|
||||
|
||||
fun attachCardView(holder: HabitCardViewHolder) {
|
||||
attachedHolders.add(holder)
|
||||
}
|
||||
|
||||
fun detachCardView(holder: HabitCardViewHolder) {
|
||||
attachedHolders.remove(holder)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
adapter.onAttached()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
adapter.onDetached()
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable) {
|
||||
if (state !is BundleSavedState) {
|
||||
super.onRestoreInstanceState(state)
|
||||
return
|
||||
}
|
||||
dataOffset = state.bundle.getInt("dataOffset")
|
||||
super.onRestoreInstanceState(state.superState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
val superState = super.onSaveInstanceState()
|
||||
val bundle = Bundle().apply {
|
||||
putInt("dataOffset", dataOffset)
|
||||
}
|
||||
return BundleSavedState(superState, bundle)
|
||||
}
|
||||
|
||||
interface Controller {
|
||||
fun drop(from: Int, to: Int) {}
|
||||
fun onItemClick(pos: Int) {}
|
||||
fun onItemLongClick(pos: Int) {}
|
||||
fun startDrag(position: Int) {}
|
||||
}
|
||||
|
||||
private inner class CardViewGestureDetector(
|
||||
private val holder: HabitCardViewHolder
|
||||
) : GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
override fun onLongPress(e: MotionEvent) {
|
||||
val position = holder.adapterPosition
|
||||
controller.get().onItemLongClick(position)
|
||||
if (adapter.isSortable) touchHelper.startDrag(holder)
|
||||
}
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
val position = holder.adapterPosition
|
||||
controller.get().onItemClick(position)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
inner class TouchHelperCallback : ItemTouchHelper.Callback() {
|
||||
override fun getMovementFlags(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
): Int {
|
||||
return makeMovementFlags(UP or DOWN, START or END)
|
||||
}
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
from: RecyclerView.ViewHolder,
|
||||
to: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
controller.get().drop(from.adapterPosition, to.adapterPosition)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSwiped(
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
direction: Int
|
||||
) {
|
||||
}
|
||||
|
||||
override fun isItemViewSwipeEnabled() = false
|
||||
override fun isLongPressDragEnabled() = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Build.VERSION_CODES.LOLLIPOP
|
||||
import android.os.Build.VERSION_CODES.M
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.Layout
|
||||
import android.text.TextUtils
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.common.views.RingView
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.ModelObservable
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.utils.dp
|
||||
import org.isoron.uhabits.utils.sres
|
||||
import org.isoron.uhabits.utils.toThemedAndroidColor
|
||||
import javax.inject.Inject
|
||||
|
||||
class HabitCardViewFactory
|
||||
@Inject constructor(
|
||||
@ActivityContext val context: Context,
|
||||
private val checkmarkPanelFactory: CheckmarkPanelViewFactory,
|
||||
private val numberPanelFactory: NumberPanelViewFactory,
|
||||
private val behavior: ListHabitsBehavior
|
||||
) {
|
||||
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
|
||||
}
|
||||
|
||||
class HabitCardView(
|
||||
@ActivityContext context: Context,
|
||||
private val checkmarkPanelFactory: CheckmarkPanelViewFactory,
|
||||
private val numberPanelFactory: NumberPanelViewFactory,
|
||||
private val behavior: ListHabitsBehavior
|
||||
) : FrameLayout(context),
|
||||
ModelObservable.Listener {
|
||||
|
||||
var buttonCount
|
||||
get() = checkmarkPanel.buttonCount
|
||||
set(value) {
|
||||
checkmarkPanel.buttonCount = value
|
||||
numberPanel.buttonCount = value
|
||||
}
|
||||
|
||||
var dataOffset = 0
|
||||
set(value) {
|
||||
field = value
|
||||
checkmarkPanel.dataOffset = value
|
||||
numberPanel.dataOffset = value
|
||||
}
|
||||
|
||||
var habit: Habit? = null
|
||||
set(newHabit) {
|
||||
if (isAttachedToWindow) {
|
||||
field?.observable?.removeListener(this)
|
||||
newHabit?.observable?.addListener(this)
|
||||
}
|
||||
field = newHabit
|
||||
if (newHabit != null) copyAttributesFrom(newHabit)
|
||||
}
|
||||
|
||||
var score
|
||||
get() = scoreRing.percentage.toDouble()
|
||||
set(value) {
|
||||
scoreRing.percentage = value.toFloat()
|
||||
scoreRing.precision = 1.0f / 16
|
||||
}
|
||||
|
||||
var unit
|
||||
get() = numberPanel.units
|
||||
set(value) {
|
||||
numberPanel.units = value
|
||||
}
|
||||
|
||||
var values
|
||||
get() = checkmarkPanel.values
|
||||
set(values) {
|
||||
checkmarkPanel.values = values
|
||||
numberPanel.values = values.map { it / 1000.0 }.toDoubleArray()
|
||||
}
|
||||
|
||||
var threshold: Double
|
||||
get() = numberPanel.threshold
|
||||
set(value) {
|
||||
numberPanel.threshold = value
|
||||
}
|
||||
|
||||
private var checkmarkPanel: CheckmarkPanelView
|
||||
private var numberPanel: NumberPanelView
|
||||
private var innerFrame: LinearLayout
|
||||
private var label: TextView
|
||||
private var scoreRing: RingView
|
||||
|
||||
init {
|
||||
scoreRing = RingView(context).apply {
|
||||
val thickness = dp(3f)
|
||||
val margin = dp(8f).toInt()
|
||||
val ringSize = dp(15f).toInt()
|
||||
layoutParams = LinearLayout.LayoutParams(ringSize, ringSize).apply {
|
||||
setMargins(margin, 0, margin, 0)
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
setThickness(thickness)
|
||||
}
|
||||
|
||||
label = TextView(context).apply {
|
||||
maxLines = 2
|
||||
ellipsize = TextUtils.TruncateAt.END
|
||||
layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)
|
||||
if (SDK_INT >= M) breakStrategy = Layout.BREAK_STRATEGY_BALANCED
|
||||
}
|
||||
|
||||
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
||||
onToggle = { timestamp, value ->
|
||||
triggerRipple(timestamp)
|
||||
habit?.let { behavior.onToggle(it, timestamp, value) }
|
||||
}
|
||||
}
|
||||
|
||||
numberPanel = numberPanelFactory.create().apply {
|
||||
visibility = GONE
|
||||
onEdit = { timestamp ->
|
||||
triggerRipple(timestamp)
|
||||
habit?.let { behavior.onEdit(it, timestamp) }
|
||||
}
|
||||
}
|
||||
|
||||
innerFrame = LinearLayout(context).apply {
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
if (SDK_INT >= LOLLIPOP) elevation = dp(1f)
|
||||
|
||||
addView(scoreRing)
|
||||
addView(label)
|
||||
addView(checkmarkPanel)
|
||||
addView(numberPanel)
|
||||
|
||||
setOnTouchListener { v, event ->
|
||||
if (SDK_INT >= LOLLIPOP)
|
||||
v.background.setHotspot(event.x, event.y)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
clipToPadding = false
|
||||
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
val margin = dp(3f).toInt()
|
||||
setPadding(margin, 0, margin, margin)
|
||||
addView(innerFrame)
|
||||
}
|
||||
|
||||
override fun onModelChange() {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
habit?.let { copyAttributesFrom(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun setSelected(isSelected: Boolean) {
|
||||
super.setSelected(isSelected)
|
||||
updateBackground(isSelected)
|
||||
}
|
||||
|
||||
fun triggerRipple(timestamp: Timestamp) {
|
||||
val today = DateUtils.getTodayWithOffset()
|
||||
val offset = timestamp.daysUntil(today) - dataOffset
|
||||
val button = checkmarkPanel.buttons[offset]
|
||||
val y = button.height / 2.0f
|
||||
val x = checkmarkPanel.x + button.x + (button.width / 2).toFloat()
|
||||
triggerRipple(x, y)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
habit?.observable?.addListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
habit?.observable?.removeListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
private fun copyAttributesFrom(h: Habit) {
|
||||
|
||||
fun getActiveColor(habit: Habit): Int {
|
||||
return when (habit.isArchived) {
|
||||
true -> sres.getColor(R.attr.mediumContrastTextColor)
|
||||
false -> habit.color.toThemedAndroidColor(context)
|
||||
}
|
||||
}
|
||||
|
||||
val c = getActiveColor(h)
|
||||
label.apply {
|
||||
text = h.name
|
||||
setTextColor(c)
|
||||
}
|
||||
scoreRing.apply {
|
||||
color = c
|
||||
}
|
||||
checkmarkPanel.apply {
|
||||
color = c
|
||||
visibility = when (h.isNumerical) {
|
||||
true -> View.GONE
|
||||
false -> View.VISIBLE
|
||||
}
|
||||
}
|
||||
numberPanel.apply {
|
||||
color = c
|
||||
units = h.unit
|
||||
threshold = h.targetValue
|
||||
visibility = when (h.isNumerical) {
|
||||
true -> View.VISIBLE
|
||||
false -> View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun triggerRipple(x: Float, y: Float) {
|
||||
val background = innerFrame.background
|
||||
if (SDK_INT >= LOLLIPOP) background.setHotspot(x, y)
|
||||
background.state = intArrayOf(
|
||||
android.R.attr.state_pressed,
|
||||
android.R.attr.state_enabled
|
||||
)
|
||||
Handler().postDelayed({ background.state = intArrayOf() }, 25)
|
||||
}
|
||||
|
||||
private fun updateBackground(isSelected: Boolean) {
|
||||
if (SDK_INT < LOLLIPOP) {
|
||||
val background = when (isSelected) {
|
||||
true -> sres.getDrawable(R.attr.selectedBackground)
|
||||
false -> sres.getDrawable(R.attr.cardBackground)
|
||||
}
|
||||
innerFrame.setBackgroundDrawable(background)
|
||||
return
|
||||
}
|
||||
|
||||
val background = when (isSelected) {
|
||||
true -> R.drawable.selected_box
|
||||
false -> R.drawable.ripple
|
||||
}
|
||||
innerFrame.setBackgroundResource(background)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class HabitCardViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Build.VERSION_CODES.LOLLIPOP
|
||||
import android.text.TextPaint
|
||||
import android.view.View.MeasureSpec.EXACTLY
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.common.views.ScrollableChart
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.utils.DateUtils.formatHeaderDate
|
||||
import org.isoron.uhabits.core.utils.DateUtils.getStartOfTodayCalendarWithOffset
|
||||
import org.isoron.uhabits.core.utils.MidnightTimer
|
||||
import org.isoron.uhabits.utils.dim
|
||||
import org.isoron.uhabits.utils.dp
|
||||
import org.isoron.uhabits.utils.isRTL
|
||||
import org.isoron.uhabits.utils.sres
|
||||
import org.isoron.uhabits.utils.toMeasureSpec
|
||||
import java.util.GregorianCalendar
|
||||
|
||||
class HeaderView(
|
||||
context: Context,
|
||||
val prefs: Preferences,
|
||||
val midnightTimer: MidnightTimer
|
||||
) : ScrollableChart(context),
|
||||
Preferences.Listener,
|
||||
MidnightTimer.MidnightListener {
|
||||
|
||||
private var drawer = Drawer()
|
||||
|
||||
var buttonCount: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
init {
|
||||
setScrollerBucketSize(dim(R.dimen.checkmarkWidth).toInt())
|
||||
setBackgroundColor(sres.getColor(R.attr.headerBackgroundColor))
|
||||
if (SDK_INT >= LOLLIPOP) elevation = dp(2.0f)
|
||||
}
|
||||
|
||||
override fun atMidnight() {
|
||||
post { invalidate() }
|
||||
}
|
||||
|
||||
override fun onCheckmarkSequenceChanged() {
|
||||
updateScrollDirection()
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
updateScrollDirection()
|
||||
prefs.addListener(this)
|
||||
midnightTimer.addListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
midnightTimer.removeListener(this)
|
||||
prefs.removeListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
drawer.draw(canvas)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val height = dim(R.dimen.checkmarkHeight)
|
||||
setMeasuredDimension(widthMeasureSpec, height.toMeasureSpec(EXACTLY))
|
||||
}
|
||||
|
||||
private fun updateScrollDirection() {
|
||||
var direction = -1
|
||||
if (prefs.isCheckmarkSequenceReversed) direction *= -1
|
||||
if (isRTL()) direction *= -1
|
||||
setScrollDirection(direction)
|
||||
}
|
||||
|
||||
private inner class Drawer {
|
||||
private val rect = RectF()
|
||||
private val paint = TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
isAntiAlias = true
|
||||
textSize = dim(R.dimen.tinyTextSize)
|
||||
textAlign = Paint.Align.CENTER
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
color = sres.getColor(R.attr.mediumContrastTextColor)
|
||||
}
|
||||
|
||||
fun draw(canvas: Canvas) {
|
||||
val day = getStartOfTodayCalendarWithOffset()
|
||||
val width = dim(R.dimen.checkmarkWidth)
|
||||
val height = dim(R.dimen.checkmarkHeight)
|
||||
val isReversed = prefs.isCheckmarkSequenceReversed
|
||||
|
||||
day.add(GregorianCalendar.DAY_OF_MONTH, -dataOffset)
|
||||
val em = paint.measureText("m")
|
||||
|
||||
repeat(buttonCount) { index ->
|
||||
rect.set(0f, 0f, width, height)
|
||||
rect.offset(canvas.width.toFloat(), 0f)
|
||||
|
||||
if (isReversed) rect.offset(-(index + 1) * width, 0f)
|
||||
else rect.offset((index - buttonCount) * width, 0f)
|
||||
|
||||
if (isRTL()) rect.set(
|
||||
canvas.width - rect.right,
|
||||
rect.top,
|
||||
canvas.width - rect.left,
|
||||
rect.bottom
|
||||
)
|
||||
|
||||
val y1 = rect.centerY() - 0.25 * em
|
||||
val y2 = rect.centerY() + 1.25 * em
|
||||
val lines = formatHeaderDate(day).toUpperCase().split("\n")
|
||||
canvas.drawText(lines[0], rect.centerX(), y1.toFloat(), paint)
|
||||
canvas.drawText(lines[1], rect.centerX(), y2.toFloat(), paint)
|
||||
day.add(GregorianCalendar.DAY_OF_MONTH, -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.content.Context
|
||||
import android.graphics.Color.WHITE
|
||||
import android.graphics.Typeface
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.HintList
|
||||
import org.isoron.uhabits.utils.dp
|
||||
|
||||
class HintView(
|
||||
context: Context,
|
||||
private val hintList: HintList
|
||||
) : LinearLayout(context) {
|
||||
|
||||
val hintContent: TextView
|
||||
|
||||
init {
|
||||
isClickable = true
|
||||
visibility = GONE
|
||||
orientation = VERTICAL
|
||||
val p1 = dp(16.0f).toInt()
|
||||
val p2 = dp(4.0f).toInt()
|
||||
setPadding(p1, p1, p2, p1)
|
||||
setBackgroundColor(resources.getColor(R.color.indigo_500))
|
||||
|
||||
val hintTitle = TextView(context).apply {
|
||||
setTextColor(WHITE)
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
text = resources.getString(R.string.hint_title)
|
||||
}
|
||||
|
||||
hintContent = TextView(context).apply {
|
||||
setTextColor(WHITE)
|
||||
setPadding(0, dp(5.0f).toInt(), 0, 0)
|
||||
}
|
||||
|
||||
addView(hintTitle, WRAP_CONTENT, WRAP_CONTENT)
|
||||
addView(hintContent, WRAP_CONTENT, WRAP_CONTENT)
|
||||
setOnClickListener { dismiss() }
|
||||
}
|
||||
|
||||
public override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
showNext()
|
||||
}
|
||||
|
||||
fun showNext() {
|
||||
if (!hintList.shouldShow()) return
|
||||
val hint = hintList.pop() ?: return
|
||||
|
||||
hintContent.text = hint
|
||||
requestLayout()
|
||||
|
||||
alpha = 0.0f
|
||||
visibility = View.VISIBLE
|
||||
animate().alpha(1f).duration = 500
|
||||
}
|
||||
|
||||
private fun dismiss() {
|
||||
animate().alpha(0f).setDuration(500).setListener(DismissAnimator())
|
||||
}
|
||||
|
||||
private inner class DismissAnimator : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: android.animation.Animator) {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Typeface
|
||||
import android.text.TextPaint
|
||||
import android.view.View
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.View.OnLongClickListener
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.utils.InterfaceUtils.getDimension
|
||||
import org.isoron.uhabits.utils.StyledResources
|
||||
import org.isoron.uhabits.utils.getFontAwesome
|
||||
import org.isoron.uhabits.utils.showMessage
|
||||
import java.text.DecimalFormat
|
||||
import javax.inject.Inject
|
||||
|
||||
private val BOLD_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.BOLD)
|
||||
private val NORMAL_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.NORMAL)
|
||||
|
||||
fun Double.toShortString(): String = when {
|
||||
this >= 1e9 -> String.format("%.1fG", this / 1e9)
|
||||
this >= 1e8 -> String.format("%.0fM", this / 1e6)
|
||||
this >= 1e7 -> String.format("%.1fM", this / 1e6)
|
||||
this >= 1e6 -> String.format("%.1fM", this / 1e6)
|
||||
this >= 1e5 -> String.format("%.0fk", this / 1e3)
|
||||
this >= 1e4 -> String.format("%.1fk", this / 1e3)
|
||||
this >= 1e3 -> String.format("%.1fk", this / 1e3)
|
||||
this >= 1e2 -> DecimalFormat("#").format(this)
|
||||
this >= 1e1 -> DecimalFormat("#.#").format(this)
|
||||
else -> DecimalFormat("#.##").format(this)
|
||||
}
|
||||
|
||||
class NumberButtonViewFactory
|
||||
@Inject constructor(
|
||||
@ActivityContext val context: Context,
|
||||
val preferences: Preferences
|
||||
) {
|
||||
fun create() = NumberButtonView(context, preferences)
|
||||
}
|
||||
|
||||
class NumberButtonView(
|
||||
@ActivityContext context: Context,
|
||||
val preferences: Preferences
|
||||
) : View(context),
|
||||
OnClickListener,
|
||||
OnLongClickListener {
|
||||
|
||||
var color = 0
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var value = 0.0
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var threshold = 0.0
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var units = ""
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var onEdit: () -> Unit = {}
|
||||
private var drawer: Drawer = Drawer(context)
|
||||
|
||||
init {
|
||||
setOnClickListener(this)
|
||||
setOnLongClickListener(this)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (preferences.isShortToggleEnabled) onEdit()
|
||||
else showMessage(resources.getString(R.string.long_press_to_edit))
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
onEdit()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
drawer.draw(canvas)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val width = getDimension(context, R.dimen.checkmarkWidth).toInt()
|
||||
val height = getDimension(context, R.dimen.checkmarkHeight).toInt()
|
||||
setMeasuredDimension(width, height)
|
||||
}
|
||||
|
||||
private inner class Drawer(context: Context) {
|
||||
|
||||
private val em: Float
|
||||
private val rect: RectF = RectF()
|
||||
private val sr = StyledResources(context)
|
||||
|
||||
private val lightGrey: Int
|
||||
private val darkGrey: Int
|
||||
|
||||
private val pRegular: TextPaint = TextPaint().apply {
|
||||
textSize = getDimension(context, R.dimen.smallerTextSize)
|
||||
typeface = NORMAL_TYPEFACE
|
||||
isAntiAlias = true
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
|
||||
private val pBold: TextPaint = TextPaint().apply {
|
||||
textSize = getDimension(context, R.dimen.smallTextSize)
|
||||
typeface = BOLD_TYPEFACE
|
||||
isAntiAlias = true
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
|
||||
init {
|
||||
em = pBold.measureText("m")
|
||||
lightGrey = sr.getColor(R.attr.lowContrastTextColor)
|
||||
darkGrey = sr.getColor(R.attr.mediumContrastTextColor)
|
||||
}
|
||||
|
||||
fun draw(canvas: Canvas) {
|
||||
val activeColor = when {
|
||||
value <= 0.0 -> lightGrey
|
||||
value < threshold -> darkGrey
|
||||
else -> color
|
||||
}
|
||||
|
||||
val label: String
|
||||
val typeface: Typeface
|
||||
|
||||
if (value >= 0) {
|
||||
label = value.toShortString()
|
||||
typeface = BOLD_TYPEFACE
|
||||
} else if (preferences.areQuestionMarksEnabled()) {
|
||||
label = resources.getString(R.string.fa_question)
|
||||
typeface = getFontAwesome()
|
||||
} else {
|
||||
label = "0"
|
||||
typeface = BOLD_TYPEFACE
|
||||
}
|
||||
|
||||
pBold.color = activeColor
|
||||
pBold.typeface = typeface
|
||||
pRegular.color = activeColor
|
||||
|
||||
rect.set(0f, 0f, width.toFloat(), height.toFloat())
|
||||
canvas.drawText(label, rect.centerX(), rect.centerY(), pBold)
|
||||
|
||||
rect.offset(0f, 1.2f * em)
|
||||
canvas.drawText(units, rect.centerX(), rect.centerY(), pRegular)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.Context
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class NumberPanelViewFactory
|
||||
@Inject constructor(
|
||||
@ActivityContext val context: Context,
|
||||
val preferences: Preferences,
|
||||
val buttonFactory: NumberButtonViewFactory
|
||||
) {
|
||||
fun create() = NumberPanelView(context, preferences, buttonFactory)
|
||||
}
|
||||
|
||||
class NumberPanelView(
|
||||
@ActivityContext context: Context,
|
||||
preferences: Preferences,
|
||||
private val buttonFactory: NumberButtonViewFactory
|
||||
) : ButtonPanelView<NumberButtonView>(context, preferences) {
|
||||
|
||||
var values = DoubleArray(0)
|
||||
set(values) {
|
||||
field = values
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var threshold = 0.0
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var color = 0
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var units = ""
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onEdit: (Timestamp) -> Unit = {}
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
override fun createButton() = buttonFactory.create()!!
|
||||
|
||||
@Synchronized
|
||||
override fun setupButtons() {
|
||||
val today = DateUtils.getTodayWithOffset()
|
||||
|
||||
buttons.forEachIndexed { index, button ->
|
||||
val timestamp = today.minus(index + dataOffset)
|
||||
button.value = when {
|
||||
index + dataOffset < values.size -> values[index + dataOffset]
|
||||
else -> 0.0
|
||||
}
|
||||
button.color = color
|
||||
button.threshold = threshold
|
||||
button.units = units
|
||||
button.onEdit = { onEdit(timestamp) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.View.MeasureSpec.EXACTLY
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.utils.dp
|
||||
import org.isoron.uhabits.utils.toMeasureSpec
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class ShadowView(context: Context) : View(context) {
|
||||
init {
|
||||
alpha = 0.2f
|
||||
background = resources.getDrawable(R.drawable.shadow)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(
|
||||
widthMeasureSpec,
|
||||
dp(2.0f).toInt().toMeasureSpec(EXACTLY)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities.habits.show
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.os.Bundle
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.isoron.uhabits.AndroidDirFinder
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||
import org.isoron.uhabits.activities.HabitsDirFinder
|
||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
|
||||
import org.isoron.uhabits.core.commands.Command
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
|
||||
import org.isoron.uhabits.core.ui.views.OnDateClickedListener
|
||||
import org.isoron.uhabits.intents.IntentFactory
|
||||
import org.isoron.uhabits.utils.showMessage
|
||||
import org.isoron.uhabits.utils.showSendFileScreen
|
||||
import org.isoron.uhabits.widgets.WidgetUpdater
|
||||
|
||||
class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
|
||||
private lateinit var commandRunner: CommandRunner
|
||||
private lateinit var menu: ShowHabitMenu
|
||||
private lateinit var view: ShowHabitView
|
||||
private lateinit var habit: Habit
|
||||
private lateinit var preferences: Preferences
|
||||
private lateinit var themeSwitcher: AndroidThemeSwitcher
|
||||
private lateinit var widgetUpdater: WidgetUpdater
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main)
|
||||
private lateinit var presenter: ShowHabitPresenter
|
||||
private val screen = Screen()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val appComponent = (applicationContext as HabitsApplication).component
|
||||
val habitList = appComponent.habitList
|
||||
habit = habitList.getById(ContentUris.parseId(intent.data!!))!!
|
||||
preferences = appComponent.preferences
|
||||
commandRunner = appComponent.commandRunner
|
||||
widgetUpdater = appComponent.widgetUpdater
|
||||
|
||||
themeSwitcher = AndroidThemeSwitcher(this, preferences)
|
||||
themeSwitcher.apply()
|
||||
|
||||
presenter = ShowHabitPresenter(
|
||||
commandRunner = commandRunner,
|
||||
habit = habit,
|
||||
habitList = habitList,
|
||||
preferences = preferences,
|
||||
screen = screen,
|
||||
)
|
||||
|
||||
view = ShowHabitView(this)
|
||||
|
||||
val menuPresenter = ShowHabitMenuPresenter(
|
||||
commandRunner = commandRunner,
|
||||
habit = habit,
|
||||
habitList = habitList,
|
||||
screen = screen,
|
||||
system = HabitsDirFinder(AndroidDirFinder(this)),
|
||||
taskRunner = appComponent.taskRunner,
|
||||
)
|
||||
|
||||
menu = ShowHabitMenu(
|
||||
activity = this,
|
||||
presenter = menuPresenter,
|
||||
preferences = preferences,
|
||||
)
|
||||
|
||||
view.setListener(presenter)
|
||||
setContentView(view)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(m: Menu): Boolean {
|
||||
return menu.onCreateOptionsMenu(m)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return menu.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
commandRunner.addListener(this)
|
||||
supportFragmentManager.findFragmentByTag("historyEditor")?.let {
|
||||
(it as HistoryEditorDialog).setOnDateClickedListener(presenter.historyCardPresenter)
|
||||
}
|
||||
screen.refresh()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
commandRunner.removeListener(this)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onCommandFinished(command: Command) {
|
||||
screen.refresh()
|
||||
}
|
||||
|
||||
inner class Screen : ShowHabitMenuPresenter.Screen, ShowHabitPresenter.Screen {
|
||||
override fun updateWidgets() {
|
||||
widgetUpdater.updateWidgets()
|
||||
}
|
||||
|
||||
override fun refresh() {
|
||||
scope.launch {
|
||||
view.setState(
|
||||
ShowHabitPresenter.buildState(
|
||||
habit = habit,
|
||||
preferences = preferences,
|
||||
theme = themeSwitcher.currentTheme,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showHistoryEditorDialog(listener: OnDateClickedListener) {
|
||||
val dialog = HistoryEditorDialog()
|
||||
dialog.arguments = Bundle().apply {
|
||||
putLong("habit", habit.id!!)
|
||||
}
|
||||
dialog.setOnDateClickedListener(listener)
|
||||
dialog.show(supportFragmentManager, "historyEditor")
|
||||
}
|
||||
|
||||
override fun showFeedback() {
|
||||
window.decorView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
}
|
||||
|
||||
override fun showNumberPicker(
|
||||
value: Double,
|
||||
unit: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback,
|
||||
) {
|
||||
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, callback).show()
|
||||
}
|
||||
|
||||
override fun showEditHabitScreen(habit: Habit) {
|
||||
startActivity(IntentFactory().startEditActivity(this@ShowHabitActivity, habit))
|
||||
}
|
||||
|
||||
override fun showMessage(m: ShowHabitMenuPresenter.Message?) {
|
||||
when (m) {
|
||||
ShowHabitMenuPresenter.Message.COULD_NOT_EXPORT -> {
|
||||
showMessage(resources.getString(R.string.could_not_export))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun showSendFileScreen(filename: String) {
|
||||
this@ShowHabitActivity.showSendFileScreen(filename)
|
||||
}
|
||||
|
||||
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback) {
|
||||
ConfirmDeleteDialog(this@ShowHabitActivity, callback, 1).show()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
this@ShowHabitActivity.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show
|
||||
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuPresenter
|
||||
|
||||
class ShowHabitMenu(
|
||||
val activity: ShowHabitActivity,
|
||||
val presenter: ShowHabitMenuPresenter,
|
||||
val preferences: Preferences,
|
||||
) {
|
||||
fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
activity.menuInflater.inflate(R.menu.show_habit, menu)
|
||||
if (preferences.isDeveloper) {
|
||||
menu.findItem(R.id.action_randomize).isVisible = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_edit_habit -> {
|
||||
presenter.onEditHabit()
|
||||
return true
|
||||
}
|
||||
R.id.action_delete -> {
|
||||
presenter.onDeleteHabit()
|
||||
return true
|
||||
}
|
||||
R.id.action_randomize -> {
|
||||
presenter.onRandomize()
|
||||
return true
|
||||
}
|
||||
R.id.export -> {
|
||||
presenter.onExportCSV()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitState
|
||||
import org.isoron.uhabits.databinding.ShowHabitBinding
|
||||
import org.isoron.uhabits.utils.setupToolbar
|
||||
|
||||
class ShowHabitView(context: Context) : FrameLayout(context) {
|
||||
private val binding = ShowHabitBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
init {
|
||||
addView(binding.root)
|
||||
}
|
||||
|
||||
fun setState(data: ShowHabitState) {
|
||||
setupToolbar(binding.toolbar, title = data.title, color = data.color)
|
||||
binding.subtitleCard.setState(data.subtitle)
|
||||
binding.overviewCard.setState(data.overview)
|
||||
binding.notesCard.setState(data.notes)
|
||||
binding.targetCard.setState(data.target)
|
||||
binding.streakCard.setState(data.streaks)
|
||||
binding.scoreCard.setState(data.scores)
|
||||
binding.frequencyCard.setState(data.frequency)
|
||||
binding.historyCard.setState(data.history)
|
||||
binding.barCard.setState(data.bar)
|
||||
if (data.isNumerical) {
|
||||
binding.overviewCard.visibility = GONE
|
||||
binding.streakCard.visibility = GONE
|
||||
} else {
|
||||
binding.targetCard.visibility = GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun setListener(presenter: ShowHabitPresenter) {
|
||||
binding.scoreCard.setListener(presenter.scoreCardPresenter)
|
||||
binding.historyCard.setListener(presenter.historyCardPresenter)
|
||||
binding.barCard.setListener(presenter.barCardPresenter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities.habits.show.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.LinearLayout
|
||||
import org.isoron.platform.time.JavaLocalDateFormatter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardState
|
||||
import org.isoron.uhabits.core.ui.views.BarChart
|
||||
import org.isoron.uhabits.databinding.ShowHabitBarBinding
|
||||
import org.isoron.uhabits.utils.toThemedAndroidColor
|
||||
import java.util.Locale
|
||||
|
||||
class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
|
||||
|
||||
private var binding = ShowHabitBarBinding.inflate(LayoutInflater.from(context), this)
|
||||
|
||||
fun setState(state: BarCardState) {
|
||||
val androidColor = state.color.toThemedAndroidColor(context)
|
||||
binding.chart.view = BarChart(state.theme, JavaLocalDateFormatter(Locale.US)).apply {
|
||||
series = mutableListOf(state.entries.map { it.value / 1000.0 })
|
||||
colors = mutableListOf(theme.color(state.color.paletteIndex))
|
||||
axis = state.entries.map { it.timestamp.toLocalDate() }
|
||||
}
|
||||
binding.chart.resetDataOffset()
|
||||
binding.chart.postInvalidate()
|
||||
|
||||
binding.title.setTextColor(androidColor)
|
||||
if (state.isNumerical) {
|
||||
binding.boolSpinner.visibility = GONE
|
||||
} else {
|
||||
binding.numericalSpinner.visibility = GONE
|
||||
}
|
||||
|
||||
binding.numericalSpinner.onItemSelectedListener = null
|
||||
binding.numericalSpinner.setSelection(state.numericalSpinnerPosition)
|
||||
binding.boolSpinner.setSelection(state.boolSpinnerPosition)
|
||||
}
|
||||
|
||||
fun setListener(presenter: BarCardPresenter) {
|
||||
binding.boolSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long,
|
||||
) {
|
||||
presenter.onBoolSpinnerPosition(position)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
}
|
||||
}
|
||||
binding.numericalSpinner.onItemSelectedListener =
|
||||
object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long,
|
||||
) {
|
||||
presenter.onNumericalSpinnerPosition(position)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities.habits.show.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardState
|
||||
import org.isoron.uhabits.databinding.ShowHabitFrequencyBinding
|
||||
import org.isoron.uhabits.utils.toThemedAndroidColor
|
||||
|
||||
class FrequencyCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
|
||||
|
||||
private var binding = ShowHabitFrequencyBinding.inflate(LayoutInflater.from(context), this)
|
||||
|
||||
fun setState(state: FrequencyCardState) {
|
||||
val androidColor = state.color.toThemedAndroidColor(context)
|
||||
binding.frequencyChart.setFrequency(state.frequency)
|
||||
binding.frequencyChart.setFirstWeekday(state.firstWeekday)
|
||||
binding.title.setTextColor(androidColor)
|
||||
binding.frequencyChart.setColor(androidColor)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities.habits.show.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import org.isoron.platform.time.JavaLocalDateFormatter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardState
|
||||
import org.isoron.uhabits.core.ui.views.HistoryChart
|
||||
import org.isoron.uhabits.databinding.ShowHabitHistoryBinding
|
||||
import org.isoron.uhabits.utils.toThemedAndroidColor
|
||||
import java.util.Locale
|
||||
|
||||
class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
|
||||
|
||||
private var binding = ShowHabitHistoryBinding.inflate(LayoutInflater.from(context), this)
|
||||
|
||||
fun setState(state: HistoryCardState) {
|
||||
val androidColor = state.color.toThemedAndroidColor(context)
|
||||
binding.title.setTextColor(androidColor)
|
||||
binding.chart.view = HistoryChart(
|
||||
today = state.today,
|
||||
paletteColor = state.color,
|
||||
theme = state.theme,
|
||||
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
|
||||
series = state.series,
|
||||
firstWeekday = state.firstWeekday,
|
||||
)
|
||||
binding.chart.postInvalidate()
|
||||
}
|
||||
|
||||
fun setListener(presenter: HistoryCardPresenter) {
|
||||
binding.edit.setOnClickListener { presenter.onClickEditButton() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardState
|
||||
import org.isoron.uhabits.databinding.ShowHabitNotesBinding
|
||||
|
||||
class NotesCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
|
||||
private val binding = ShowHabitNotesBinding.inflate(LayoutInflater.from(context), this)
|
||||
fun setState(state: NotesCardState) {
|
||||
if (state.description.isEmpty()) {
|
||||
visibility = GONE
|
||||
} else {
|
||||
visibility = VISIBLE
|
||||
binding.habitNotes.text = state.description
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities.habits.show.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardState
|
||||
import org.isoron.uhabits.databinding.ShowHabitOverviewBinding
|
||||
import org.isoron.uhabits.utils.StyledResources
|
||||
import org.isoron.uhabits.utils.toThemedAndroidColor
|
||||
|
||||
class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
|
||||
|
||||
private val binding = ShowHabitOverviewBinding.inflate(LayoutInflater.from(context), this)
|
||||
|
||||
private fun formatPercentageDiff(percentageDiff: Float): String {
|
||||
return String.format(
|
||||
"%s%.0f%%",
|
||||
if (percentageDiff >= 0) "+" else "\u2212",
|
||||
Math.abs(percentageDiff) * 100
|
||||
)
|
||||
}
|
||||
|
||||
fun setState(state: OverviewCardState) {
|
||||
val androidColor = state.color.toThemedAndroidColor(context)
|
||||
val res = StyledResources(context)
|
||||
val inactiveColor = res.getColor(R.attr.mediumContrastTextColor)
|
||||
binding.monthDiffLabel.setTextColor(if (state.scoreMonthDiff >= 0) androidColor else inactiveColor)
|
||||
binding.monthDiffLabel.text = formatPercentageDiff(state.scoreMonthDiff)
|
||||
binding.scoreLabel.setTextColor(androidColor)
|
||||
binding.scoreLabel.text = String.format("%.0f%%", state.scoreToday * 100)
|
||||
binding.scoreRing.color = androidColor
|
||||
binding.scoreRing.percentage = state.scoreToday
|
||||
binding.title.setTextColor(androidColor)
|
||||
binding.totalCountLabel.setTextColor(androidColor)
|
||||
binding.totalCountLabel.text = state.totalCount.toString()
|
||||
binding.yearDiffLabel.setTextColor(if (state.scoreYearDiff >= 0) androidColor else inactiveColor)
|
||||
binding.yearDiffLabel.text = formatPercentageDiff(state.scoreYearDiff)
|
||||
postInvalidate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities.habits.show.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.LinearLayout
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardState
|
||||
import org.isoron.uhabits.databinding.ShowHabitScoreBinding
|
||||
import org.isoron.uhabits.utils.toThemedAndroidColor
|
||||
|
||||
class ScoreCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
|
||||
private var binding = ShowHabitScoreBinding.inflate(LayoutInflater.from(context), this)
|
||||
|
||||
fun setState(state: ScoreCardState) {
|
||||
val androidColor = state.color.toThemedAndroidColor(context)
|
||||
binding.title.setTextColor(androidColor)
|
||||
binding.spinner.setSelection(state.spinnerPosition)
|
||||
binding.scoreView.setScores(state.scores)
|
||||
binding.scoreView.reset()
|
||||
binding.scoreView.setBucketSize(state.bucketSize)
|
||||
binding.scoreView.setColor(androidColor)
|
||||
}
|
||||
|
||||
fun setListener(presenter: ScoreCardPresenter) {
|
||||
binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
presenter.onSpinnerPosition(position)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities.habits.show.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCardState
|
||||
import org.isoron.uhabits.databinding.ShowHabitStreakBinding
|
||||
import org.isoron.uhabits.utils.toThemedAndroidColor
|
||||
|
||||
class StreakCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
|
||||
private val binding = ShowHabitStreakBinding.inflate(LayoutInflater.from(context), this)
|
||||
fun setState(state: StreakCardState) {
|
||||
val color = state.color.toThemedAndroidColor(context)
|
||||
binding.title.setTextColor(color)
|
||||
binding.streakChart.setColor(color)
|
||||
binding.streakChart.setStreaks(state.bestStreaks)
|
||||
postInvalidate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities.habits.show.views
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.habits.list.views.toShortString
|
||||
import org.isoron.uhabits.core.models.Frequency
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState
|
||||
import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding
|
||||
import org.isoron.uhabits.utils.InterfaceUtils
|
||||
import org.isoron.uhabits.utils.formatTime
|
||||
import org.isoron.uhabits.utils.toThemedAndroidColor
|
||||
import java.util.Locale
|
||||
|
||||
class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
|
||||
|
||||
private val binding = ShowHabitSubtitleBinding.inflate(LayoutInflater.from(context), this)
|
||||
|
||||
init {
|
||||
val fontAwesome = InterfaceUtils.getFontAwesome(context)
|
||||
binding.targetIcon.typeface = fontAwesome
|
||||
binding.frequencyIcon.typeface = fontAwesome
|
||||
binding.reminderIcon.typeface = fontAwesome
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun setState(state: SubtitleCardState) {
|
||||
val color = state.color.toThemedAndroidColor(context)
|
||||
val reminder = state.reminder
|
||||
binding.frequencyLabel.text = state.frequency.format(resources)
|
||||
binding.questionLabel.setTextColor(color)
|
||||
binding.questionLabel.text = state.question
|
||||
binding.reminderLabel.text = if (reminder != null) {
|
||||
formatTime(context, reminder.hour, reminder.minute)
|
||||
} else {
|
||||
resources.getString(R.string.reminder_off)
|
||||
}
|
||||
binding.targetText.text = "${state.targetValue.toShortString()} ${state.unit}"
|
||||
|
||||
binding.questionLabel.visibility = View.VISIBLE
|
||||
binding.targetIcon.visibility = View.VISIBLE
|
||||
binding.targetText.visibility = View.VISIBLE
|
||||
if (!state.isNumerical) {
|
||||
binding.targetIcon.visibility = View.GONE
|
||||
binding.targetText.visibility = View.GONE
|
||||
}
|
||||
if (state.question.isEmpty()) {
|
||||
binding.questionLabel.visibility = View.GONE
|
||||
}
|
||||
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
private fun Frequency.format(resources: Resources): String {
|
||||
val num = this.numerator
|
||||
val den = this.denominator
|
||||
if (num == den) {
|
||||
return resources.getString(R.string.every_day)
|
||||
}
|
||||
if (den == 7) {
|
||||
return resources.getString(R.string.x_times_per_week, num)
|
||||
}
|
||||
if (den == 30 || den == 31) {
|
||||
return resources.getString(R.string.x_times_per_month, num)
|
||||
}
|
||||
if (num == 1) {
|
||||
if (den == 7) {
|
||||
return resources.getString(R.string.every_week)
|
||||
}
|
||||
if (den % 7 == 0) {
|
||||
return resources.getString(R.string.every_x_weeks, den / 7)
|
||||
}
|
||||
if (den == 30 || den == 31) {
|
||||
return resources.getString(R.string.every_month)
|
||||
}
|
||||
return resources.getString(R.string.every_x_days, den)
|
||||
}
|
||||
return String.format(
|
||||
Locale.US,
|
||||
"%d %s %d %s",
|
||||
num,
|
||||
resources.getString(R.string.times_every),
|
||||
den,
|
||||
resources.getString(R.string.days),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities.habits.show.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardState
|
||||
import org.isoron.uhabits.databinding.ShowHabitTargetBinding
|
||||
import org.isoron.uhabits.utils.toThemedAndroidColor
|
||||
|
||||
class TargetCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
|
||||
private val binding = ShowHabitTargetBinding.inflate(LayoutInflater.from(context), this)
|
||||
fun setState(state: TargetCardState) {
|
||||
val androidColor = state.color.toThemedAndroidColor(context)
|
||||
binding.targetChart.setValues(state.values)
|
||||
binding.targetChart.setTargets(state.targets)
|
||||
binding.targetChart.setLabels(state.intervals.map { intervalToLabel(resources, it) })
|
||||
binding.title.setTextColor(androidColor)
|
||||
binding.targetChart.setColor(androidColor)
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun intervalToLabel(resources: Resources, interval: Int) = when (interval) {
|
||||
1 -> resources.getString(R.string.today)
|
||||
7 -> resources.getString(R.string.week)
|
||||
30 -> resources.getString(R.string.month)
|
||||
91 -> resources.getString(R.string.quarter)
|
||||
else -> resources.getString(R.string.year)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.intro
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import com.github.paolorotolo.appintro.AppIntro2
|
||||
import com.github.paolorotolo.appintro.AppIntroFragment
|
||||
import org.isoron.uhabits.R
|
||||
|
||||
/**
|
||||
* Activity that introduces the app to the user, shown only after the app is
|
||||
* launched for the first time.
|
||||
*/
|
||||
class IntroActivity : AppIntro2() {
|
||||
override fun init(savedInstanceState: Bundle?) {
|
||||
showStatusBar(false)
|
||||
|
||||
addSlide(
|
||||
AppIntroFragment.newInstance(
|
||||
getString(R.string.intro_title_1),
|
||||
getString(R.string.intro_description_1),
|
||||
R.drawable.intro_icon_1,
|
||||
Color.parseColor("#194673")
|
||||
)
|
||||
)
|
||||
|
||||
addSlide(
|
||||
AppIntroFragment.newInstance(
|
||||
getString(R.string.intro_title_2),
|
||||
getString(R.string.intro_description_2),
|
||||
R.drawable.intro_icon_2,
|
||||
Color.parseColor("#ffa726")
|
||||
)
|
||||
)
|
||||
|
||||
addSlide(
|
||||
AppIntroFragment.newInstance(
|
||||
getString(R.string.intro_title_4),
|
||||
getString(R.string.intro_description_4),
|
||||
R.drawable.intro_icon_4,
|
||||
Color.parseColor("#9575cd")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNextPressed() {}
|
||||
|
||||
override fun onDonePressed() {
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onSlideChanged() {}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user