Android Studio file structure

This commit is contained in:
2015-03-15 08:11:19 -04:00
parent 11d6bbd6f6
commit 1ff3c1c857
190 changed files with 1124 additions and 1036 deletions

93
app/app.iml Normal file
View 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
View 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

Binary file not shown.

3
app/lint.xml Normal file
View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<lint>
</lint>

View 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>

Binary file not shown.

View File

@@ -0,0 +1,2 @@
alter table habits add column reminder_hour integer;
alter table habits add column reminder_min integer;

View File

@@ -0,0 +1 @@
alter table habits add column highlight integer not null default 0;

View 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}
}
}

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}
};
}

View File

@@ -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);
}
}
}

View 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);
}
}

View File

@@ -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();
}
}

File diff suppressed because it is too large Load Diff

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View 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;
}
}

View 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;
}
}

View 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();
}
}

View 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);
}
}

View 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();
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View 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);
}
}

View 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;
}

View 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;
}

View 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;
}
}

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Some files were not shown because too many files have changed in this diff Show More