Android Studio file structure
93
app/app.iml
Normal file
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="uHabits" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="android-gradle" name="Android-Gradle">
|
||||
<configuration>
|
||||
<option name="GRADLE_PROJECT_PATH" value=":app" />
|
||||
</configuration>
|
||||
</facet>
|
||||
<facet type="android" name="Android">
|
||||
<configuration>
|
||||
<option name="SELECTED_BUILD_VARIANT" value="debug" />
|
||||
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
|
||||
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
|
||||
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
|
||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugAndroidTest" />
|
||||
<option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" />
|
||||
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateDebugAndroidTestSources" />
|
||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
||||
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
|
||||
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
||||
<output url="file://$MODULE_DIR$/build/intermediates/classes/debug" />
|
||||
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/debug" />
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/debug" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/androidTest/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Android API 22 Platform" jdkType="Android SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" exported="" name="support-v4-22.0.0" level="project" />
|
||||
<orderEntry type="library" exported="" name="ActiveAndroid" level="project" />
|
||||
<orderEntry type="library" exported="" name="support-annotations-22.0.0" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
|
||||
24
app/build.gradle
Normal file
@@ -0,0 +1,24 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 22
|
||||
buildToolsVersion "21.1.2"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.isoron.uhabits"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 22
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:support-v4:22.0.0'
|
||||
compile files('libs/ActiveAndroid.jar')
|
||||
}
|
||||
BIN
app/libs/ActiveAndroid.jar
Normal file
3
app/lint.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<lint>
|
||||
</lint>
|
||||
50
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.isoron.uhabits"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="21"
|
||||
android:targetSdkVersion="22" />
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<application
|
||||
android:name="com.activeandroid.app.Application"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme" >
|
||||
<meta-data
|
||||
android:name="AA_DB_NAME"
|
||||
android:value="uhabits.db" />
|
||||
<meta-data
|
||||
android:name="AA_DB_VERSION"
|
||||
android:value="6" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleInstance" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver android:name=".ReminderAlarmReceiver" >
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".ShowHabitActivity"
|
||||
android:label="@string/title_activity_show_habit"
|
||||
android:parentActivityName=".MainActivity" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.isoron.uhabits.MainActivity" />
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/assets/fontawesome-webfont.ttf
Normal file
2
app/src/main/assets/migrations/5.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
alter table habits add column reminder_hour integer;
|
||||
alter table habits add column reminder_min integer;
|
||||
1
app/src/main/assets/migrations/6.sql
Normal file
@@ -0,0 +1 @@
|
||||
alter table habits add column highlight integer not null default 0;
|
||||
203
app/src/main/java/com/android/colorpicker/ColorPickerDialog.java
Normal file
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* 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 org.isoron.uhabits.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.DialogFragment;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import com.android.colorpicker.ColorPickerSwatch.OnColorSelectedListener;
|
||||
|
||||
/**
|
||||
* 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 DialogFragment 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 org.isoron.uhabits.R;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 org.isoron.uhabits.R;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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,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.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
140
app/src/main/java/com/android/datetimepicker/Utils.java
Normal file
@@ -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,480 @@
|
||||
/*
|
||||
* 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.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 Button mDoneButton;
|
||||
private Button mClearButton;
|
||||
|
||||
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);
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
689
app/src/main/java/com/android/datetimepicker/date/MonthView.java
Normal file
@@ -0,0 +1,689 @@
|
||||
/*
|
||||
* 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.security.InvalidParameterException;
|
||||
import java.util.Calendar;
|
||||
import java.util.Formatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
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.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.support.v4.widget.ExploreByTouchHelper;
|
||||
import android.text.format.DateFormat;
|
||||
import android.text.format.DateUtils;
|
||||
import android.text.format.Time;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
|
||||
import com.android.datetimepicker.Utils;
|
||||
import com.android.datetimepicker.date.MonthAdapter.CalendarDay;
|
||||
|
||||
/**
|
||||
* 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 org.isoron.uhabits.R;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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,212 @@
|
||||
/*
|
||||
* 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 java.text.DateFormatSymbols;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
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 com.android.datetimepicker.Utils;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
private int mAmPmTextColor;
|
||||
private int mSelectedColor;
|
||||
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,122 @@
|
||||
/*
|
||||
* 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 org.isoron.uhabits.R;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 org.isoron.uhabits.R;
|
||||
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.format.DateUtils;
|
||||
import android.text.format.Time;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.View.OnTouchListener;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.android.datetimepicker.HapticFeedbackController;
|
||||
|
||||
/**
|
||||
* 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 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,399 @@
|
||||
/*
|
||||
* 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 org.isoron.uhabits.R;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
private final Paint mPaint = new Paint();
|
||||
|
||||
private boolean mIsInitialized;
|
||||
private boolean mDrawValuesReady;
|
||||
|
||||
private float mCircleRadiusMultiplier;
|
||||
private float mAmPmCircleRadiusMultiplier;
|
||||
private float mInnerNumbersRadiusMultiplier;
|
||||
private float mOuterNumbersRadiusMultiplier;
|
||||
private float mNumbersRadiusMultiplier;
|
||||
private float mSelectionRadiusMultiplier;
|
||||
private float mAnimationRadiusMultiplier;
|
||||
private boolean mIs24HourMode;
|
||||
private boolean mHasInnerCircle;
|
||||
private int 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();
|
||||
|
||||
int blue = res.getColor(R.color.blue);
|
||||
mPaint.setColor(blue);
|
||||
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();
|
||||
int color;
|
||||
if (themeDark) {
|
||||
color = res.getColor(R.color.red);
|
||||
mSelectionAlpha = SELECTED_ALPHA_THEME_DARK;
|
||||
} else {
|
||||
color = res.getColor(R.color.blue);
|
||||
mSelectionAlpha = SELECTED_ALPHA;
|
||||
}
|
||||
mPaint.setColor(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 org.isoron.uhabits.R;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
package com.mobeta.android.dslv;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.widget.AdapterView;
|
||||
|
||||
/**
|
||||
* Class that starts and stops item drags on a {@link DragSortListView}
|
||||
* based on touch gestures. This class also inherits from
|
||||
* {@link SimpleFloatViewManager}, which provides basic float View
|
||||
* creation.
|
||||
*
|
||||
* An instance of this class is meant to be passed to the methods
|
||||
* {@link DragSortListView#setTouchListener()} and
|
||||
* {@link DragSortListView#setFloatViewManager()} of your
|
||||
* {@link DragSortListView} instance.
|
||||
*/
|
||||
public class DragSortController extends SimpleFloatViewManager implements View.OnTouchListener, GestureDetector.OnGestureListener {
|
||||
|
||||
/**
|
||||
* Drag init mode enum.
|
||||
*/
|
||||
public static final int ON_DOWN = 0;
|
||||
public static final int ON_DRAG = 1;
|
||||
public static final int ON_LONG_PRESS = 2;
|
||||
|
||||
private int mDragInitMode = ON_DOWN;
|
||||
|
||||
private boolean mSortEnabled = true;
|
||||
|
||||
/**
|
||||
* Remove mode enum.
|
||||
*/
|
||||
public static final int CLICK_REMOVE = 0;
|
||||
public static final int FLING_REMOVE = 1;
|
||||
|
||||
/**
|
||||
* The current remove mode.
|
||||
*/
|
||||
private int mRemoveMode;
|
||||
|
||||
private boolean mRemoveEnabled = false;
|
||||
private boolean mIsRemoving = false;
|
||||
|
||||
private GestureDetector mDetector;
|
||||
|
||||
private GestureDetector mFlingRemoveDetector;
|
||||
|
||||
private int mTouchSlop;
|
||||
|
||||
public static final int MISS = -1;
|
||||
|
||||
private int mHitPos = MISS;
|
||||
private int mFlingHitPos = MISS;
|
||||
|
||||
private int mClickRemoveHitPos = MISS;
|
||||
|
||||
private int[] mTempLoc = new int[2];
|
||||
|
||||
private int mItemX;
|
||||
private int mItemY;
|
||||
|
||||
private int mCurrX;
|
||||
private int mCurrY;
|
||||
|
||||
private boolean mDragging = false;
|
||||
|
||||
private float mFlingSpeed = 500f;
|
||||
|
||||
private int mDragHandleId;
|
||||
|
||||
private int mClickRemoveId;
|
||||
|
||||
private int mFlingHandleId;
|
||||
private boolean mCanDrag;
|
||||
|
||||
private DragSortListView mDslv;
|
||||
private int mPositionX;
|
||||
|
||||
/**
|
||||
* Calls {@link #DragSortController(DragSortListView, int)} with a
|
||||
* 0 drag handle id, FLING_RIGHT_REMOVE remove mode,
|
||||
* and ON_DOWN drag init. By default, sorting is enabled, and
|
||||
* removal is disabled.
|
||||
*
|
||||
* @param dslv The DSLV instance
|
||||
*/
|
||||
public DragSortController(DragSortListView dslv) {
|
||||
this(dslv, 0, ON_DOWN, FLING_REMOVE);
|
||||
}
|
||||
|
||||
public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode) {
|
||||
this(dslv, dragHandleId, dragInitMode, removeMode, 0);
|
||||
}
|
||||
|
||||
public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode, int clickRemoveId) {
|
||||
this(dslv, dragHandleId, dragInitMode, removeMode, clickRemoveId, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, sorting is enabled, and removal is disabled.
|
||||
*
|
||||
* @param dslv The DSLV instance
|
||||
* @param dragHandleId The resource id of the View that represents
|
||||
* the drag handle in a list item.
|
||||
*/
|
||||
public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode,
|
||||
int removeMode, int clickRemoveId, int flingHandleId) {
|
||||
super(dslv);
|
||||
mDslv = dslv;
|
||||
mDetector = new GestureDetector(dslv.getContext(), this);
|
||||
mFlingRemoveDetector = new GestureDetector(dslv.getContext(), mFlingRemoveListener);
|
||||
mFlingRemoveDetector.setIsLongpressEnabled(false);
|
||||
mTouchSlop = ViewConfiguration.get(dslv.getContext()).getScaledTouchSlop();
|
||||
mDragHandleId = dragHandleId;
|
||||
mClickRemoveId = clickRemoveId;
|
||||
mFlingHandleId = flingHandleId;
|
||||
setRemoveMode(removeMode);
|
||||
setDragInitMode(dragInitMode);
|
||||
}
|
||||
|
||||
|
||||
public int getDragInitMode() {
|
||||
return mDragInitMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set how a drag is initiated. Needs to be one of
|
||||
* {@link ON_DOWN}, {@link ON_DRAG}, or {@link ON_LONG_PRESS}.
|
||||
*
|
||||
* @param mode The drag init mode.
|
||||
*/
|
||||
public void setDragInitMode(int mode) {
|
||||
mDragInitMode = mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable list item sorting. Disabling is useful if only item
|
||||
* removal is desired. Prevents drags in the vertical direction.
|
||||
*
|
||||
* @param enabled Set <code>true</code> to enable list
|
||||
* item sorting.
|
||||
*/
|
||||
public void setSortEnabled(boolean enabled) {
|
||||
mSortEnabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isSortEnabled() {
|
||||
return mSortEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* One of {@link CLICK_REMOVE}, {@link FLING_RIGHT_REMOVE},
|
||||
* {@link FLING_LEFT_REMOVE},
|
||||
* {@link SLIDE_RIGHT_REMOVE}, or {@link SLIDE_LEFT_REMOVE}.
|
||||
*/
|
||||
public void setRemoveMode(int mode) {
|
||||
mRemoveMode = mode;
|
||||
}
|
||||
|
||||
public int getRemoveMode() {
|
||||
return mRemoveMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable item removal without affecting remove mode.
|
||||
*/
|
||||
public void setRemoveEnabled(boolean enabled) {
|
||||
mRemoveEnabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isRemoveEnabled() {
|
||||
return mRemoveEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the resource id for the View that represents the drag
|
||||
* handle in a list item.
|
||||
*
|
||||
* @param id An android resource id.
|
||||
*/
|
||||
public void setDragHandleId(int id) {
|
||||
mDragHandleId = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the resource id for the View that represents the fling
|
||||
* handle in a list item.
|
||||
*
|
||||
* @param id An android resource id.
|
||||
*/
|
||||
public void setFlingHandleId(int id) {
|
||||
mFlingHandleId = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the resource id for the View that represents click
|
||||
* removal button.
|
||||
*
|
||||
* @param id An android resource id.
|
||||
*/
|
||||
public void setClickRemoveId(int id) {
|
||||
mClickRemoveId = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets flags to restrict certain motions of the floating View
|
||||
* based on DragSortController settings (such as remove mode).
|
||||
* Starts the drag on the DragSortListView.
|
||||
*
|
||||
* @param position The list item position (includes headers).
|
||||
* @param deltaX Touch x-coord minus left edge of floating View.
|
||||
* @param deltaY Touch y-coord minus top edge of floating View.
|
||||
*
|
||||
* @return True if drag started, false otherwise.
|
||||
*/
|
||||
public boolean startDrag(int position, int deltaX, int deltaY) {
|
||||
|
||||
int dragFlags = 0;
|
||||
if (mSortEnabled && !mIsRemoving) {
|
||||
dragFlags |= DragSortListView.DRAG_POS_Y | DragSortListView.DRAG_NEG_Y;
|
||||
}
|
||||
if (mRemoveEnabled && mIsRemoving) {
|
||||
dragFlags |= DragSortListView.DRAG_POS_X;
|
||||
dragFlags |= DragSortListView.DRAG_NEG_X;
|
||||
}
|
||||
|
||||
mDragging = mDslv.startDrag(position - mDslv.getHeaderViewsCount(), dragFlags, deltaX,
|
||||
deltaY);
|
||||
return mDragging;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent ev) {
|
||||
if (!mDslv.isDragEnabled() || mDslv.listViewIntercepted()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mDetector.onTouchEvent(ev);
|
||||
if (mRemoveEnabled && mDragging && mRemoveMode == FLING_REMOVE) {
|
||||
mFlingRemoveDetector.onTouchEvent(ev);
|
||||
}
|
||||
|
||||
int action = ev.getAction() & MotionEvent.ACTION_MASK;
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
mCurrX = (int) ev.getX();
|
||||
mCurrY = (int) ev.getY();
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (mRemoveEnabled && mIsRemoving) {
|
||||
int x = mPositionX >= 0 ? mPositionX : -mPositionX;
|
||||
int removePoint = mDslv.getWidth() / 2;
|
||||
if (x > removePoint) {
|
||||
mDslv.stopDragWithVelocity(true, 0);
|
||||
}
|
||||
}
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
mIsRemoving = false;
|
||||
mDragging = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides to provide fading when slide removal is enabled.
|
||||
*/
|
||||
@Override
|
||||
public void onDragFloatView(View floatView, Point position, Point touch) {
|
||||
|
||||
if (mRemoveEnabled && mIsRemoving) {
|
||||
mPositionX = position.x;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the position to start dragging based on the ACTION_DOWN
|
||||
* MotionEvent. This function simply calls
|
||||
* {@link #dragHandleHitPosition(MotionEvent)}. Override
|
||||
* to change drag handle behavior;
|
||||
* this function is called internally when an ACTION_DOWN
|
||||
* event is detected.
|
||||
*
|
||||
* @param ev The ACTION_DOWN MotionEvent.
|
||||
*
|
||||
* @return The list position to drag if a drag-init gesture is
|
||||
* detected; MISS if unsuccessful.
|
||||
*/
|
||||
public int startDragPosition(MotionEvent ev) {
|
||||
return dragHandleHitPosition(ev);
|
||||
}
|
||||
|
||||
public int startFlingPosition(MotionEvent ev) {
|
||||
return mRemoveMode == FLING_REMOVE ? flingHandleHitPosition(ev) : MISS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for the touch of an item's drag handle (specified by
|
||||
* {@link #setDragHandleId(int)}), and returns that item's position
|
||||
* if a drag handle touch was detected.
|
||||
*
|
||||
* @param ev The ACTION_DOWN MotionEvent.
|
||||
|
||||
* @return The list position of the item whose drag handle was
|
||||
* touched; MISS if unsuccessful.
|
||||
*/
|
||||
public int dragHandleHitPosition(MotionEvent ev) {
|
||||
return viewIdHitPosition(ev, mDragHandleId);
|
||||
}
|
||||
|
||||
public int flingHandleHitPosition(MotionEvent ev) {
|
||||
return viewIdHitPosition(ev, mFlingHandleId);
|
||||
}
|
||||
|
||||
public int viewIdHitPosition(MotionEvent ev, int id) {
|
||||
final int x = (int) ev.getX();
|
||||
final int y = (int) ev.getY();
|
||||
|
||||
int touchPos = mDslv.pointToPosition(x, y); // includes headers/footers
|
||||
|
||||
final int numHeaders = mDslv.getHeaderViewsCount();
|
||||
final int numFooters = mDslv.getFooterViewsCount();
|
||||
final int count = mDslv.getCount();
|
||||
|
||||
// Log.d("mobeta", "touch down on position " + itemnum);
|
||||
// We're only interested if the touch was on an
|
||||
// item that's not a header or footer.
|
||||
if (touchPos != AdapterView.INVALID_POSITION && touchPos >= numHeaders
|
||||
&& touchPos < (count - numFooters)) {
|
||||
final View item = mDslv.getChildAt(touchPos - mDslv.getFirstVisiblePosition());
|
||||
final int rawX = (int) ev.getRawX();
|
||||
final int rawY = (int) ev.getRawY();
|
||||
|
||||
View dragBox = id == 0 ? item : (View) item.findViewById(id);
|
||||
if (dragBox != null) {
|
||||
dragBox.getLocationOnScreen(mTempLoc);
|
||||
|
||||
if (rawX > mTempLoc[0] && rawY > mTempLoc[1] &&
|
||||
rawX < mTempLoc[0] + dragBox.getWidth() &&
|
||||
rawY < mTempLoc[1] + dragBox.getHeight()) {
|
||||
|
||||
mItemX = item.getLeft();
|
||||
mItemY = item.getTop();
|
||||
|
||||
return touchPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MISS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent ev) {
|
||||
if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) {
|
||||
mClickRemoveHitPos = viewIdHitPosition(ev, mClickRemoveId);
|
||||
}
|
||||
|
||||
mHitPos = startDragPosition(ev);
|
||||
if (mHitPos != MISS && mDragInitMode == ON_DOWN) {
|
||||
startDrag(mHitPos, (int) ev.getX() - mItemX, (int) ev.getY() - mItemY);
|
||||
}
|
||||
|
||||
mIsRemoving = false;
|
||||
mCanDrag = true;
|
||||
mPositionX = 0;
|
||||
mFlingHitPos = startFlingPosition(ev);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
|
||||
if(e1 == null) return false;
|
||||
if(e2 == null) return false;
|
||||
|
||||
final int x1 = (int) e1.getX();
|
||||
final int y1 = (int) e1.getY();
|
||||
final int x2 = (int) e2.getX();
|
||||
final int y2 = (int) e2.getY();
|
||||
final int deltaX = x2 - mItemX;
|
||||
final int deltaY = y2 - mItemY;
|
||||
|
||||
if (mCanDrag && !mDragging && (mHitPos != MISS || mFlingHitPos != MISS)) {
|
||||
if (mHitPos != MISS) {
|
||||
if (mDragInitMode == ON_DRAG && Math.abs(y2 - y1) > mTouchSlop && mSortEnabled) {
|
||||
startDrag(mHitPos, deltaX, deltaY);
|
||||
}
|
||||
else if (mDragInitMode != ON_DOWN && Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled)
|
||||
{
|
||||
mIsRemoving = true;
|
||||
startDrag(mFlingHitPos, deltaX, deltaY);
|
||||
}
|
||||
} else if (mFlingHitPos != MISS) {
|
||||
if (Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled) {
|
||||
mIsRemoving = true;
|
||||
startDrag(mFlingHitPos, deltaX, deltaY);
|
||||
} else if (Math.abs(y2 - y1) > mTouchSlop) {
|
||||
mCanDrag = false; // if started to scroll the list then
|
||||
// don't allow sorting nor fling-removing
|
||||
}
|
||||
}
|
||||
}
|
||||
// return whatever
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
// Log.d("mobeta", "lift listener long pressed");
|
||||
if (mHitPos != MISS && mDragInitMode == ON_LONG_PRESS) {
|
||||
mDslv.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
startDrag(mHitPos, mCurrX - mItemX, mCurrY - mItemY);
|
||||
}
|
||||
}
|
||||
|
||||
// complete the OnGestureListener interface
|
||||
@Override
|
||||
public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// complete the OnGestureListener interface
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent ev) {
|
||||
if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) {
|
||||
if (mClickRemoveHitPos != MISS) {
|
||||
mDslv.removeItem(mClickRemoveHitPos - mDslv.getHeaderViewsCount());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// complete the OnGestureListener interface
|
||||
@Override
|
||||
public void onShowPress(MotionEvent ev) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
private GestureDetector.OnGestureListener mFlingRemoveListener =
|
||||
new GestureDetector.SimpleOnGestureListener() {
|
||||
@Override
|
||||
public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
|
||||
float velocityY) {
|
||||
// Log.d("mobeta", "on fling remove called");
|
||||
if (mRemoveEnabled && mIsRemoving) {
|
||||
int w = mDslv.getWidth();
|
||||
int minPos = w / 5;
|
||||
if (velocityX > mFlingSpeed) {
|
||||
if (mPositionX > -minPos) {
|
||||
mDslv.stopDragWithVelocity(true, velocityX);
|
||||
}
|
||||
} else if (velocityX < -mFlingSpeed) {
|
||||
if (mPositionX < minPos) {
|
||||
mDslv.stopDragWithVelocity(true, velocityX);
|
||||
}
|
||||
}
|
||||
mIsRemoving = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package com.mobeta.android.dslv;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
|
||||
|
||||
/**
|
||||
* A subclass of {@link android.widget.CursorAdapter} that provides
|
||||
* reordering of the elements in the Cursor based on completed
|
||||
* drag-sort operations. The reordering is a simple mapping of
|
||||
* list positions into Cursor positions (the Cursor is unchanged).
|
||||
* To persist changes made by drag-sorts, one can retrieve the
|
||||
* mapping with the {@link #getCursorPositions()} method, which
|
||||
* returns the reordered list of Cursor positions.
|
||||
*
|
||||
* An instance of this class is passed
|
||||
* to {@link DragSortListView#setAdapter(ListAdapter)} and, since
|
||||
* this class implements the {@link DragSortListView.DragSortListener}
|
||||
* interface, it is automatically set as the DragSortListener for
|
||||
* the DragSortListView instance.
|
||||
*/
|
||||
public abstract class DragSortCursorAdapter extends CursorAdapter implements DragSortListView.DragSortListener {
|
||||
|
||||
public static final int REMOVED = -1;
|
||||
|
||||
/**
|
||||
* Key is ListView position, value is Cursor position
|
||||
*/
|
||||
private SparseIntArray mListMapping = new SparseIntArray();
|
||||
|
||||
private ArrayList<Integer> mRemovedCursorPositions = new ArrayList<Integer>();
|
||||
|
||||
public DragSortCursorAdapter(Context context, Cursor c) {
|
||||
super(context, c);
|
||||
}
|
||||
|
||||
public DragSortCursorAdapter(Context context, Cursor c, boolean autoRequery) {
|
||||
super(context, c, autoRequery);
|
||||
}
|
||||
|
||||
public DragSortCursorAdapter(Context context, Cursor c, int flags) {
|
||||
super(context, c, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps Cursor and clears list-Cursor mapping.
|
||||
*
|
||||
* @see android.widget.CursorAdapter#swapCursor(android.database.Cursor)
|
||||
*/
|
||||
@Override
|
||||
public Cursor swapCursor(Cursor newCursor) {
|
||||
Cursor old = super.swapCursor(newCursor);
|
||||
resetMappings();
|
||||
return old;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes Cursor and clears list-Cursor mapping.
|
||||
*
|
||||
* @see android.widget.CursorAdapter#changeCursor(android.database.Cursor)
|
||||
*/
|
||||
@Override
|
||||
public void changeCursor(Cursor cursor) {
|
||||
super.changeCursor(cursor);
|
||||
resetMappings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets list-cursor mapping.
|
||||
*/
|
||||
public void reset() {
|
||||
resetMappings();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void resetMappings() {
|
||||
mListMapping.clear();
|
||||
mRemovedCursorPositions.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return super.getItem(mListMapping.get(position, position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return super.getItemId(mListMapping.get(position, position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, View convertView, ViewGroup parent) {
|
||||
return super.getDropDownView(mListMapping.get(position, position), convertView, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
return super.getView(mListMapping.get(position, position), convertView, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* On drop, this updates the mapping between Cursor positions
|
||||
* and ListView positions. The Cursor is unchanged. Retrieve
|
||||
* the current mapping with {@link getCursorPositions()}.
|
||||
*
|
||||
* @see DragSortListView.DropListener#drop(int, int)
|
||||
*/
|
||||
@Override
|
||||
public void drop(int from, int to) {
|
||||
if (from != to) {
|
||||
int cursorFrom = mListMapping.get(from, from);
|
||||
|
||||
if (from > to) {
|
||||
for (int i = from; i > to; --i) {
|
||||
mListMapping.put(i, mListMapping.get(i - 1, i - 1));
|
||||
}
|
||||
} else {
|
||||
for (int i = from; i < to; ++i) {
|
||||
mListMapping.put(i, mListMapping.get(i + 1, i + 1));
|
||||
}
|
||||
}
|
||||
mListMapping.put(to, cursorFrom);
|
||||
|
||||
cleanMapping();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On remove, this updates the mapping between Cursor positions
|
||||
* and ListView positions. The Cursor is unchanged. Retrieve
|
||||
* the current mapping with {@link getCursorPositions()}.
|
||||
*
|
||||
* @see DragSortListView.RemoveListener#remove(int)
|
||||
*/
|
||||
@Override
|
||||
public void remove(int which) {
|
||||
int cursorPos = mListMapping.get(which, which);
|
||||
if (!mRemovedCursorPositions.contains(cursorPos)) {
|
||||
mRemovedCursorPositions.add(cursorPos);
|
||||
}
|
||||
|
||||
int newCount = getCount();
|
||||
for (int i = which; i < newCount; ++i) {
|
||||
mListMapping.put(i, mListMapping.get(i + 1, i + 1));
|
||||
}
|
||||
|
||||
mListMapping.delete(newCount);
|
||||
|
||||
cleanMapping();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing. Just completes DragSortListener interface.
|
||||
*/
|
||||
@Override
|
||||
public void drag(int from, int to) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove unnecessary mappings from sparse array.
|
||||
*/
|
||||
private void cleanMapping() {
|
||||
ArrayList<Integer> toRemove = new ArrayList<Integer>();
|
||||
|
||||
int size = mListMapping.size();
|
||||
for (int i = 0; i < size; ++i) {
|
||||
if (mListMapping.keyAt(i) == mListMapping.valueAt(i)) {
|
||||
toRemove.add(mListMapping.keyAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
size = toRemove.size();
|
||||
for (int i = 0; i < size; ++i) {
|
||||
mListMapping.delete(toRemove.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return super.getCount() - mRemovedCursorPositions.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Cursor position mapped to by the provided list position
|
||||
* (given all previously handled drag-sort
|
||||
* operations).
|
||||
*
|
||||
* @param position List position
|
||||
*
|
||||
* @return The mapped-to Cursor position
|
||||
*/
|
||||
public int getCursorPosition(int position) {
|
||||
return mListMapping.get(position, position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current order of Cursor positions presented by the
|
||||
* list.
|
||||
*/
|
||||
public ArrayList<Integer> getCursorPositions() {
|
||||
ArrayList<Integer> result = new ArrayList<Integer>();
|
||||
|
||||
for (int i = 0; i < getCount(); ++i) {
|
||||
result.add(mListMapping.get(i, i));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list position mapped to by the provided Cursor position.
|
||||
* If the provided Cursor position has been removed by a drag-sort,
|
||||
* this returns {@link #REMOVED}.
|
||||
*
|
||||
* @param cursorPosition A Cursor position
|
||||
* @return The mapped-to list position or REMOVED
|
||||
*/
|
||||
public int getListPosition(int cursorPosition) {
|
||||
if (mRemovedCursorPositions.contains(cursorPosition)) {
|
||||
return REMOVED;
|
||||
}
|
||||
|
||||
int index = mListMapping.indexOfValue(cursorPosition);
|
||||
if (index < 0) {
|
||||
return cursorPosition;
|
||||
} else {
|
||||
return mListMapping.keyAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
100
app/src/main/java/com/mobeta/android/dslv/DragSortItemView.java
Normal file
@@ -0,0 +1,100 @@
|
||||
package com.mobeta.android.dslv;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.View.MeasureSpec;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Lightweight ViewGroup that wraps list items obtained from user's
|
||||
* ListAdapter. ItemView expects a single child that has a definite
|
||||
* height (i.e. the child's layout height is not MATCH_PARENT).
|
||||
* The width of
|
||||
* ItemView will always match the width of its child (that is,
|
||||
* the width MeasureSpec given to ItemView is passed directly
|
||||
* to the child, and the ItemView measured width is set to the
|
||||
* child's measured width). The height of ItemView can be anything;
|
||||
* the
|
||||
*
|
||||
*
|
||||
* The purpose of this class is to optimize slide
|
||||
* shuffle animations.
|
||||
*/
|
||||
public class DragSortItemView extends ViewGroup {
|
||||
|
||||
private int mGravity = Gravity.TOP;
|
||||
|
||||
public DragSortItemView(Context context) {
|
||||
super(context);
|
||||
|
||||
// always init with standard ListView layout params
|
||||
setLayoutParams(new AbsListView.LayoutParams(
|
||||
ViewGroup.LayoutParams.FILL_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
//setClipChildren(true);
|
||||
}
|
||||
|
||||
public void setGravity(int gravity) {
|
||||
mGravity = gravity;
|
||||
}
|
||||
|
||||
public int getGravity() {
|
||||
return mGravity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
final View child = getChildAt(0);
|
||||
|
||||
if (child == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mGravity == Gravity.TOP) {
|
||||
child.layout(0, 0, getMeasuredWidth(), child.getMeasuredHeight());
|
||||
} else {
|
||||
child.layout(0, getMeasuredHeight() - child.getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
|
||||
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
|
||||
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
|
||||
|
||||
final View child = getChildAt(0);
|
||||
if (child == null) {
|
||||
setMeasuredDimension(0, width);
|
||||
return;
|
||||
}
|
||||
|
||||
if (child.isLayoutRequested()) {
|
||||
// Always let child be as tall as it wants.
|
||||
measureChild(child, widthMeasureSpec,
|
||||
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
|
||||
}
|
||||
|
||||
if (heightMode == MeasureSpec.UNSPECIFIED) {
|
||||
ViewGroup.LayoutParams lp = getLayoutParams();
|
||||
|
||||
if (lp.height > 0) {
|
||||
height = lp.height;
|
||||
} else {
|
||||
height = child.getMeasuredHeight();
|
||||
}
|
||||
}
|
||||
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.mobeta.android.dslv;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.View.MeasureSpec;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.Checkable;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Lightweight ViewGroup that wraps list items obtained from user's
|
||||
* ListAdapter. ItemView expects a single child that has a definite
|
||||
* height (i.e. the child's layout height is not MATCH_PARENT).
|
||||
* The width of
|
||||
* ItemView will always match the width of its child (that is,
|
||||
* the width MeasureSpec given to ItemView is passed directly
|
||||
* to the child, and the ItemView measured width is set to the
|
||||
* child's measured width). The height of ItemView can be anything;
|
||||
* the
|
||||
*
|
||||
*
|
||||
* The purpose of this class is to optimize slide
|
||||
* shuffle animations.
|
||||
*/
|
||||
public class DragSortItemViewCheckable extends DragSortItemView implements Checkable {
|
||||
|
||||
public DragSortItemViewCheckable(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked() {
|
||||
View child = getChildAt(0);
|
||||
if (child instanceof Checkable)
|
||||
return ((Checkable) child).isChecked();
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(boolean checked) {
|
||||
View child = getChildAt(0);
|
||||
if (child instanceof Checkable)
|
||||
((Checkable) child).setChecked(checked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toggle() {
|
||||
View child = getChildAt(0);
|
||||
if (child instanceof Checkable)
|
||||
((Checkable) child).toggle();
|
||||
}
|
||||
}
|
||||
3075
app/src/main/java/com/mobeta/android/dslv/DragSortListView.java
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.mobeta.android.dslv;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
// taken from v4 rev. 10 ResourceCursorAdapter.java
|
||||
|
||||
/**
|
||||
* Static library support version of the framework's {@link android.widget.ResourceCursorAdapter}.
|
||||
* Used to write apps that run on platforms prior to Android 3.0. When running
|
||||
* on Android 3.0 or above, this implementation is still used; it does not try
|
||||
* to switch to the framework's implementation. See the framework SDK
|
||||
* documentation for a class overview.
|
||||
*/
|
||||
public abstract class ResourceDragSortCursorAdapter extends DragSortCursorAdapter {
|
||||
private int mLayout;
|
||||
|
||||
private int mDropDownLayout;
|
||||
|
||||
private LayoutInflater mInflater;
|
||||
|
||||
/**
|
||||
* Constructor the enables auto-requery.
|
||||
*
|
||||
* @deprecated This option is discouraged, as it results in Cursor queries
|
||||
* being performed on the application's UI thread and thus can cause poor
|
||||
* responsiveness or even Application Not Responding errors. As an alternative,
|
||||
* use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
|
||||
*
|
||||
* @param context The context where the ListView associated with this adapter is running
|
||||
* @param layout resource identifier of a layout file that defines the views
|
||||
* for this list item. Unless you override them later, this will
|
||||
* define both the item views and the drop down views.
|
||||
*/
|
||||
@Deprecated
|
||||
public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c) {
|
||||
super(context, c);
|
||||
mLayout = mDropDownLayout = layout;
|
||||
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with default behavior as per
|
||||
* {@link CursorAdapter#CursorAdapter(Context, Cursor, boolean)}; it is recommended
|
||||
* you not use this, but instead {@link #ResourceCursorAdapter(Context, int, Cursor, int)}.
|
||||
* When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
|
||||
* will always be set.
|
||||
*
|
||||
* @param context The context where the ListView associated with this adapter is running
|
||||
* @param layout resource identifier of a layout file that defines the views
|
||||
* for this list item. Unless you override them later, this will
|
||||
* define both the item views and the drop down views.
|
||||
* @param c The cursor from which to get the data.
|
||||
* @param autoRequery If true the adapter will call requery() on the
|
||||
* cursor whenever it changes so the most recent
|
||||
* data is always displayed. Using true here is discouraged.
|
||||
*/
|
||||
public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c, boolean autoRequery) {
|
||||
super(context, c, autoRequery);
|
||||
mLayout = mDropDownLayout = layout;
|
||||
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard constructor.
|
||||
*
|
||||
* @param context The context where the ListView associated with this adapter is running
|
||||
* @param layout Resource identifier of a layout file that defines the views
|
||||
* for this list item. Unless you override them later, this will
|
||||
* define both the item views and the drop down views.
|
||||
* @param c The cursor from which to get the data.
|
||||
* @param flags Flags used to determine the behavior of the adapter,
|
||||
* as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}.
|
||||
*/
|
||||
public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c, int flags) {
|
||||
super(context, c, flags);
|
||||
mLayout = mDropDownLayout = layout;
|
||||
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates view(s) from the specified XML file.
|
||||
*
|
||||
* @see android.widget.CursorAdapter#newView(android.content.Context,
|
||||
* android.database.Cursor, ViewGroup)
|
||||
*/
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return mInflater.inflate(mLayout, parent, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return mInflater.inflate(mDropDownLayout, parent, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Sets the layout resource of the item views.</p>
|
||||
*
|
||||
* @param layout the layout resources used to create item views
|
||||
*/
|
||||
public void setViewResource(int layout) {
|
||||
mLayout = layout;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Sets the layout resource of the drop down views.</p>
|
||||
*
|
||||
* @param dropDownLayout the layout resources used to create drop down views
|
||||
*/
|
||||
public void setDropDownViewResource(int dropDownLayout) {
|
||||
mDropDownLayout = dropDownLayout;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
/*
|
||||
* Copyright (C) 2006 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.mobeta.android.dslv;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.ImageView;
|
||||
|
||||
// taken from sdk/sources/android-16/android/widget/SimpleCursorAdapter.java
|
||||
|
||||
/**
|
||||
* An easy adapter to map columns from a cursor to TextViews or ImageViews
|
||||
* defined in an XML file. You can specify which columns you want, which
|
||||
* views you want to display the columns, and the XML file that defines
|
||||
* the appearance of these views.
|
||||
*
|
||||
* Binding occurs in two phases. First, if a
|
||||
* {@link android.widget.SimpleCursorAdapter.ViewBinder} is available,
|
||||
* {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
|
||||
* is invoked. If the returned value is true, binding has occured. If the
|
||||
* returned value is false and the view to bind is a TextView,
|
||||
* {@link #setViewText(TextView, String)} is invoked. If the returned value
|
||||
* is false and the view to bind is an ImageView,
|
||||
* {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
|
||||
* binding can be found, an {@link IllegalStateException} is thrown.
|
||||
*
|
||||
* If this adapter is used with filtering, for instance in an
|
||||
* {@link android.widget.AutoCompleteTextView}, you can use the
|
||||
* {@link android.widget.SimpleCursorAdapter.CursorToStringConverter} and the
|
||||
* {@link android.widget.FilterQueryProvider} interfaces
|
||||
* to get control over the filtering process. You can refer to
|
||||
* {@link #convertToString(android.database.Cursor)} and
|
||||
* {@link #runQueryOnBackgroundThread(CharSequence)} for more information.
|
||||
*/
|
||||
public class SimpleDragSortCursorAdapter extends ResourceDragSortCursorAdapter {
|
||||
/**
|
||||
* A list of columns containing the data to bind to the UI.
|
||||
* This field should be made private, so it is hidden from the SDK.
|
||||
* {@hide}
|
||||
*/
|
||||
protected int[] mFrom;
|
||||
/**
|
||||
* A list of View ids representing the views to which the data must be bound.
|
||||
* This field should be made private, so it is hidden from the SDK.
|
||||
* {@hide}
|
||||
*/
|
||||
protected int[] mTo;
|
||||
|
||||
private int mStringConversionColumn = -1;
|
||||
private CursorToStringConverter mCursorToStringConverter;
|
||||
private ViewBinder mViewBinder;
|
||||
|
||||
String[] mOriginalFrom;
|
||||
|
||||
/**
|
||||
* Constructor the enables auto-requery.
|
||||
*
|
||||
* @deprecated This option is discouraged, as it results in Cursor queries
|
||||
* being performed on the application's UI thread and thus can cause poor
|
||||
* responsiveness or even Application Not Responding errors. As an alternative,
|
||||
* use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
|
||||
*/
|
||||
@Deprecated
|
||||
public SimpleDragSortCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
|
||||
super(context, layout, c);
|
||||
mTo = to;
|
||||
mOriginalFrom = from;
|
||||
findColumns(c, from);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard constructor.
|
||||
*
|
||||
* @param context The context where the ListView associated with this
|
||||
* SimpleListItemFactory is running
|
||||
* @param layout resource identifier of a layout file that defines the views
|
||||
* for this list item. The layout file should include at least
|
||||
* those named views defined in "to"
|
||||
* @param c The database cursor. Can be null if the cursor is not available yet.
|
||||
* @param from A list of column names representing the data to bind to the UI. Can be null
|
||||
* if the cursor is not available yet.
|
||||
* @param to The views that should display column in the "from" parameter.
|
||||
* These should all be TextViews. The first N views in this list
|
||||
* are given the values of the first N columns in the from
|
||||
* parameter. Can be null if the cursor is not available yet.
|
||||
* @param flags Flags used to determine the behavior of the adapter,
|
||||
* as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}.
|
||||
*/
|
||||
public SimpleDragSortCursorAdapter(Context context, int layout,
|
||||
Cursor c, String[] from, int[] to, int flags) {
|
||||
super(context, layout, c, flags);
|
||||
mTo = to;
|
||||
mOriginalFrom = from;
|
||||
findColumns(c, from);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds all of the field names passed into the "to" parameter of the
|
||||
* constructor with their corresponding cursor columns as specified in the
|
||||
* "from" parameter.
|
||||
*
|
||||
* Binding occurs in two phases. First, if a
|
||||
* {@link android.widget.SimpleCursorAdapter.ViewBinder} is available,
|
||||
* {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
|
||||
* is invoked. If the returned value is true, binding has occured. If the
|
||||
* returned value is false and the view to bind is a TextView,
|
||||
* {@link #setViewText(TextView, String)} is invoked. If the returned value is
|
||||
* false and the view to bind is an ImageView,
|
||||
* {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
|
||||
* binding can be found, an {@link IllegalStateException} is thrown.
|
||||
*
|
||||
* @throws IllegalStateException if binding cannot occur
|
||||
*
|
||||
* @see android.widget.CursorAdapter#bindView(android.view.View,
|
||||
* android.content.Context, android.database.Cursor)
|
||||
* @see #getViewBinder()
|
||||
* @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder)
|
||||
* @see #setViewImage(ImageView, String)
|
||||
* @see #setViewText(TextView, String)
|
||||
*/
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
final ViewBinder binder = mViewBinder;
|
||||
final int count = mTo.length;
|
||||
final int[] from = mFrom;
|
||||
final int[] to = mTo;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
final View v = view.findViewById(to[i]);
|
||||
if (v != null) {
|
||||
boolean bound = false;
|
||||
if (binder != null) {
|
||||
bound = binder.setViewValue(v, cursor, from[i]);
|
||||
}
|
||||
|
||||
if (!bound) {
|
||||
String text = cursor.getString(from[i]);
|
||||
if (text == null) {
|
||||
text = "";
|
||||
}
|
||||
|
||||
if (v instanceof TextView) {
|
||||
setViewText((TextView) v, text);
|
||||
} else if (v instanceof ImageView) {
|
||||
setViewImage((ImageView) v, text);
|
||||
} else {
|
||||
throw new IllegalStateException(v.getClass().getName() + " is not a " +
|
||||
" view that can be bounds by this SimpleCursorAdapter");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ViewBinder} used to bind data to views.
|
||||
*
|
||||
* @return a ViewBinder or null if the binder does not exist
|
||||
*
|
||||
* @see #bindView(android.view.View, android.content.Context, android.database.Cursor)
|
||||
* @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder)
|
||||
*/
|
||||
public ViewBinder getViewBinder() {
|
||||
return mViewBinder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the binder used to bind data to views.
|
||||
*
|
||||
* @param viewBinder the binder used to bind data to views, can be null to
|
||||
* remove the existing binder
|
||||
*
|
||||
* @see #bindView(android.view.View, android.content.Context, android.database.Cursor)
|
||||
* @see #getViewBinder()
|
||||
*/
|
||||
public void setViewBinder(ViewBinder viewBinder) {
|
||||
mViewBinder = viewBinder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by bindView() to set the image for an ImageView but only if
|
||||
* there is no existing ViewBinder or if the existing ViewBinder cannot
|
||||
* handle binding to an ImageView.
|
||||
*
|
||||
* By default, the value will be treated as an image resource. If the
|
||||
* value cannot be used as an image resource, the value is used as an
|
||||
* image Uri.
|
||||
*
|
||||
* Intended to be overridden by Adapters that need to filter strings
|
||||
* retrieved from the database.
|
||||
*
|
||||
* @param v ImageView to receive an image
|
||||
* @param value the value retrieved from the cursor
|
||||
*/
|
||||
public void setViewImage(ImageView v, String value) {
|
||||
try {
|
||||
v.setImageResource(Integer.parseInt(value));
|
||||
} catch (NumberFormatException nfe) {
|
||||
v.setImageURI(Uri.parse(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by bindView() to set the text for a TextView but only if
|
||||
* there is no existing ViewBinder or if the existing ViewBinder cannot
|
||||
* handle binding to a TextView.
|
||||
*
|
||||
* Intended to be overridden by Adapters that need to filter strings
|
||||
* retrieved from the database.
|
||||
*
|
||||
* @param v TextView to receive text
|
||||
* @param text the text to be set for the TextView
|
||||
*/
|
||||
public void setViewText(TextView v, String text) {
|
||||
v.setText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of the column used to get a String representation
|
||||
* of the Cursor.
|
||||
*
|
||||
* @return a valid index in the current Cursor or -1
|
||||
*
|
||||
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
|
||||
* @see #setStringConversionColumn(int)
|
||||
* @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
|
||||
* @see #getCursorToStringConverter()
|
||||
*/
|
||||
public int getStringConversionColumn() {
|
||||
return mStringConversionColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the index of the column in the Cursor used to get a String
|
||||
* representation of that Cursor. The column is used to convert the
|
||||
* Cursor to a String only when the current CursorToStringConverter
|
||||
* is null.
|
||||
*
|
||||
* @param stringConversionColumn a valid index in the current Cursor or -1 to use the default
|
||||
* conversion mechanism
|
||||
*
|
||||
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
|
||||
* @see #getStringConversionColumn()
|
||||
* @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
|
||||
* @see #getCursorToStringConverter()
|
||||
*/
|
||||
public void setStringConversionColumn(int stringConversionColumn) {
|
||||
mStringConversionColumn = stringConversionColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the converter used to convert the filtering Cursor
|
||||
* into a String.
|
||||
*
|
||||
* @return null if the converter does not exist or an instance of
|
||||
* {@link android.widget.SimpleCursorAdapter.CursorToStringConverter}
|
||||
*
|
||||
* @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
|
||||
* @see #getStringConversionColumn()
|
||||
* @see #setStringConversionColumn(int)
|
||||
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
|
||||
*/
|
||||
public CursorToStringConverter getCursorToStringConverter() {
|
||||
return mCursorToStringConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the converter used to convert the filtering Cursor
|
||||
* into a String.
|
||||
*
|
||||
* @param cursorToStringConverter the Cursor to String converter, or
|
||||
* null to remove the converter
|
||||
*
|
||||
* @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
|
||||
* @see #getStringConversionColumn()
|
||||
* @see #setStringConversionColumn(int)
|
||||
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
|
||||
*/
|
||||
public void setCursorToStringConverter(CursorToStringConverter cursorToStringConverter) {
|
||||
mCursorToStringConverter = cursorToStringConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CharSequence representation of the specified Cursor as defined
|
||||
* by the current CursorToStringConverter. If no CursorToStringConverter
|
||||
* has been set, the String conversion column is used instead. If the
|
||||
* conversion column is -1, the returned String is empty if the cursor
|
||||
* is null or Cursor.toString().
|
||||
*
|
||||
* @param cursor the Cursor to convert to a CharSequence
|
||||
*
|
||||
* @return a non-null CharSequence representing the cursor
|
||||
*/
|
||||
@Override
|
||||
public CharSequence convertToString(Cursor cursor) {
|
||||
if (mCursorToStringConverter != null) {
|
||||
return mCursorToStringConverter.convertToString(cursor);
|
||||
} else if (mStringConversionColumn > -1) {
|
||||
return cursor.getString(mStringConversionColumn);
|
||||
}
|
||||
|
||||
return super.convertToString(cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a map from an array of strings to an array of column-id integers in cursor c.
|
||||
* If c is null, the array will be discarded.
|
||||
*
|
||||
* @param c the cursor to find the columns from
|
||||
* @param from the Strings naming the columns of interest
|
||||
*/
|
||||
private void findColumns(Cursor c, String[] from) {
|
||||
if (c != null) {
|
||||
int i;
|
||||
int count = from.length;
|
||||
if (mFrom == null || mFrom.length != count) {
|
||||
mFrom = new int[count];
|
||||
}
|
||||
for (i = 0; i < count; i++) {
|
||||
mFrom[i] = c.getColumnIndexOrThrow(from[i]);
|
||||
}
|
||||
} else {
|
||||
mFrom = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor swapCursor(Cursor c) {
|
||||
// super.swapCursor() will notify observers before we have
|
||||
// a valid mapping, make sure we have a mapping before this
|
||||
// happens
|
||||
findColumns(c, mOriginalFrom);
|
||||
return super.swapCursor(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the cursor and change the column-to-view mappings at the same time.
|
||||
*
|
||||
* @param c The database cursor. Can be null if the cursor is not available yet.
|
||||
* @param from A list of column names representing the data to bind to the UI. Can be null
|
||||
* if the cursor is not available yet.
|
||||
* @param to The views that should display column in the "from" parameter.
|
||||
* These should all be TextViews. The first N views in this list
|
||||
* are given the values of the first N columns in the from
|
||||
* parameter. Can be null if the cursor is not available yet.
|
||||
*/
|
||||
public void changeCursorAndColumns(Cursor c, String[] from, int[] to) {
|
||||
mOriginalFrom = from;
|
||||
mTo = to;
|
||||
// super.changeCursor() will notify observers before we have
|
||||
// a valid mapping, make sure we have a mapping before this
|
||||
// happens
|
||||
findColumns(c, mOriginalFrom);
|
||||
super.changeCursor(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* This class can be used by external clients of SimpleCursorAdapter
|
||||
* to bind values fom the Cursor to views.
|
||||
*
|
||||
* You should use this class to bind values from the Cursor to views
|
||||
* that are not directly supported by SimpleCursorAdapter or to
|
||||
* change the way binding occurs for views supported by
|
||||
* SimpleCursorAdapter.
|
||||
*
|
||||
* @see SimpleCursorAdapter#bindView(android.view.View, android.content.Context, android.database.Cursor)
|
||||
* @see SimpleCursorAdapter#setViewImage(ImageView, String)
|
||||
* @see SimpleCursorAdapter#setViewText(TextView, String)
|
||||
*/
|
||||
public static interface ViewBinder {
|
||||
/**
|
||||
* Binds the Cursor column defined by the specified index to the specified view.
|
||||
*
|
||||
* When binding is handled by this ViewBinder, this method must return true.
|
||||
* If this method returns false, SimpleCursorAdapter will attempts to handle
|
||||
* the binding on its own.
|
||||
*
|
||||
* @param view the view to bind the data to
|
||||
* @param cursor the cursor to get the data from
|
||||
* @param columnIndex the column at which the data can be found in the cursor
|
||||
*
|
||||
* @return true if the data was bound to the view, false otherwise
|
||||
*/
|
||||
boolean setViewValue(View view, Cursor cursor, int columnIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* This class can be used by external clients of SimpleCursorAdapter
|
||||
* to define how the Cursor should be converted to a String.
|
||||
*
|
||||
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
|
||||
*/
|
||||
public static interface CursorToStringConverter {
|
||||
/**
|
||||
* Returns a CharSequence representing the specified Cursor.
|
||||
*
|
||||
* @param cursor the cursor for which a CharSequence representation
|
||||
* is requested
|
||||
*
|
||||
* @return a non-null CharSequence representing the cursor
|
||||
*/
|
||||
CharSequence convertToString(Cursor cursor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.mobeta.android.dslv;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Color;
|
||||
import android.widget.ListView;
|
||||
import android.widget.ImageView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Simple implementation of the FloatViewManager class. Uses list
|
||||
* items as they appear in the ListView to create the floating View.
|
||||
*/
|
||||
public class SimpleFloatViewManager implements DragSortListView.FloatViewManager {
|
||||
|
||||
private Bitmap mFloatBitmap;
|
||||
|
||||
private ImageView mImageView;
|
||||
|
||||
private int mFloatBGColor = Color.BLACK;
|
||||
|
||||
private ListView mListView;
|
||||
|
||||
public SimpleFloatViewManager(ListView lv) {
|
||||
mListView = lv;
|
||||
}
|
||||
|
||||
public void setBackgroundColor(int color) {
|
||||
mFloatBGColor = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* This simple implementation creates a Bitmap copy of the
|
||||
* list item currently shown at ListView <code>position</code>.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateFloatView(int position) {
|
||||
// Guaranteed that this will not be null? I think so. Nope, got
|
||||
// a NullPointerException once...
|
||||
View v = mListView.getChildAt(position + mListView.getHeaderViewsCount() - mListView.getFirstVisiblePosition());
|
||||
|
||||
if (v == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
v.setPressed(false);
|
||||
|
||||
// Create a copy of the drawing cache so that it does not get
|
||||
// recycled by the framework when the list tries to clean up memory
|
||||
//v.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
|
||||
v.setDrawingCacheEnabled(true);
|
||||
mFloatBitmap = Bitmap.createBitmap(v.getDrawingCache());
|
||||
v.setDrawingCacheEnabled(false);
|
||||
|
||||
if (mImageView == null) {
|
||||
mImageView = new ImageView(mListView.getContext());
|
||||
}
|
||||
mImageView.setBackgroundColor(mFloatBGColor);
|
||||
mImageView.setPadding(0, 0, 0, 0);
|
||||
mImageView.setImageBitmap(mFloatBitmap);
|
||||
mImageView.setLayoutParams(new ViewGroup.LayoutParams(v.getWidth(), v.getHeight()));
|
||||
|
||||
return mImageView;
|
||||
}
|
||||
|
||||
/**
|
||||
* This does nothing
|
||||
*/
|
||||
@Override
|
||||
public void onDragFloatView(View floatView, Point position, Point touch) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the Bitmap from the ImageView created in
|
||||
* onCreateFloatView() and tells the system to recycle it.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroyFloatView(View floatView) {
|
||||
((ImageView) floatView).setImageDrawable(null);
|
||||
|
||||
mFloatBitmap.recycle();
|
||||
mFloatBitmap = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
35
app/src/main/java/org/isoron/helpers/ColorHelper.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package org.isoron.helpers;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
public class ColorHelper
|
||||
{
|
||||
public static final int[] palette = { Color.parseColor("#900000"),
|
||||
Color.parseColor("#c54100"), Color.parseColor("#c0ab00"),
|
||||
Color.parseColor("#8db600"), Color.parseColor("#117209"),
|
||||
Color.parseColor("#06965b"), Color.parseColor("#069a95"),
|
||||
Color.parseColor("#114896"), Color.parseColor("#501394"),
|
||||
Color.parseColor("#872086"), Color.parseColor("#c31764"),
|
||||
Color.parseColor("#000000"), Color.parseColor("#aaaaaa") };
|
||||
|
||||
public static int mixColors(int color1, int color2, float amount)
|
||||
{
|
||||
final byte ALPHA_CHANNEL = 24;
|
||||
final byte RED_CHANNEL = 16;
|
||||
final byte GREEN_CHANNEL = 8;
|
||||
final byte BLUE_CHANNEL = 0;
|
||||
|
||||
final float inverseAmount = 1.0f - amount;
|
||||
|
||||
int a = ((int) (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) +
|
||||
((float) (color2 >> ALPHA_CHANNEL & 0xff) * inverseAmount))) & 0xff;
|
||||
int r = ((int) (((float) (color1 >> RED_CHANNEL & 0xff) * amount) +
|
||||
((float) (color2 >> RED_CHANNEL & 0xff) * inverseAmount))) & 0xff;
|
||||
int g = ((int) (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) +
|
||||
((float) (color2 >> GREEN_CHANNEL & 0xff) * inverseAmount))) & 0xff;
|
||||
int b = ((int) (((float) (color1 & 0xff) * amount) +
|
||||
((float) (color2 & 0xff) * inverseAmount))) & 0xff;
|
||||
|
||||
return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL | b << BLUE_CHANNEL;
|
||||
}
|
||||
}
|
||||
15
app/src/main/java/org/isoron/helpers/Command.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package org.isoron.helpers;
|
||||
|
||||
|
||||
public abstract class Command {
|
||||
public abstract void execute();
|
||||
public abstract void undo();
|
||||
|
||||
public Integer getExecuteStringId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public Integer getUndoStringId() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
101
app/src/main/java/org/isoron/helpers/DateHelper.java
Normal file
@@ -0,0 +1,101 @@
|
||||
package org.isoron.helpers;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class DateHelper
|
||||
{
|
||||
public static int millisecondsInOneDay = 24 * 60 * 60 * 1000;
|
||||
|
||||
public static long getLocalTime()
|
||||
{
|
||||
TimeZone tz = TimeZone.getDefault();
|
||||
long now = new Date().getTime();
|
||||
return now + tz.getOffset(now);
|
||||
}
|
||||
|
||||
public static long getStartOfDay(long timestamp)
|
||||
{
|
||||
return (timestamp / millisecondsInOneDay) * millisecondsInOneDay;
|
||||
}
|
||||
|
||||
public static long getStartOfToday()
|
||||
{
|
||||
return getStartOfDay(DateHelper.getLocalTime());
|
||||
}
|
||||
|
||||
// public static Date getStartOfDay(Date date)
|
||||
// {
|
||||
// Calendar calendar = Calendar.getInstance();
|
||||
// calendar.setTime(date);
|
||||
// calendar.set(Calendar.HOUR_OF_DAY, 0);
|
||||
// calendar.set(Calendar.MINUTE, 0);
|
||||
// calendar.set(Calendar.SECOND, 0);
|
||||
// calendar.set(Calendar.MILLISECOND, 0);
|
||||
// return calendar.getTime();
|
||||
// }
|
||||
|
||||
public static int differenceInDays(Date from, Date to)
|
||||
{
|
||||
long milliseconds = getStartOfDay(to.getTime()) - getStartOfDay(from.getTime());
|
||||
int days = (int) (milliseconds / millisecondsInOneDay);
|
||||
return days;
|
||||
}
|
||||
|
||||
public static String differenceInWords(Date from, Date to)
|
||||
{
|
||||
Integer days = differenceInDays(from, to);
|
||||
boolean negative = (days < 0);
|
||||
days = Math.abs(days);
|
||||
|
||||
Integer weeks = (int) Math.round(days / 7.0);
|
||||
Double months = days / 30.4;
|
||||
Double years = days / 365.0;
|
||||
|
||||
StringBuffer s = new StringBuffer();
|
||||
DecimalFormat df = new DecimalFormat("#.#");
|
||||
|
||||
if(months > 18)
|
||||
{
|
||||
s.append(df.format(years));
|
||||
s.append(" years");
|
||||
}
|
||||
else if(weeks > 6)
|
||||
{
|
||||
s.append(df.format(months));
|
||||
s.append(" months");
|
||||
}
|
||||
else if(days > 13)
|
||||
{
|
||||
s.append(weeks);
|
||||
s.append(" weeks");
|
||||
}
|
||||
else if(days > 6)
|
||||
{
|
||||
s.append(days);
|
||||
s.append(" days");
|
||||
}
|
||||
else
|
||||
{
|
||||
if(days == 0)
|
||||
s.append("Today");
|
||||
else if(days == 1 && negative)
|
||||
s.append("Yesterday");
|
||||
else if(days == 1 && !negative)
|
||||
s.append("Tomorrow");
|
||||
else
|
||||
{
|
||||
if(negative)
|
||||
s.append("past ");
|
||||
s.append(new SimpleDateFormat("EEEE").format(to));
|
||||
}
|
||||
}
|
||||
|
||||
if(negative && days > 6)
|
||||
s.append(" ago");
|
||||
|
||||
return s.toString();
|
||||
}
|
||||
}
|
||||
41
app/src/main/java/org/isoron/helpers/DialogHelper.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package org.isoron.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
public abstract class DialogHelper
|
||||
{
|
||||
|
||||
// public static AlertDialog alert(Activity context, String title, String message, OnClickListener positiveClickListener) {
|
||||
// return new AlertDialog.Builder(context)
|
||||
// .setTitle(title)
|
||||
// .setMessage(message)
|
||||
// .setPositiveButton(android.R.string.yes, positiveClickListener)
|
||||
// .setNegativeButton(android.R.string.no, null).show();
|
||||
// }
|
||||
|
||||
public static abstract class SimpleClickListener implements OnClickListener
|
||||
{
|
||||
public abstract void onClick();
|
||||
|
||||
public void onClick(DialogInterface dialog, int whichButton)
|
||||
{
|
||||
onClick();
|
||||
}
|
||||
}
|
||||
|
||||
public static interface OnSavedListener
|
||||
{
|
||||
public void onSaved(Command command);
|
||||
}
|
||||
|
||||
public static void showSoftKeyboard(View view)
|
||||
{
|
||||
InputMethodManager imm = (InputMethodManager)
|
||||
view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
}
|
||||
185
app/src/main/java/org/isoron/uhabits/MainActivity.java
Normal file
@@ -0,0 +1,185 @@
|
||||
package org.isoron.uhabits;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.isoron.helpers.Command;
|
||||
import org.isoron.uhabits.dialogs.ListHabitsFragment;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class MainActivity extends Activity
|
||||
{
|
||||
private static int MAX_UNDO_LEVEL = 15;
|
||||
private ListHabitsFragment listHabitsFragment;
|
||||
private LinkedList<Command> undoList;
|
||||
private LinkedList<Command> redoList;
|
||||
|
||||
private Toast toast;
|
||||
|
||||
public static void createReminderAlarms(Context context)
|
||||
{
|
||||
for (Habit habit : Habit.getHabitsWithReminder())
|
||||
createReminderAlarm(context, habit, null);
|
||||
}
|
||||
|
||||
public static void createReminderAlarm(Context context, Habit habit, Long reminderTime)
|
||||
{
|
||||
Uri uri = Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId());
|
||||
|
||||
if (reminderTime == null)
|
||||
{
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(System.currentTimeMillis());
|
||||
calendar.set(Calendar.HOUR_OF_DAY, habit.reminder_hour);
|
||||
calendar.set(Calendar.MINUTE, habit.reminder_min);
|
||||
calendar.set(Calendar.SECOND, 0);
|
||||
|
||||
reminderTime = calendar.getTimeInMillis();
|
||||
|
||||
if (System.currentTimeMillis() > reminderTime)
|
||||
{
|
||||
reminderTime += AlarmManager.INTERVAL_DAY;
|
||||
}
|
||||
}
|
||||
|
||||
Intent alarmIntent = new Intent(context, ReminderAlarmReceiver.class);
|
||||
alarmIntent.setAction(ReminderAlarmReceiver.ACTION_REMIND);
|
||||
alarmIntent.setData(uri);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
|
||||
((int) (habit.getId() % Integer.MAX_VALUE)) + 1,
|
||||
alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= 19)
|
||||
{
|
||||
manager.setExact(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent);
|
||||
} else
|
||||
{
|
||||
manager.set(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent);
|
||||
}
|
||||
|
||||
Log.d("Alarm", String.format("Setting alarm (%s): %s", DateFormat.getDateTimeInstance()
|
||||
.format(new Date(reminderTime)), habit.name));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
getActionBar().setElevation(5);
|
||||
|
||||
setContentView(R.layout.list_habits_activity);
|
||||
|
||||
listHabitsFragment = (ListHabitsFragment) getFragmentManager().findFragmentById(
|
||||
R.id.fragment1);
|
||||
|
||||
Log.d("MainActivity", "Creating activity");
|
||||
|
||||
undoList = new LinkedList<>();
|
||||
redoList = new LinkedList<>();
|
||||
|
||||
createReminderAlarms(MainActivity.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart()
|
||||
{
|
||||
super.onStart();
|
||||
listHabitsFragment.notifyDataSetChanged();
|
||||
Log.d("MainActivity", "Starting activity");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu)
|
||||
{
|
||||
getMenuInflater().inflate(R.menu.main_activity, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item)
|
||||
{
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.action_settings)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public void executeCommand(Command command, boolean datasetChanged)
|
||||
{
|
||||
executeCommand(command, datasetChanged, true);
|
||||
}
|
||||
|
||||
public void executeCommand(Command command, boolean datasetChanged, boolean clearRedoStack)
|
||||
{
|
||||
undoList.push(command);
|
||||
if (undoList.size() > MAX_UNDO_LEVEL)
|
||||
undoList.removeLast();
|
||||
if (clearRedoStack)
|
||||
redoList.clear();
|
||||
command.execute();
|
||||
|
||||
showToast(command.getExecuteStringId());
|
||||
if (datasetChanged)
|
||||
{
|
||||
listHabitsFragment.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void undo()
|
||||
{
|
||||
if (undoList.isEmpty())
|
||||
{
|
||||
showToast(R.string.toast_nothing_to_undo);
|
||||
return;
|
||||
}
|
||||
|
||||
Command last = undoList.pop();
|
||||
redoList.push(last);
|
||||
last.undo();
|
||||
showToast(last.getUndoStringId());
|
||||
|
||||
listHabitsFragment.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void redo()
|
||||
{
|
||||
if (redoList.isEmpty())
|
||||
{
|
||||
showToast(R.string.toast_nothing_to_redo);
|
||||
return;
|
||||
}
|
||||
Command last = redoList.pop();
|
||||
executeCommand(last, true, false);
|
||||
}
|
||||
|
||||
private void showToast(Integer stringId)
|
||||
{
|
||||
if (stringId == null)
|
||||
return;
|
||||
if (toast == null)
|
||||
toast = Toast.makeText(this, stringId, Toast.LENGTH_SHORT);
|
||||
else
|
||||
toast.setText(stringId);
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
161
app/src/main/java/org/isoron/uhabits/ReminderAlarmReceiver.java
Normal file
@@ -0,0 +1,161 @@
|
||||
package org.isoron.uhabits;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
|
||||
public class ReminderAlarmReceiver extends BroadcastReceiver
|
||||
{
|
||||
|
||||
public static String ACTION_CHECK = "org.isoron.uhabits.ACTION_CHECK";
|
||||
public static String ACTION_DISMISS = "org.isoron.uhabits.ACTION_DISMISS";
|
||||
public static String ACTION_REMIND = "org.isoron.uhabits.ACTION_REMIND";
|
||||
public static String ACTION_REMOVE_REMINDER = "org.isoron.uhabits.ACTION_REMOVE_REMINDER";
|
||||
public static String ACTION_SNOOZE = "org.isoron.uhabits.ACTION_SNOOZE";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent)
|
||||
{
|
||||
String action = intent.getAction();
|
||||
|
||||
if(action.equals(ACTION_REMIND))
|
||||
createNotification(context, intent.getData());
|
||||
|
||||
else if(action.equals(ACTION_DISMISS))
|
||||
dismissAllHabits();
|
||||
|
||||
else if(action.equals(ACTION_CHECK))
|
||||
checkHabit(context, intent.getData());
|
||||
|
||||
else if(action.equals(ACTION_SNOOZE))
|
||||
snoozeHabit(context, intent.getData());
|
||||
}
|
||||
|
||||
private void snoozeHabit(Context context, Uri data)
|
||||
{
|
||||
int delayMinutes = 15;
|
||||
Habit habit = Habit.get(ContentUris.parseId(data));
|
||||
MainActivity.createReminderAlarm(context, habit, new Date().getTime() + delayMinutes * 1000);
|
||||
dismissNotification(context);
|
||||
}
|
||||
|
||||
private void checkHabit(Context context, Uri data)
|
||||
{
|
||||
Habit habit = Habit.get(ContentUris.parseId(data));
|
||||
habit.toggleRepetitionToday();
|
||||
habit.save();
|
||||
dismissNotification(context);
|
||||
}
|
||||
|
||||
private void dismissAllHabits()
|
||||
{
|
||||
for(Habit h : Habit.getHighlightedHabits())
|
||||
{
|
||||
Log.d("Alarm", String.format("Removing highlight from: %s", h.name));
|
||||
h.highlight = 0;
|
||||
h.save();
|
||||
}
|
||||
}
|
||||
|
||||
private void dismissNotification(Context context)
|
||||
{
|
||||
NotificationManager notificationManager = (NotificationManager) context
|
||||
.getSystemService(Activity.NOTIFICATION_SERVICE);
|
||||
|
||||
notificationManager.cancel(1);
|
||||
}
|
||||
|
||||
|
||||
private void createNotification(Context context, Uri data)
|
||||
{
|
||||
Log.d("Alarm", "Alarm received!");
|
||||
|
||||
Habit habit = Habit.get(ContentUris.parseId(data));
|
||||
|
||||
if(habit.hasImplicitRepToday())
|
||||
{
|
||||
Log.d("Alarm", String.format("(%s) has implicit rep today", habit.name));
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d("Alarm", String.format("Applying highlight: %s", habit.name));
|
||||
habit.highlight = 1;
|
||||
habit.save();
|
||||
|
||||
// Check if reminder has been turned off after alarm was scheduled
|
||||
if(habit.reminder_hour == null)
|
||||
return;
|
||||
|
||||
Intent contentIntent = new Intent(context, MainActivity.class);
|
||||
contentIntent.setData(data);
|
||||
PendingIntent contentPendingIntent = PendingIntent.getActivity(context, 0, contentIntent, 0);
|
||||
|
||||
Intent deleteIntent = new Intent(context, ReminderAlarmReceiver.class);
|
||||
deleteIntent.setAction(ACTION_DISMISS);
|
||||
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, 0);
|
||||
|
||||
Intent checkIntent = new Intent(context, ReminderAlarmReceiver.class);
|
||||
checkIntent.setData(data);
|
||||
checkIntent.setAction(ACTION_CHECK);
|
||||
PendingIntent checkIntentPending = PendingIntent.getBroadcast(context, 0, checkIntent, 0);
|
||||
|
||||
Intent snoozeIntent = new Intent(context, ReminderAlarmReceiver.class);
|
||||
snoozeIntent.setData(data);
|
||||
snoozeIntent.setAction(ACTION_SNOOZE);
|
||||
PendingIntent snoozeIntentPending = PendingIntent.getBroadcast(context, 0, snoozeIntent, 0);
|
||||
|
||||
Uri soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
|
||||
|
||||
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
|
||||
inboxStyle.setBigContentTitle("Habit Reminder:");
|
||||
List<Habit> pendingHabits = Habit.getHighlightedHabits();
|
||||
StringBuffer contentText = new StringBuffer();
|
||||
for(Habit h : pendingHabits)
|
||||
{
|
||||
if(h.hasImplicitRepToday())
|
||||
continue;
|
||||
|
||||
inboxStyle.addLine(h.name);
|
||||
if(contentText.length() > 0)
|
||||
contentText.append(", ");
|
||||
contentText.append(h.name);
|
||||
Log.d("Alarm", String.format("Found highlighted: %s", h.name));
|
||||
}
|
||||
|
||||
Notification notification =
|
||||
new NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle("Habit Reminder")
|
||||
.setContentText(contentText)
|
||||
.setContentIntent(contentPendingIntent)
|
||||
.setDeleteIntent(deletePendingIntent)
|
||||
.addAction(R.drawable.ic_action_check, "Check", checkIntentPending)
|
||||
.addAction(R.drawable.ic_action_snooze, "Later", snoozeIntentPending)
|
||||
.setSound(soundUri)
|
||||
.setStyle(inboxStyle)
|
||||
.build();
|
||||
|
||||
notification.flags |= Notification.FLAG_AUTO_CANCEL;
|
||||
|
||||
NotificationManager notificationManager = (NotificationManager) context
|
||||
.getSystemService(Activity.NOTIFICATION_SERVICE);
|
||||
|
||||
notificationManager.notify(1, notification);
|
||||
}
|
||||
|
||||
}
|
||||
54
app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package org.isoron.uhabits;
|
||||
|
||||
import org.isoron.uhabits.dialogs.ListHabitsFragment;
|
||||
import org.isoron.uhabits.dialogs.ShowHabitFragment;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentUris;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
public class ShowHabitActivity extends Activity
|
||||
{
|
||||
|
||||
public Habit habit;
|
||||
private ShowHabitFragment showHabitFragment;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
getActionBar().setElevation(5);
|
||||
Uri data = getIntent().getData();
|
||||
habit = Habit.get(ContentUris.parseId(data));
|
||||
getActionBar().setTitle(habit.name);
|
||||
getActionBar().setBackgroundDrawable(new ColorDrawable(habit.color));
|
||||
|
||||
setContentView(R.layout.show_habit_activity);
|
||||
showHabitFragment = (ShowHabitFragment) getFragmentManager().findFragmentById(
|
||||
R.id.fragment2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu)
|
||||
{
|
||||
getMenuInflater().inflate(R.menu.show_habit, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item)
|
||||
{
|
||||
int id = item.getItemId();
|
||||
if(id == R.id.action_settings)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package org.isoron.uhabits.dialogs;
|
||||
|
||||
import org.isoron.helpers.ColorHelper;
|
||||
import org.isoron.helpers.Command;
|
||||
import org.isoron.helpers.DialogHelper.OnSavedListener;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
|
||||
import android.app.DialogFragment;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorMatrix;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.colorpicker.ColorPickerDialog;
|
||||
import com.android.colorpicker.ColorPickerSwatch;
|
||||
import com.android.datetimepicker.time.RadialPickerLayout;
|
||||
import com.android.datetimepicker.time.TimePickerDialog;
|
||||
import com.android.datetimepicker.time.TimePickerDialog.OnTimeSetListener;
|
||||
|
||||
public class EditHabitFragment extends DialogFragment implements OnClickListener
|
||||
{
|
||||
private int mode;
|
||||
static final int EDIT_MODE = 0;
|
||||
static final int CREATE_MODE = 1;
|
||||
|
||||
private OnSavedListener onSavedListener;
|
||||
|
||||
private Habit originalHabit, modified_habit;
|
||||
private TextView tvName, tvDescription, tvFreqNum, tvFreqDen, tvInputReminder;
|
||||
|
||||
static class SolidColorMatrix extends ColorMatrix
|
||||
{
|
||||
public SolidColorMatrix(int color)
|
||||
{
|
||||
float matrix[] = { 0.0f, 0.0f, 0.0f, 0.0f, Color.red(color), 0.0f, 0.0f, 0.0f, 0.0f,
|
||||
Color.green(color), 0.0f, 0.0f, 0.0f, 0.0f, Color.blue(color), 0.0f, 0.0f,
|
||||
0.0f, 1.0f, 0 };
|
||||
set(matrix);
|
||||
}
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Factory *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
static EditHabitFragment editSingleHabitFragment(long id)
|
||||
{
|
||||
EditHabitFragment frag = new EditHabitFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("habitId", id);
|
||||
args.putInt("editMode", EDIT_MODE);
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
static EditHabitFragment createHabitFragment()
|
||||
{
|
||||
EditHabitFragment frag = new EditHabitFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putInt("editMode", CREATE_MODE);
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Creation *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState)
|
||||
{
|
||||
View view = inflater.inflate(R.layout.edit_habit, container, false);
|
||||
tvName = (TextView) view.findViewById(R.id.input_name);
|
||||
tvDescription = (TextView) view.findViewById(R.id.input_description);
|
||||
tvFreqNum = (TextView) view.findViewById(R.id.input_freq_num);
|
||||
tvFreqDen = (TextView) view.findViewById(R.id.input_freq_den);
|
||||
tvInputReminder = (TextView) view.findViewById(R.id.input_reminder_time);
|
||||
|
||||
Button buttonSave = (Button) view.findViewById(R.id.buttonSave);
|
||||
Button buttonDiscard = (Button) view.findViewById(R.id.buttonDiscard);
|
||||
|
||||
buttonSave.setOnClickListener(this);
|
||||
buttonDiscard.setOnClickListener(this);
|
||||
tvInputReminder.setOnClickListener(this);
|
||||
|
||||
ImageButton buttonPickColor = (ImageButton) view.findViewById(R.id.button_pick_color);
|
||||
|
||||
Bundle args = getArguments();
|
||||
mode = (Integer) args.get("editMode");
|
||||
|
||||
if(mode == CREATE_MODE)
|
||||
{
|
||||
getDialog().setTitle("Create habit");
|
||||
modified_habit = new Habit();
|
||||
}
|
||||
else if(mode == EDIT_MODE)
|
||||
{
|
||||
originalHabit = Habit.get((Long) args.get("habitId"));
|
||||
modified_habit = new Habit(originalHabit);
|
||||
|
||||
getDialog().setTitle("Edit habit");
|
||||
tvName.append(modified_habit.name);
|
||||
tvDescription.append(modified_habit.description);
|
||||
tvFreqNum.setText(null);
|
||||
tvFreqDen.setText(null);
|
||||
tvFreqNum.append(modified_habit.freq_num.toString());
|
||||
tvFreqDen.append(modified_habit.freq_den.toString());
|
||||
}
|
||||
|
||||
changeColor(modified_habit.color);
|
||||
updateReminder();
|
||||
|
||||
buttonPickColor.setOnClickListener(new OnClickListener()
|
||||
{
|
||||
public void onClick(View view)
|
||||
{
|
||||
ColorPickerDialog picker = ColorPickerDialog.newInstance(
|
||||
R.string.color_picker_default_title,
|
||||
ColorHelper.palette, modified_habit.color, 4, ColorPickerDialog.SIZE_SMALL);
|
||||
|
||||
picker.setOnColorSelectedListener(new ColorPickerSwatch.OnColorSelectedListener()
|
||||
{
|
||||
public void onColorSelected(int color)
|
||||
{
|
||||
modified_habit.color = color;
|
||||
changeColor(color);
|
||||
}
|
||||
});
|
||||
picker.show(getFragmentManager(), "picker");
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void changeColor(Integer color)
|
||||
{
|
||||
// SolidColorMatrix matrix = new SolidColorMatrix(color);
|
||||
// ColorMatrixColorFilter filter = new ColorMatrixColorFilter(matrix);
|
||||
// Drawable background = getActivity().getResources().getDrawable(
|
||||
// R.drawable.apptheme_edit_text_holo_light);
|
||||
// background.setColorFilter(filter);
|
||||
// tvName.setBackgroundDrawable(background);
|
||||
tvName.setTextColor(color);
|
||||
}
|
||||
|
||||
private void updateReminder()
|
||||
{
|
||||
if(modified_habit.reminder_hour != null)
|
||||
{
|
||||
tvInputReminder.setTextColor(Color.BLACK);
|
||||
tvInputReminder.setText(String.format("%02d:%02d", modified_habit.reminder_hour,
|
||||
modified_habit.reminder_min));
|
||||
}
|
||||
else
|
||||
{
|
||||
tvInputReminder.setTextColor(Color.GRAY);
|
||||
tvInputReminder.setText("Off");
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnSavedListener(OnSavedListener onSavedListener)
|
||||
{
|
||||
this.onSavedListener = onSavedListener;
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Callback *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
@Override
|
||||
public void onClick(View v)
|
||||
{
|
||||
int id = v.getId();
|
||||
|
||||
/* Due date spinner */
|
||||
if(id == R.id.input_reminder_time)
|
||||
{
|
||||
int default_hour = 8;
|
||||
int default_min = 0;
|
||||
|
||||
if(modified_habit.reminder_hour != null) {
|
||||
default_hour = modified_habit.reminder_hour;
|
||||
default_min = modified_habit.reminder_min;
|
||||
}
|
||||
|
||||
TimePickerDialog timePicker = TimePickerDialog.newInstance(new OnTimeSetListener()
|
||||
{
|
||||
|
||||
@Override
|
||||
public void onTimeSet(RadialPickerLayout view, int hour, int minute)
|
||||
{
|
||||
modified_habit.reminder_hour = hour;
|
||||
modified_habit.reminder_min = minute;
|
||||
updateReminder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeCleared(RadialPickerLayout view)
|
||||
{
|
||||
modified_habit.reminder_hour = null;
|
||||
modified_habit.reminder_min = null;
|
||||
updateReminder();
|
||||
}
|
||||
}, default_hour, default_min, true);
|
||||
timePicker.show(getFragmentManager(), "timePicker");
|
||||
}
|
||||
|
||||
/* Save button */
|
||||
if(id == R.id.buttonSave)
|
||||
{
|
||||
Command command = null;
|
||||
|
||||
modified_habit.name = tvName.getText().toString().trim();
|
||||
modified_habit.description = tvDescription.getText().toString().trim();
|
||||
modified_habit.freq_num = Integer.parseInt(tvFreqNum.getText().toString());
|
||||
modified_habit.freq_den = Integer.parseInt(tvFreqDen.getText().toString());
|
||||
|
||||
Boolean valid = true;
|
||||
|
||||
if(modified_habit.name.length() == 0)
|
||||
{
|
||||
tvName.setError("Name cannot be blank.");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if(modified_habit.freq_num <= 0)
|
||||
{
|
||||
tvFreqNum.setError("Frequency has to be positive.");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if(!valid)
|
||||
return;
|
||||
|
||||
if(mode == EDIT_MODE)
|
||||
command = originalHabit.new EditCommand(modified_habit);
|
||||
|
||||
if(mode == CREATE_MODE)
|
||||
command = new Habit.CreateCommand(modified_habit);
|
||||
|
||||
if(onSavedListener != null)
|
||||
onSavedListener.onSaved(command);
|
||||
|
||||
dismiss();
|
||||
}
|
||||
|
||||
/* Discard button */
|
||||
if(id == R.id.buttonDiscard)
|
||||
{
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
package org.isoron.uhabits.dialogs;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.isoron.helpers.Command;
|
||||
import org.isoron.helpers.DateHelper;
|
||||
import org.isoron.helpers.DialogHelper.OnSavedListener;
|
||||
import org.isoron.uhabits.MainActivity;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.ShowHabitActivity;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
import android.transition.Explode;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.Display;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.View.OnLongClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.LinearLayout.LayoutParams;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.mobeta.android.dslv.DragSortController;
|
||||
import com.mobeta.android.dslv.DragSortListView;
|
||||
import com.mobeta.android.dslv.DragSortListView.DropListener;
|
||||
|
||||
public class ListHabitsFragment extends Fragment implements OnSavedListener, OnItemClickListener,
|
||||
OnLongClickListener, DropListener, OnClickListener
|
||||
{
|
||||
|
||||
private int tvNameWidth;
|
||||
private int button_count;
|
||||
ListHabitsAdapter adapter;
|
||||
DragSortListView listView;
|
||||
MainActivity mainActivity;
|
||||
TextView tvNameHeader;
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Adapter *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
class ListHabitsAdapter extends BaseAdapter
|
||||
{
|
||||
|
||||
private Context context;
|
||||
private LayoutInflater inflater;
|
||||
private Typeface fontawesome;
|
||||
|
||||
String habits[] = { "wake up early", "work out", "meditate", "take vitamins",
|
||||
"go to school",
|
||||
"cook dinner & lunch" };
|
||||
|
||||
public ListHabitsAdapter(Context context)
|
||||
{
|
||||
this.context = context;
|
||||
|
||||
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
fontawesome = Typeface.createFromAsset(context.getAssets(), "fontawesome-webfont.ttf");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount()
|
||||
{
|
||||
return Habit.getCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position)
|
||||
{
|
||||
return Habit.getByPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position)
|
||||
{
|
||||
return ((Habit) getItem(position)).getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View view, ViewGroup parent)
|
||||
{
|
||||
final Habit habit = (Habit) getItem(position);
|
||||
|
||||
if(view == null || (Long) view.getTag(R.id.KEY_TIMESTAMP) != DateHelper.getStartOfToday())
|
||||
{
|
||||
view = inflater.inflate(R.layout.list_habits_item, null);
|
||||
((TextView) view.findViewById(R.id.tvStar)).setTypeface(fontawesome);
|
||||
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(tvNameWidth,
|
||||
LayoutParams.WRAP_CONTENT, 1);
|
||||
((TextView) view.findViewById(R.id.tvName)).setLayoutParams(params);
|
||||
|
||||
Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
|
||||
.getDefaultDisplay();
|
||||
Point size = new Point();
|
||||
display.getSize(size);
|
||||
|
||||
LinearLayout.LayoutParams llp = new LinearLayout.LayoutParams(
|
||||
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
llp.setMargins(2, 0, 2, 0);
|
||||
|
||||
for (int i = 0; i < button_count; i++)
|
||||
{
|
||||
View check = inflater.inflate(R.layout.list_habits_item_check, null);
|
||||
TextView btCheck = (TextView) check.findViewById(R.id.tvCheck);
|
||||
btCheck.setTypeface(fontawesome);
|
||||
btCheck.setOnLongClickListener(ListHabitsFragment.this);
|
||||
// btCheck.setLayoutParams(llp);
|
||||
((LinearLayout) view.findViewById(R.id.llButtons)).addView(check);
|
||||
}
|
||||
|
||||
// LinearLayout llInner = (LinearLayout) view.findViewById(R.id.llInner);
|
||||
// llInner.setOnClickListener(ListHabitsFragment.this);
|
||||
|
||||
view.setTag(R.id.KEY_TIMESTAMP, DateHelper.getStartOfToday());
|
||||
}
|
||||
|
||||
TextView tvStar = (TextView) view.findViewById(R.id.tvStar);
|
||||
TextView tvName = (TextView) view.findViewById(R.id.tvName);
|
||||
|
||||
|
||||
if(habit == null)
|
||||
{
|
||||
tvName.setText(null);
|
||||
return view;
|
||||
}
|
||||
|
||||
LinearLayout llInner = (LinearLayout) view.findViewById(R.id.llInner);
|
||||
llInner.setTag(R.string.habit_key, habit.getId());
|
||||
|
||||
int inactiveColor = Color.rgb(230, 230, 230);
|
||||
int activeColor = habit.color;
|
||||
|
||||
tvName.setText(habit.name);
|
||||
tvName.setTextColor(activeColor);
|
||||
|
||||
int score = habit.getScore();
|
||||
|
||||
if(score < Habit.HALF_STAR_CUTOFF)
|
||||
{
|
||||
tvStar.setText(context.getString(R.string.fa_star_o));
|
||||
tvStar.setTextColor(inactiveColor);
|
||||
}
|
||||
else if(score < Habit.FULL_STAR_CUTOFF)
|
||||
{
|
||||
tvStar.setText(context.getString(R.string.fa_star_half_o));
|
||||
tvStar.setTextColor(inactiveColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
tvStar.setText(context.getString(R.string.fa_star));
|
||||
tvStar.setTextColor(activeColor);
|
||||
}
|
||||
|
||||
LinearLayout llButtons = (LinearLayout) view.findViewById(R.id.llButtons);
|
||||
int m = llButtons.getChildCount();
|
||||
|
||||
long dateTo = DateHelper.getStartOfDay(DateHelper.getLocalTime());
|
||||
long dateFrom = dateTo - m * DateHelper.millisecondsInOneDay;
|
||||
|
||||
int isChecked[] = habit.getReps(dateFrom, dateTo);
|
||||
|
||||
for (int i = 0; i < m; i++)
|
||||
{
|
||||
|
||||
TextView tvCheck = (TextView) llButtons.getChildAt(i);
|
||||
tvCheck.setTag(R.string.habit_key, habit.getId());
|
||||
tvCheck.setTag(R.string.offset_key, i);
|
||||
|
||||
switch(isChecked[i])
|
||||
{
|
||||
case 2:
|
||||
tvCheck.setText(R.string.fa_check);
|
||||
tvCheck.setTextColor(activeColor);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
tvCheck.setText(R.string.fa_check);
|
||||
tvCheck.setTextColor(inactiveColor);
|
||||
break;
|
||||
|
||||
case 0:
|
||||
tvCheck.setText(R.string.fa_times);
|
||||
tvCheck.setTextColor(inactiveColor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Creation *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState)
|
||||
{
|
||||
View view = inflater.inflate(R.layout.list_habits_fragment, container, false);
|
||||
|
||||
DisplayMetrics dm = getResources().getDisplayMetrics();
|
||||
int width = (int) (dm.widthPixels / dm.density);
|
||||
button_count = (int) ((width - 160) / 42);
|
||||
tvNameWidth = (int) ((width - 30 - button_count * 42) * dm.density);
|
||||
|
||||
tvNameHeader = (TextView) view.findViewById(R.id.tvNameHeader);
|
||||
// updateStarCount();
|
||||
|
||||
adapter = new ListHabitsAdapter(getActivity());
|
||||
listView = (DragSortListView) view.findViewById(R.id.listView);
|
||||
listView.setAdapter(adapter);
|
||||
listView.setOnItemClickListener(this);
|
||||
registerForContextMenu(listView);
|
||||
listView.setDropListener(this);
|
||||
|
||||
DragSortController controller = new DragSortController(listView);
|
||||
controller.setDragHandleId(R.id.tvStar);
|
||||
controller.setRemoveEnabled(false);
|
||||
controller.setSortEnabled(true);
|
||||
controller.setDragInitMode(1);
|
||||
|
||||
listView.setFloatViewManager(controller);
|
||||
listView.setOnTouchListener(controller);
|
||||
listView.setDragEnabled(true);
|
||||
|
||||
GregorianCalendar day = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
|
||||
day.setTimeInMillis(DateHelper.getStartOfDay(DateHelper.getLocalTime()));
|
||||
|
||||
for (int i = 0; i < button_count; i++)
|
||||
{
|
||||
View check = inflater.inflate(R.layout.list_habits_header_check, null);
|
||||
Button btCheck = (Button) check.findViewById(R.id.tvCheck);
|
||||
btCheck.setText(day.getDisplayName(GregorianCalendar.DAY_OF_WEEK,
|
||||
GregorianCalendar.SHORT, Locale.US) + "\n"
|
||||
+ Integer.toString(day.get(GregorianCalendar.DAY_OF_MONTH)));
|
||||
((LinearLayout) view.findViewById(R.id.llButtonsHeader)).addView(check);
|
||||
|
||||
day.add(GregorianCalendar.DAY_OF_MONTH, -1);
|
||||
}
|
||||
|
||||
mainActivity = (MainActivity) getActivity();
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
|
||||
{
|
||||
inflater.inflate(R.menu.show_habits_options, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo)
|
||||
{
|
||||
super.onCreateContextMenu(menu, view, menuInfo);
|
||||
getActivity().getMenuInflater().inflate(R.menu.show_habits_context, menu);
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Callback *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item)
|
||||
{
|
||||
int id = item.getItemId();
|
||||
|
||||
if(id == R.id.action_add)
|
||||
{
|
||||
EditHabitFragment frag = EditHabitFragment.createHabitFragment();
|
||||
frag.setOnSavedListener(this);
|
||||
frag.show(getFragmentManager(), "dialog");
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem menuItem)
|
||||
{
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuItem.getMenuInfo();
|
||||
final int id = menuItem.getItemId();
|
||||
final Habit habit = Habit.get(info.id);
|
||||
|
||||
if(id == R.id.action_edit_habit)
|
||||
{
|
||||
EditHabitFragment frag = EditHabitFragment.editSingleHabitFragment(habit.getId());
|
||||
frag.setOnSavedListener(this);
|
||||
frag.show(getFragmentManager(), "dialog");
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onContextItemSelected(menuItem);
|
||||
}
|
||||
|
||||
long lastLongClick = 0;
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
|
||||
{
|
||||
if(new Date().getTime() - lastLongClick < 1000) return;
|
||||
|
||||
Habit habit = Habit.getByPosition(position);
|
||||
Log.d("ItemClick", Long.toString(id));
|
||||
|
||||
Intent intent = new Intent(getActivity(), ShowHabitActivity.class);
|
||||
intent.setData(Uri.parse("content://org.isoron.uhabits/habit/"
|
||||
+ habit.getId()));
|
||||
getActivity().getWindow().setExitTransition(new Explode());
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaved(Command command)
|
||||
{
|
||||
executeCommand(command);
|
||||
MainActivity.createReminderAlarms(mainActivity);
|
||||
}
|
||||
|
||||
public void notifyDataSetChanged()
|
||||
{
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v)
|
||||
{
|
||||
int id = v.getId();
|
||||
|
||||
if(id == R.id.tvCheck)
|
||||
{
|
||||
lastLongClick = new Date().getTime();
|
||||
Habit habit = Habit.get((Long) v.getTag(R.string.habit_key));
|
||||
int offset = (Integer) v.getTag(R.string.offset_key);
|
||||
long timestamp = DateHelper.getStartOfDay(DateHelper.getLocalTime() - offset
|
||||
* DateHelper.millisecondsInOneDay);
|
||||
|
||||
executeCommand(habit.new ToggleRepetitionCommand(timestamp));
|
||||
|
||||
Vibrator vb = (Vibrator) getActivity().getSystemService(Context.VIBRATOR_SERVICE);
|
||||
vb.vibrate(100);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void executeCommand(Command c)
|
||||
{
|
||||
mainActivity.executeCommand(c, false);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drop(int from, int to)
|
||||
{
|
||||
Habit.reorder(from, to);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v)
|
||||
{
|
||||
}
|
||||
|
||||
void updateStarCount()
|
||||
{
|
||||
Log.d("StarCount", "updating star count");
|
||||
String msg = "";
|
||||
int starCount = Habit.getStarCount();
|
||||
|
||||
if(starCount == 1)
|
||||
msg = String.format("%d star", starCount);
|
||||
else if(starCount > 1)
|
||||
msg = String.format("%d stars", starCount);
|
||||
|
||||
tvNameHeader.setText(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.isoron.uhabits.dialogs;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import org.isoron.helpers.ColorHelper;
|
||||
import org.isoron.helpers.DateHelper;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.ShowHabitActivity;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
import org.isoron.uhabits.views.HabitHistoryView;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Bundle;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.LinearLayout.LayoutParams;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class ShowHabitFragment extends Fragment
|
||||
{
|
||||
protected ShowHabitActivity activity;
|
||||
private Habit habit;
|
||||
|
||||
@Override
|
||||
public void onStart()
|
||||
{
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState)
|
||||
{
|
||||
Log.d("ShowHabitActivity", "Creating view...");
|
||||
|
||||
View view = inflater.inflate(R.layout.show_habit, container, false);
|
||||
activity = (ShowHabitActivity) getActivity();
|
||||
habit = activity.habit;
|
||||
|
||||
int darkerHabitColor = ColorHelper.mixColors(habit.color, Color.BLACK, 0.75f);
|
||||
activity.getWindow().setStatusBarColor(darkerHabitColor);
|
||||
|
||||
TextView tvHistory = (TextView) view.findViewById(R.id.tvHistory);
|
||||
TextView tvOverview = (TextView) view.findViewById(R.id.tvOverview);
|
||||
tvHistory.setTextColor(habit.color);
|
||||
tvOverview.setTextColor(habit.color);
|
||||
|
||||
TextView tvStrength = (TextView) view.findViewById(R.id.tvStrength);
|
||||
tvStrength.setText(String.format("%.2f%%", ((float) habit.getScore() / Habit.MAX_SCORE) * 100));
|
||||
|
||||
LinearLayout llHistory = (LinearLayout) view.findViewById(R.id.llHistory);
|
||||
|
||||
HabitHistoryView hhv = new HabitHistoryView(activity, habit, 40);
|
||||
llHistory.addView(hhv);
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
484
app/src/main/java/org/isoron/uhabits/models/Habit.java
Normal file
@@ -0,0 +1,484 @@
|
||||
package org.isoron.uhabits.models;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.isoron.helpers.ColorHelper;
|
||||
import org.isoron.helpers.Command;
|
||||
import org.isoron.helpers.DateHelper;
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Delete;
|
||||
import com.activeandroid.query.From;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.activeandroid.query.Update;
|
||||
import com.activeandroid.util.SQLiteUtils;
|
||||
|
||||
@Table(name = "Habits")
|
||||
public class Habit extends Model
|
||||
{
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Fields *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
public static final int HALF_STAR_CUTOFF = 5999000;
|
||||
public static final int FULL_STAR_CUTOFF = 12973000;
|
||||
public static final int MAX_SCORE = 19259500;
|
||||
|
||||
@Column(name = "name")
|
||||
public String name;
|
||||
|
||||
@Column(name = "description")
|
||||
public String description;
|
||||
|
||||
@Column(name = "freq_num")
|
||||
public Integer freq_num;
|
||||
|
||||
@Column(name = "freq_den")
|
||||
public Integer freq_den;
|
||||
|
||||
@Column(name = "color")
|
||||
public Integer color;
|
||||
|
||||
@Column(name = "position")
|
||||
public Integer position;
|
||||
|
||||
@Column(name = "reminder_hour")
|
||||
public Integer reminder_hour;
|
||||
|
||||
@Column(name = "reminder_min")
|
||||
public Integer reminder_min;
|
||||
|
||||
@Column(name = "highlight")
|
||||
public Integer highlight;
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Commands *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
public static class CreateCommand extends Command
|
||||
{
|
||||
private Habit model;
|
||||
private Long savedId;
|
||||
|
||||
public CreateCommand(Habit model)
|
||||
{
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute()
|
||||
{
|
||||
Habit savedHabit = new Habit(model);
|
||||
if(savedId == null)
|
||||
{
|
||||
savedHabit.save();
|
||||
savedId = savedHabit.getId();
|
||||
}
|
||||
else
|
||||
{
|
||||
savedHabit.save(savedId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void undo()
|
||||
{
|
||||
Habit.get(savedId).delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getExecuteStringId()
|
||||
{
|
||||
return R.string.toast_habit_created;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getUndoStringId()
|
||||
{
|
||||
return R.string.toast_habit_deleted;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class EditCommand extends Command
|
||||
{
|
||||
private Habit original;
|
||||
private Habit modified;
|
||||
private long savedId;
|
||||
private boolean hasIntervalChanged;
|
||||
|
||||
public EditCommand(Habit modified)
|
||||
{
|
||||
this.savedId = getId();
|
||||
this.modified = new Habit(modified);
|
||||
this.original = new Habit(Habit.this);
|
||||
|
||||
hasIntervalChanged = (this.original.freq_den != this.modified.freq_den
|
||||
|| this.original.freq_num != this.modified.freq_num);
|
||||
}
|
||||
|
||||
public void execute()
|
||||
{
|
||||
Habit habit = Habit.get(savedId);
|
||||
habit.copyAttributes(modified);
|
||||
habit.save();
|
||||
if(hasIntervalChanged)
|
||||
habit.deleteScoresNewerThan(0);
|
||||
}
|
||||
|
||||
public void undo()
|
||||
{
|
||||
Habit habit = Habit.get(savedId);
|
||||
habit.copyAttributes(original);
|
||||
habit.save();
|
||||
if(hasIntervalChanged)
|
||||
habit.deleteScoresNewerThan(0);
|
||||
}
|
||||
|
||||
public Integer getExecuteStringId()
|
||||
{
|
||||
return R.string.toast_habit_changed;
|
||||
}
|
||||
|
||||
public Integer getUndoStringId()
|
||||
{
|
||||
return R.string.toast_habit_changed_back;
|
||||
}
|
||||
}
|
||||
|
||||
public class ToggleRepetitionCommand extends Command
|
||||
{
|
||||
private Long offset;
|
||||
|
||||
public ToggleRepetitionCommand(long offset)
|
||||
{
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute()
|
||||
{
|
||||
toggleRepetition(offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void undo()
|
||||
{
|
||||
execute();
|
||||
}
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Accessors *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
public Habit(Habit model)
|
||||
{
|
||||
copyAttributes(model);
|
||||
}
|
||||
|
||||
public void copyAttributes(Habit model)
|
||||
{
|
||||
this.name = model.name;
|
||||
this.description = model.description;
|
||||
this.freq_num = model.freq_num;
|
||||
this.freq_den = model.freq_den;
|
||||
this.color = model.color;
|
||||
this.position = model.position;
|
||||
this.reminder_hour = model.reminder_hour;
|
||||
this.reminder_min = model.reminder_min;
|
||||
this.highlight = model.highlight;
|
||||
}
|
||||
|
||||
public Habit()
|
||||
{
|
||||
this.color = ColorHelper.palette[11];
|
||||
this.position = Habit.getCount();
|
||||
this.highlight = 0;
|
||||
}
|
||||
|
||||
public static Habit get(Long id)
|
||||
{
|
||||
return Habit.load(Habit.class, id);
|
||||
}
|
||||
|
||||
public void save(Long id)
|
||||
{
|
||||
save();
|
||||
Habit.updateId(getId(), id);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public static void updateId(long oldId, long newId)
|
||||
{
|
||||
SQLiteUtils.execSql(String.format(
|
||||
"update Habits set Id = %d where Id = %d", newId, oldId));
|
||||
}
|
||||
|
||||
protected static From select()
|
||||
{
|
||||
return new Select().from(Habit.class).orderBy("position");
|
||||
}
|
||||
|
||||
public static int getCount()
|
||||
{
|
||||
return select().count();
|
||||
}
|
||||
|
||||
public static Habit getByPosition(int position)
|
||||
{
|
||||
return select().offset(position).executeSingle();
|
||||
}
|
||||
|
||||
public static java.util.List<Habit> getHabits()
|
||||
{
|
||||
return select().execute();
|
||||
}
|
||||
|
||||
public static java.util.List<Habit> getHighlightedHabits()
|
||||
{
|
||||
return select().where("highlight = 1").orderBy("reminder_hour desc, reminder_min desc")
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static java.util.List<Habit> getHabitsWithReminder()
|
||||
{
|
||||
return select().where("reminder_hour is not null").execute();
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Repetitions *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
protected From selectReps()
|
||||
{
|
||||
return new Select().from(Repetition.class).where("habit = ?", getId())
|
||||
.orderBy("timestamp");
|
||||
}
|
||||
|
||||
protected From selectRepsFromTo(long timeFrom, long timeTo)
|
||||
{
|
||||
return selectReps().and("timestamp >= ?", timeFrom).and(
|
||||
"timestamp <= ?", timeTo);
|
||||
}
|
||||
|
||||
public boolean hasRep(long timestamp)
|
||||
{
|
||||
int count = selectReps().where("timestamp = ?", timestamp).count();
|
||||
return (count > 0);
|
||||
}
|
||||
|
||||
public boolean hasRepToday()
|
||||
{
|
||||
return hasRep(DateHelper.getStartOfToday());
|
||||
}
|
||||
|
||||
public void deleteReps(long timestamp)
|
||||
{
|
||||
new Delete().from(Repetition.class).where("habit = ?", getId())
|
||||
.and("timestamp = ?", timestamp).execute();
|
||||
}
|
||||
|
||||
public int[] getReps(long timeFrom, long timeTo)
|
||||
{
|
||||
long timeFromExtended = timeFrom - (long)(freq_den) * DateHelper.millisecondsInOneDay;
|
||||
List<Repetition> reps = selectRepsFromTo(timeFromExtended, timeTo).execute();
|
||||
|
||||
int nDaysExtended = (int) ((timeTo - timeFromExtended) / DateHelper.millisecondsInOneDay);
|
||||
int checkExtended[] = new int[nDaysExtended + 1];
|
||||
|
||||
int nDays = (int) ((timeTo - timeFrom) / DateHelper.millisecondsInOneDay);
|
||||
|
||||
// mark explicit checks
|
||||
for (Repetition rep : reps)
|
||||
{
|
||||
int offset = (int) ((rep.timestamp - timeFrom) / DateHelper.millisecondsInOneDay);
|
||||
checkExtended[nDays - offset] = 2;
|
||||
}
|
||||
|
||||
// marks implicit checks
|
||||
for(int i=0; i<nDays; i++)
|
||||
{
|
||||
int counter = 0;
|
||||
|
||||
for(int j=0; j<freq_den; j++)
|
||||
if(checkExtended[i+j] == 2) counter++;
|
||||
|
||||
if(counter >= freq_num)
|
||||
checkExtended[i] = Math.max(checkExtended[i], 1);
|
||||
}
|
||||
|
||||
int check[] = new int[nDays + 1];
|
||||
for(int i=0; i<nDays+1; i++)
|
||||
check[i] = checkExtended[i];
|
||||
|
||||
return check;
|
||||
}
|
||||
|
||||
public boolean hasImplicitRepToday()
|
||||
{
|
||||
long today = DateHelper.getStartOfToday();
|
||||
int reps[] = getReps(today - DateHelper.millisecondsInOneDay, today);
|
||||
return (reps[0] > 0);
|
||||
}
|
||||
|
||||
public Repetition getOldestRep()
|
||||
{
|
||||
return (Repetition) selectReps().limit(1).executeSingle();
|
||||
}
|
||||
|
||||
public void toggleRepetition(long timestamp)
|
||||
{
|
||||
if(hasRep(timestamp))
|
||||
{
|
||||
deleteReps(timestamp);
|
||||
}
|
||||
else
|
||||
{
|
||||
Repetition rep = new Repetition();
|
||||
rep.habit = this;
|
||||
rep.timestamp = timestamp;
|
||||
rep.save();
|
||||
}
|
||||
|
||||
deleteScoresNewerThan(timestamp);
|
||||
}
|
||||
|
||||
public void toggleRepetitionToday()
|
||||
{
|
||||
toggleRepetition(DateHelper.getStartOfToday());
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Scoring *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
public Score getNewestScore()
|
||||
{
|
||||
return new Select().from(Score.class).where("habit = ?", getId())
|
||||
.orderBy("timestamp desc").limit(1).executeSingle();
|
||||
}
|
||||
|
||||
public void deleteScoresNewerThan(long timestamp)
|
||||
{
|
||||
new Delete().from(Score.class).where("habit = ?", getId())
|
||||
.and("timestamp >= ?", timestamp).execute();
|
||||
}
|
||||
|
||||
public Integer getScore()
|
||||
{
|
||||
int beginningScore;
|
||||
long beginningTime;
|
||||
|
||||
long today = DateHelper.getStartOfDay(DateHelper.getLocalTime());
|
||||
long day = DateHelper.millisecondsInOneDay;
|
||||
|
||||
double freq = ((double) freq_num) / freq_den;
|
||||
double multiplier = Math.pow(0.5, 1.0 / (14.0 / freq - 1));
|
||||
|
||||
Score newestScore = getNewestScore();
|
||||
if(newestScore == null)
|
||||
{
|
||||
Repetition oldestRep = getOldestRep();
|
||||
if(oldestRep == null)
|
||||
return 0;
|
||||
beginningTime = oldestRep.timestamp;
|
||||
beginningScore = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
beginningTime = newestScore.timestamp + day;
|
||||
beginningScore = newestScore.score;
|
||||
}
|
||||
|
||||
long nDays = (today - beginningTime) / day;
|
||||
if(nDays < 0)
|
||||
return newestScore.score;
|
||||
|
||||
int reps[] = getReps(beginningTime, today);
|
||||
|
||||
int lastScore = beginningScore;
|
||||
for (int i = 0; i < reps.length; i++)
|
||||
{
|
||||
Score s = new Score();
|
||||
s.habit = this;
|
||||
s.timestamp = beginningTime + day * i;
|
||||
s.score = (int) (lastScore * multiplier);
|
||||
if(reps[reps.length-i-1] == 2) {
|
||||
s.score += 1000000;
|
||||
s.score = Math.min(s.score, 19259500);
|
||||
}
|
||||
s.save();
|
||||
|
||||
lastScore = s.score;
|
||||
}
|
||||
|
||||
return lastScore;
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Ordering *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
public static void reorder(int from, int to)
|
||||
{
|
||||
if(from == to)
|
||||
return;
|
||||
|
||||
Habit h = Habit.getByPosition(from);
|
||||
if(to < from)
|
||||
new Update(Habit.class).set("position = position + 1")
|
||||
.where("position >= ? and position < ?", to, from)
|
||||
.execute();
|
||||
else
|
||||
new Update(Habit.class).set("position = position - 1")
|
||||
.where("position > ? and position <= ?", from, to)
|
||||
.execute();
|
||||
|
||||
h.position = to;
|
||||
h.save();
|
||||
}
|
||||
|
||||
public static void rebuildOrder()
|
||||
{
|
||||
List<Habit> habits = select().execute();
|
||||
int i = 0;
|
||||
for (Habit h : habits)
|
||||
{
|
||||
h.position = i++;
|
||||
h.save();
|
||||
}
|
||||
}
|
||||
|
||||
public static void roundTimestamps()
|
||||
{
|
||||
List<Repetition> reps = new Select().from(Repetition.class).execute();
|
||||
for (Repetition r : reps)
|
||||
{
|
||||
r.timestamp = DateHelper.getStartOfDay(r.timestamp);
|
||||
r.save();
|
||||
}
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Statistics *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
public static int getStarCount()
|
||||
{
|
||||
String args[] = {};
|
||||
return SQLiteUtils.intQuery(
|
||||
"select count(*) from (select score, max(timestamp) from " +
|
||||
"score group by habit) as scores where scores.score >= "
|
||||
+ Integer.toString(12973000), args);
|
||||
|
||||
}
|
||||
}
|
||||
15
app/src/main/java/org/isoron/uhabits/models/Repetition.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package org.isoron.uhabits.models;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
|
||||
@Table(name = "Repetitions")
|
||||
public class Repetition extends Model {
|
||||
|
||||
@Column(name = "habit")
|
||||
public Habit habit;
|
||||
|
||||
@Column(name = "timestamp")
|
||||
public Long timestamp;
|
||||
}
|
||||
18
app/src/main/java/org/isoron/uhabits/models/Score.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package org.isoron.uhabits.models;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
|
||||
@Table(name = "Score")
|
||||
public class Score extends Model
|
||||
{
|
||||
@Column(name = "habit")
|
||||
public Habit habit;
|
||||
|
||||
@Column(name = "timestamp")
|
||||
public Long timestamp;
|
||||
|
||||
@Column(name = "score")
|
||||
public Integer score;
|
||||
}
|
||||
236
app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java
Normal file
@@ -0,0 +1,236 @@
|
||||
package org.isoron.uhabits.views;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import org.isoron.helpers.ColorHelper;
|
||||
import org.isoron.helpers.DateHelper;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Align;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.support.v4.view.MotionEventCompat;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
public class HabitHistoryView extends View
|
||||
{
|
||||
|
||||
private Habit habit;
|
||||
private int reps[];
|
||||
|
||||
private Context context;
|
||||
private Paint pSquareBg, pSquareFg, pTextHeader;
|
||||
|
||||
private int width, height;
|
||||
private int squareSize, squareSpacing;
|
||||
private int nColumns, offsetWeeks;
|
||||
|
||||
private int colorPrimary, colorPrimaryBrighter, grey;
|
||||
|
||||
public HabitHistoryView(Context context, Habit habit, int squareSize)
|
||||
{
|
||||
super(context);
|
||||
this.habit = habit;
|
||||
this.context = context;
|
||||
this.squareSize = squareSize;
|
||||
|
||||
Typeface fontawesome = Typeface.createFromAsset(context.getAssets(),
|
||||
"fontawesome-webfont.ttf");
|
||||
|
||||
colorPrimary = habit.color;
|
||||
colorPrimaryBrighter = ColorHelper.mixColors(colorPrimary, Color.WHITE, 0.5f);
|
||||
grey = Color.rgb(230, 230, 230);
|
||||
squareSpacing = 2;
|
||||
|
||||
pTextHeader = new Paint();
|
||||
pTextHeader.setColor(Color.LTGRAY);
|
||||
pTextHeader.setTextAlign(Align.LEFT);
|
||||
pTextHeader.setTextSize(squareSize * 0.5f);
|
||||
pTextHeader.setAntiAlias(true);
|
||||
|
||||
pSquareBg = new Paint();
|
||||
pSquareBg.setColor(habit.color);
|
||||
|
||||
pSquareFg = new Paint();
|
||||
pSquareFg.setColor(Color.WHITE);
|
||||
pSquareFg.setAntiAlias(true);
|
||||
// pSquareFg.setTypeface(fontawesome);
|
||||
pSquareFg.setTextSize(squareSize * 0.5f);
|
||||
pSquareFg.setTextAlign(Align.CENTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
setMeasuredDimension(getMeasuredWidth(), 8 * squareSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh)
|
||||
{
|
||||
width = w;
|
||||
height = h;
|
||||
nColumns = (w / squareSize) - 1;
|
||||
fetchReps();
|
||||
}
|
||||
|
||||
private void fetchReps()
|
||||
{
|
||||
Calendar currentDate = new GregorianCalendar();
|
||||
currentDate.add(Calendar.DAY_OF_YEAR, -offsetWeeks * 7);
|
||||
int dayOfWeek = currentDate.get(Calendar.DAY_OF_WEEK) % 7;
|
||||
|
||||
long dateTo = DateHelper.getStartOfToday();
|
||||
for (int i = 0; i < 7 - dayOfWeek; i++)
|
||||
dateTo += DateHelper.millisecondsInOneDay;
|
||||
|
||||
for (int i = 0; i < offsetWeeks * 7; i++)
|
||||
dateTo -= DateHelper.millisecondsInOneDay;
|
||||
|
||||
long dateFrom = dateTo;
|
||||
for (int i = 0; i < nColumns * 7; i++)
|
||||
dateFrom -= DateHelper.millisecondsInOneDay;
|
||||
|
||||
reps = habit.getReps(dateFrom, dateTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
|
||||
Rect square = new Rect(0, 0, squareSize - squareSpacing, squareSize - squareSpacing);
|
||||
|
||||
Calendar currentDate = new GregorianCalendar();
|
||||
currentDate.add(Calendar.DAY_OF_YEAR, -(offsetWeeks-1) * 7);
|
||||
|
||||
int nDays = nColumns * 7;
|
||||
int todayWeekday = new GregorianCalendar().get(Calendar.DAY_OF_WEEK) % 7;
|
||||
|
||||
currentDate.add(Calendar.DAY_OF_YEAR, -nDays);
|
||||
|
||||
SimpleDateFormat dfMonth = new SimpleDateFormat("MMM");
|
||||
SimpleDateFormat dfYear = new SimpleDateFormat("yyyy");
|
||||
|
||||
String previousMonth = "";
|
||||
String previousYear = "";
|
||||
|
||||
int colors[] = { grey, colorPrimaryBrighter, colorPrimary };
|
||||
String markers[] = { context.getString(R.string.fa_times),
|
||||
context.getString(R.string.fa_check), context.getString(R.string.fa_check) };
|
||||
|
||||
float squareTextOffset = pSquareFg.getFontSpacing() * 0.4f;
|
||||
float headerTextOffset = pTextHeader.getFontSpacing() * 0.3f;
|
||||
boolean justPrintedYear = false;
|
||||
|
||||
int k = nDays;
|
||||
for (int i = 0; i < nColumns; i++)
|
||||
{
|
||||
String month = dfMonth.format(currentDate.getTime());
|
||||
String year = dfYear.format(currentDate.getTime());
|
||||
|
||||
if(!month.equals(previousMonth))
|
||||
{
|
||||
int offset = 0;
|
||||
if(justPrintedYear)
|
||||
offset += squareSize;
|
||||
|
||||
canvas.drawText(month, square.left + offset, square.bottom - headerTextOffset,
|
||||
pTextHeader);
|
||||
previousMonth = month;
|
||||
justPrintedYear = false;
|
||||
}
|
||||
else if(!year.equals(previousYear))
|
||||
{
|
||||
canvas.drawText(year, square.left, square.bottom - headerTextOffset, pTextHeader);
|
||||
previousYear = year;
|
||||
justPrintedYear = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
justPrintedYear = false;
|
||||
}
|
||||
|
||||
|
||||
square.offset(0, squareSize);
|
||||
|
||||
for (int j = 0; j < 7; j++)
|
||||
{
|
||||
if(!(i == nColumns - 1 && offsetWeeks == 0 && j > todayWeekday))
|
||||
{
|
||||
pSquareBg.setColor(colors[reps[k]]);
|
||||
canvas.drawRect(square, pSquareBg);
|
||||
// canvas.drawText(markers[reps[k]], square.centerX(), square.centerY()
|
||||
// + squareTextOffset, pSquareFg);
|
||||
canvas.drawText(Integer.toString(currentDate.get(Calendar.DAY_OF_MONTH)),
|
||||
square.centerX(), square.centerY() + squareTextOffset, pSquareFg);
|
||||
}
|
||||
|
||||
currentDate.add(Calendar.DAY_OF_MONTH, 1);
|
||||
square.offset(0, squareSize);
|
||||
k--;
|
||||
}
|
||||
|
||||
square.offset(squareSize, -8 * squareSize);
|
||||
}
|
||||
|
||||
String wdays[] = { "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri" };
|
||||
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
square.offset(0, squareSize);
|
||||
canvas.drawText(wdays[i], square.left + headerTextOffset, square.bottom
|
||||
- headerTextOffset, pTextHeader);
|
||||
}
|
||||
}
|
||||
|
||||
private Float prevX, prevY;
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event)
|
||||
{
|
||||
int action = event.getAction();
|
||||
|
||||
int pointerIndex = MotionEventCompat.getActionIndex(event);
|
||||
final float x = MotionEventCompat.getX(event, pointerIndex);
|
||||
final float y = MotionEventCompat.getY(event, pointerIndex);
|
||||
|
||||
if(action == MotionEvent.ACTION_DOWN)
|
||||
{
|
||||
prevX = x;
|
||||
prevY = y;
|
||||
}
|
||||
|
||||
if(action == MotionEvent.ACTION_MOVE)
|
||||
{
|
||||
float dx = x - prevX;
|
||||
float dy = y - prevY;
|
||||
|
||||
int newOffsetWeeks = offsetWeeks + (int) (dx / squareSize);
|
||||
newOffsetWeeks = Math.max(0, newOffsetWeeks);
|
||||
|
||||
if(newOffsetWeeks != offsetWeeks)
|
||||
{
|
||||
prevX = x;
|
||||
prevY = y;
|
||||
offsetWeeks = newOffsetWeeks;
|
||||
|
||||
fetchReps();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
23
app/src/main/res/color/date_picker_selector.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2013 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:state_pressed="true" android:color="@color/darker_blue"/>
|
||||
<item android:state_pressed="false" android:state_selected="true" android:color="@color/blue"/>
|
||||
<item android:state_pressed="false" android:state_selected="false"
|
||||
android:color="@color/date_picker_text_normal"/>
|
||||
|
||||
</selector>
|
||||
22
app/src/main/res/color/date_picker_year_selector.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2013 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:state_pressed="true" android:color="@color/darker_blue"/>
|
||||
<item android:state_pressed="false" android:state_selected="false"
|
||||
android:color="@color/date_picker_text_normal"/>
|
||||
|
||||
</selector>
|
||||
|
After Width: | Height: | Size: 196 B |
|
After Width: | Height: | Size: 187 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 290 B |
BIN
app/src/main/res/drawable-hdpi/ic_action_add.png
Normal file
|
After Width: | Height: | Size: 181 B |
BIN
app/src/main/res/drawable-hdpi/ic_action_add_dark.png
Normal file
|
After Width: | Height: | Size: 167 B |
BIN
app/src/main/res/drawable-hdpi/ic_action_check.png
Normal file
|
After Width: | Height: | Size: 432 B |
BIN
app/src/main/res/drawable-hdpi/ic_action_dismiss.png
Normal file
|
After Width: | Height: | Size: 244 B |
BIN
app/src/main/res/drawable-hdpi/ic_action_pick_color.png
Normal file
|
After Width: | Height: | Size: 552 B |
BIN
app/src/main/res/drawable-hdpi/ic_action_snooze.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 618 B |
|
After Width: | Height: | Size: 164 B |
|
After Width: | Height: | Size: 151 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 256 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_add.png
Normal file
|
After Width: | Height: | Size: 134 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_add_dark.png
Normal file
|
After Width: | Height: | Size: 129 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_check.png
Normal file
|
After Width: | Height: | Size: 337 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_dismiss.png
Normal file
|
After Width: | Height: | Size: 192 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_pick_color.png
Normal file
|
After Width: | Height: | Size: 394 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_snooze.png
Normal file
|
After Width: | Height: | Size: 694 B |
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 435 B |
22
app/src/main/res/drawable-v21/habits_item_check_normal.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item>
|
||||
<shape android:shape="oval" >
|
||||
<gradient
|
||||
android:endColor="#00000000"
|
||||
android:gradientRadius="50%p"
|
||||
android:startColor="#50000000"
|
||||
android:type="radial" />
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:bottom="5dp"
|
||||
android:left="2dp"
|
||||
android:right="4dp"
|
||||
android:top="1dp">
|
||||
<shape android:shape="oval" >
|
||||
<solid android:color="#ffffff" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
</layer-list>
|
||||
22
app/src/main/res/drawable-v21/habits_item_check_pressed.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item>
|
||||
<shape android:shape="oval" >
|
||||
<gradient
|
||||
android:endColor="#00000000"
|
||||
android:gradientRadius="50%p"
|
||||
android:startColor="#50000000"
|
||||
android:type="radial" />
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:bottom="5dp"
|
||||
android:left="2dp"
|
||||
android:right="4dp"
|
||||
android:top="1dp">
|
||||
<shape android:shape="oval" >
|
||||
<solid android:color="#20000000" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 231 B |
|
After Width: | Height: | Size: 220 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 426 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_add.png
Normal file
|
After Width: | Height: | Size: 201 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_add_dark.png
Normal file
|
After Width: | Height: | Size: 189 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_check.png
Normal file
|
After Width: | Height: | Size: 488 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_dismiss.png
Normal file
|
After Width: | Height: | Size: 289 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_pick_color.png
Normal file
|
After Width: | Height: | Size: 716 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_snooze.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.2 KiB |