Rename android module to uhabits-android

This commit is contained in:
2017-05-25 07:59:54 -04:00
parent 96f620455f
commit c20ca3921f
678 changed files with 1 additions and 1 deletions

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 android.app.Activity;
import android.app.Dialog;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatDialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ProgressBar;
import com.android.colorpicker.ColorPickerSwatch.OnColorSelectedListener;
import org.isoron.uhabits.R;
/**
* A dialog which takes in as input an array of palette and creates a palette allowing the user to
* select a specific color swatch, which invokes a listener.
*/
public class ColorPickerDialog extends AppCompatDialogFragment implements OnColorSelectedListener {
public static final int SIZE_LARGE = 1;
public static final int SIZE_SMALL = 2;
protected AlertDialog mAlertDialog;
protected static final String KEY_TITLE_ID = "title_id";
protected static final String KEY_COLORS = "palette";
protected static final String KEY_SELECTED_COLOR = "selected_color";
protected static final String KEY_COLUMNS = "columns";
protected static final String KEY_SIZE = "size";
protected int mTitleResId = R.string.color_picker_default_title;
protected int[] mColors = null;
protected int mSelectedColor;
protected int mColumns;
protected int mSize;
private ColorPickerPalette mPalette;
private ProgressBar mProgress;
protected OnColorSelectedListener mListener;
public ColorPickerDialog() {
// Empty constructor required for dialog fragments.
}
public static ColorPickerDialog newInstance(int titleResId, int[] colors, int selectedColor,
int columns, int size) {
ColorPickerDialog ret = new ColorPickerDialog();
ret.initialize(titleResId, colors, selectedColor, columns, size);
return ret;
}
public void initialize(int titleResId, int[] colors, int selectedColor, int columns, int size) {
setArguments(titleResId, columns, size);
setColors(colors, selectedColor);
}
public void setArguments(int titleResId, int columns, int size) {
Bundle bundle = new Bundle();
bundle.putInt(KEY_TITLE_ID, titleResId);
bundle.putInt(KEY_COLUMNS, columns);
bundle.putInt(KEY_SIZE, size);
setArguments(bundle);
}
public void setOnColorSelectedListener(OnColorSelectedListener listener) {
mListener = listener;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mTitleResId = getArguments().getInt(KEY_TITLE_ID);
mColumns = getArguments().getInt(KEY_COLUMNS);
mSize = getArguments().getInt(KEY_SIZE);
}
if (savedInstanceState != null) {
mColors = savedInstanceState.getIntArray(KEY_COLORS);
mSelectedColor = (Integer) savedInstanceState.getSerializable(KEY_SELECTED_COLOR);
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity activity = getActivity();
View view = LayoutInflater.from(getActivity()).inflate(R.layout.color_picker_dialog, null);
mProgress = (ProgressBar) view.findViewById(android.R.id.progress);
mPalette = (ColorPickerPalette) view.findViewById(R.id.color_picker);
mPalette.init(mSize, mColumns, this);
if (mColors != null) {
showPaletteView();
}
mAlertDialog = new AlertDialog.Builder(activity)
.setTitle(mTitleResId)
.setView(view)
.create();
return mAlertDialog;
}
@Override
public void onColorSelected(int color) {
if (mListener != null) {
mListener.onColorSelected(color);
}
if (getTargetFragment() instanceof OnColorSelectedListener) {
final OnColorSelectedListener listener =
(OnColorSelectedListener) getTargetFragment();
listener.onColorSelected(color);
}
if (color != mSelectedColor) {
mSelectedColor = color;
// Redraw palette to show checkmark on newly selected color before dismissing.
mPalette.drawPalette(mColors, mSelectedColor);
}
dismiss();
}
public void showPaletteView() {
if (mProgress != null && mPalette != null) {
mProgress.setVisibility(View.GONE);
refreshPalette();
mPalette.setVisibility(View.VISIBLE);
}
}
public void showProgressBarView() {
if (mProgress != null && mPalette != null) {
mProgress.setVisibility(View.VISIBLE);
mPalette.setVisibility(View.GONE);
}
}
public void setColors(int[] colors, int selectedColor) {
if (mColors != colors || mSelectedColor != selectedColor) {
mColors = colors;
mSelectedColor = selectedColor;
refreshPalette();
}
}
public void setColors(int[] colors) {
if (mColors != colors) {
mColors = colors;
refreshPalette();
}
}
public void setSelectedColor(int color) {
if (mSelectedColor != color) {
mSelectedColor = color;
refreshPalette();
}
}
private void refreshPalette() {
if (mPalette != null && mColors != null) {
mPalette.drawPalette(mColors, mSelectedColor);
}
}
public int[] getColors() {
return mColors;
}
public int getSelectedColor() {
return mSelectedColor;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putIntArray(KEY_COLORS, mColors);
outState.putSerializable(KEY_SELECTED_COLOR, mSelectedColor);
}
}

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,484 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.datetimepicker.date;
import 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 int mCurrentView = UNINITIALIZED;
private int mWeekStart = mCalendar.getFirstDayOfWeek();
private int mMinYear = DEFAULT_START_YEAR;
private int mMaxYear = DEFAULT_END_YEAR;
private HapticFeedbackController mHapticFeedbackController;
private boolean mDelayAnimation = true;
// Accessibility strings.
private String mDayPickerDescription;
private String mSelectDay;
private String mYearPickerDescription;
private String mSelectYear;
/**
* The callback used to indicate the user is done filling in the date.
*/
public interface OnDateSetListener {
/**
* @param view The view associated with this listener.
* @param year The year that was set.
* @param monthOfYear The month that was set (0-11) for compatibility
* with {@link java.util.Calendar}.
* @param dayOfMonth The day of the month that was set.
*/
void onDateSet(DatePickerDialog dialog, int year, int monthOfYear, int dayOfMonth);
void onDateCleared(DatePickerDialog dialog);
}
/**
* The callback used to notify other date picker components of a change in selected date.
*/
public interface OnDateChangedListener {
public void onDateChanged();
}
public DatePickerDialog() {
// Empty constructor required for dialog fragment.
}
/**
* @param callBack How the parent is notified that the date is set.
* @param year The initial year of the dialog.
* @param monthOfYear The initial month of the dialog.
* @param dayOfMonth The initial day of the dialog.
*/
public static DatePickerDialog newInstance(OnDateSetListener callBack, int year,
int monthOfYear,
int dayOfMonth) {
DatePickerDialog ret = new DatePickerDialog();
ret.initialize(callBack, year, monthOfYear, dayOfMonth);
return ret;
}
public void initialize(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) {
mCallBack = callBack;
mCalendar.set(Calendar.YEAR, year);
mCalendar.set(Calendar.MONTH, monthOfYear);
mCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Activity activity = getActivity();
activity.getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
if (savedInstanceState != null) {
mCalendar.set(Calendar.YEAR, savedInstanceState.getInt(KEY_SELECTED_YEAR));
mCalendar.set(Calendar.MONTH, savedInstanceState.getInt(KEY_SELECTED_MONTH));
mCalendar.set(Calendar.DAY_OF_MONTH, savedInstanceState.getInt(KEY_SELECTED_DAY));
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(KEY_SELECTED_YEAR, mCalendar.get(Calendar.YEAR));
outState.putInt(KEY_SELECTED_MONTH, mCalendar.get(Calendar.MONTH));
outState.putInt(KEY_SELECTED_DAY, mCalendar.get(Calendar.DAY_OF_MONTH));
outState.putInt(KEY_WEEK_START, mWeekStart);
outState.putInt(KEY_YEAR_START, mMinYear);
outState.putInt(KEY_YEAR_END, mMaxYear);
outState.putInt(KEY_CURRENT_VIEW, mCurrentView);
int listPosition = -1;
if (mCurrentView == MONTH_AND_DAY_VIEW) {
listPosition = mDayPickerView.getMostVisiblePosition();
} else if (mCurrentView == YEAR_VIEW) {
listPosition = mYearPickerView.getFirstVisiblePosition();
outState.putInt(KEY_LIST_POSITION_OFFSET, mYearPickerView.getFirstPositionOffset());
}
outState.putInt(KEY_LIST_POSITION, listPosition);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d(TAG, "onCreateView: ");
getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
View view = inflater.inflate(R.layout.date_picker_dialog, null);
mDayOfWeekView = (TextView) view.findViewById(R.id.date_picker_header);
mMonthAndDayView = (LinearLayout) view.findViewById(R.id.date_picker_month_and_day);
mMonthAndDayView.setOnClickListener(this);
mSelectedMonthTextView = (TextView) view.findViewById(R.id.date_picker_month);
mSelectedDayTextView = (TextView) view.findViewById(R.id.date_picker_day);
mYearView = (TextView) view.findViewById(R.id.date_picker_year);
mYearView.setOnClickListener(this);
int listPosition = -1;
int listPositionOffset = 0;
int currentView = MONTH_AND_DAY_VIEW;
if (savedInstanceState != null) {
mWeekStart = savedInstanceState.getInt(KEY_WEEK_START);
mMinYear = savedInstanceState.getInt(KEY_YEAR_START);
mMaxYear = savedInstanceState.getInt(KEY_YEAR_END);
currentView = savedInstanceState.getInt(KEY_CURRENT_VIEW);
listPosition = savedInstanceState.getInt(KEY_LIST_POSITION);
listPositionOffset = savedInstanceState.getInt(KEY_LIST_POSITION_OFFSET);
}
final Activity activity = getActivity();
mDayPickerView = new SimpleDayPickerView(activity, this);
mYearPickerView = new YearPickerView(activity, this);
Resources res = getResources();
mDayPickerDescription = res.getString(R.string.day_picker_description);
mSelectDay = res.getString(R.string.select_day);
mYearPickerDescription = res.getString(R.string.year_picker_description);
mSelectYear = res.getString(R.string.select_year);
mAnimator = (AccessibleDateAnimator) view.findViewById(R.id.animator);
mAnimator.addView(mDayPickerView);
mAnimator.addView(mYearPickerView);
mAnimator.setDateMillis(mCalendar.getTimeInMillis());
// TODO: Replace with animation decided upon by the design team.
Animation animation = new AlphaAnimation(0.0f, 1.0f);
animation.setDuration(ANIMATION_DURATION);
mAnimator.setInAnimation(animation);
// TODO: Replace with animation decided upon by the design team.
Animation animation2 = new AlphaAnimation(1.0f, 0.0f);
animation2.setDuration(ANIMATION_DURATION);
mAnimator.setOutAnimation(animation2);
Button mDoneButton = (Button) view.findViewById(R.id.done);
mDoneButton.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{
tryVibrate();
if (mCallBack != null)
{
mCallBack.onDateSet(DatePickerDialog.this, mCalendar.get(Calendar.YEAR),
mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH));
}
dismiss();
}
});
Button mClearButton = (Button) view.findViewById(R.id.clear);
mClearButton.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{
tryVibrate();
if (mCallBack != null)
{
mCallBack.onDateCleared(DatePickerDialog.this);
}
dismiss();
}
});
updateDisplay(false);
setCurrentView(currentView);
if (listPosition != -1) {
if (currentView == MONTH_AND_DAY_VIEW) {
mDayPickerView.postSetSelection(listPosition);
} else if (currentView == YEAR_VIEW) {
mYearPickerView.postSetSelectionFromTop(listPosition, listPositionOffset);
}
}
mHapticFeedbackController = new HapticFeedbackController(activity);
return view;
}
@Override
public void onResume() {
super.onResume();
mHapticFeedbackController.start();
}
@Override
public void onPause() {
super.onPause();
mHapticFeedbackController.stop();
}
private void setCurrentView(final int viewIndex) {
long millis = mCalendar.getTimeInMillis();
switch (viewIndex) {
case MONTH_AND_DAY_VIEW:
ObjectAnimator pulseAnimator = Utils.getPulseAnimator(mMonthAndDayView, 0.9f,
1.05f);
if (mDelayAnimation) {
pulseAnimator.setStartDelay(ANIMATION_DELAY);
mDelayAnimation = false;
}
mDayPickerView.onDateChanged();
if (mCurrentView != viewIndex) {
mMonthAndDayView.setSelected(true);
mYearView.setSelected(false);
mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW);
mCurrentView = viewIndex;
}
pulseAnimator.start();
int flags = DateUtils.FORMAT_SHOW_DATE;
String dayString = DateUtils.formatDateTime(getActivity(), millis, flags);
mAnimator.setContentDescription(mDayPickerDescription+": "+dayString);
Utils.tryAccessibilityAnnounce(mAnimator, mSelectDay);
break;
case YEAR_VIEW:
pulseAnimator = Utils.getPulseAnimator(mYearView, 0.85f, 1.1f);
if (mDelayAnimation) {
pulseAnimator.setStartDelay(ANIMATION_DELAY);
mDelayAnimation = false;
}
mYearPickerView.onDateChanged();
if (mCurrentView != viewIndex) {
mMonthAndDayView.setSelected(false);
mYearView.setSelected(true);
mAnimator.setDisplayedChild(YEAR_VIEW);
mCurrentView = viewIndex;
}
pulseAnimator.start();
CharSequence yearString = YEAR_FORMAT.format(millis);
mAnimator.setContentDescription(mYearPickerDescription+": "+yearString);
Utils.tryAccessibilityAnnounce(mAnimator, mSelectYear);
break;
}
}
private void updateDisplay(boolean announce) {
if (mDayOfWeekView != null) {
mDayOfWeekView.setText(mCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG,
Locale.getDefault()).toUpperCase(Locale.getDefault()));
}
mSelectedMonthTextView.setText(mCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT,
Locale.getDefault()).toUpperCase(Locale.getDefault()));
mSelectedDayTextView.setText(DAY_FORMAT.format(mCalendar.getTime()));
mYearView.setText(YEAR_FORMAT.format(mCalendar.getTime()));
// Accessibility.
long millis = mCalendar.getTimeInMillis();
mAnimator.setDateMillis(millis);
int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR;
String monthAndDayText = DateUtils.formatDateTime(getActivity(), millis, flags);
mMonthAndDayView.setContentDescription(monthAndDayText);
if (announce) {
flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
String fullDateText = DateUtils.formatDateTime(getActivity(), millis, flags);
Utils.tryAccessibilityAnnounce(mAnimator, fullDateText);
}
}
public void setFirstDayOfWeek(int startOfWeek) {
if (startOfWeek < Calendar.SUNDAY || startOfWeek > Calendar.SATURDAY) {
throw new IllegalArgumentException("Value must be between Calendar.SUNDAY and " +
"Calendar.SATURDAY");
}
mWeekStart = startOfWeek;
if (mDayPickerView != null) {
mDayPickerView.onChange();
}
}
public void setYearRange(int startYear, int endYear) {
if (endYear <= startYear) {
throw new IllegalArgumentException("Year end must be larger than year start");
}
mMinYear = startYear;
mMaxYear = endYear;
if (mDayPickerView != null) {
mDayPickerView.onChange();
}
}
public void setOnDateSetListener(OnDateSetListener listener) {
mCallBack = listener;
}
// If the newly selected month / year does not contain the currently selected day number,
// change the selected day number to the last day of the selected month or year.
// e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
// e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
private void adjustDayInMonthIfNeeded(int month, int year) {
int day = mCalendar.get(Calendar.DAY_OF_MONTH);
int daysInMonth = Utils.getDaysInMonth(month, year);
if (day > daysInMonth) {
mCalendar.set(Calendar.DAY_OF_MONTH, daysInMonth);
}
}
@Override
public void onClick(View v) {
tryVibrate();
if (v.getId() == R.id.date_picker_year) {
setCurrentView(YEAR_VIEW);
} else if (v.getId() == R.id.date_picker_month_and_day) {
setCurrentView(MONTH_AND_DAY_VIEW);
}
}
@Override
public void onYearSelected(int year) {
adjustDayInMonthIfNeeded(mCalendar.get(Calendar.MONTH), year);
mCalendar.set(Calendar.YEAR, year);
updatePickers();
setCurrentView(MONTH_AND_DAY_VIEW);
updateDisplay(true);
}
@Override
public void onDayOfMonthSelected(int year, int month, int day) {
mCalendar.set(Calendar.YEAR, year);
mCalendar.set(Calendar.MONTH, month);
mCalendar.set(Calendar.DAY_OF_MONTH, day);
updatePickers();
updateDisplay(true);
}
private void updatePickers() {
Iterator<OnDateChangedListener> iterator = mListeners.iterator();
while (iterator.hasNext()) {
iterator.next().onDateChanged();
}
}
@Override
public CalendarDay getSelectedDay() {
return new CalendarDay(mCalendar);
}
@Override
public int getMinYear() {
return mMinYear;
}
@Override
public int getMaxYear() {
return mMaxYear;
}
@Override
public int getFirstDayOfWeek() {
return mWeekStart;
}
@Override
public void registerOnDateChangedListener(OnDateChangedListener listener) {
mListeners.add(listener);
}
@Override
public void unregisterOnDateChangedListener(OnDateChangedListener listener) {
mListeners.remove(listener);
}
@Override
public void tryVibrate() {
mHapticFeedbackController.tryVibrate();
}
}

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

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import android.content.*;
import org.isoron.uhabits.activities.habits.list.model.*;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.io.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.notifications.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.sync.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.*;
import org.isoron.uhabits.widgets.*;
import dagger.*;
@AppScope
@Component(modules = {
AppModule.class, AndroidTaskRunner.class, SQLModelFactory.class
})
public interface AppComponent
{
CommandRunner getCommandRunner();
@AppContext
Context getContext();
CreateHabitCommandFactory getCreateHabitCommandFactory();
EditHabitCommandFactory getEditHabitCommandFactory();
GenericImporter getGenericImporter();
HabitCardListCache getHabitCardListCache();
HabitList getHabitList();
HabitLogger getHabitsLogger();
IntentFactory getIntentFactory();
IntentParser getIntentParser();
ModelFactory getModelFactory();
NotificationTray getNotificationTray();
PendingIntentFactory getPendingIntentFactory();
Preferences getPreferences();
ReminderScheduler getReminderScheduler();
SyncManager getSyncManager();
TaskRunner getTaskRunner();
WidgetPreferences getWidgetPreferences();
WidgetUpdater getWidgetUpdater();
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import java.lang.annotation.*;
import javax.inject.*;
@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface AppContext
{
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import android.content.*;
import dagger.*;
@Module
public class AppModule
{
private final Context context;
public AppModule(@AppContext Context context)
{
this.context = context;
}
@Provides
@AppContext
Context getContext()
{
return context;
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import javax.inject.*;
@Scope
public @interface AppScope {}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import android.support.annotation.*;
import android.util.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import java.text.*;
import java.util.*;
import javax.inject.*;
@AppScope
public class HabitLogger
{
@Inject
public HabitLogger()
{
}
public void logReminderScheduled(@NonNull Habit habit,
@NonNull Long reminderTime)
{
int min = Math.min(3, habit.getName().length());
String name = habit.getName().substring(0, min);
DateFormat df = DateFormats.getBackupDateFormat();
String time = df.format(new Date(reminderTime));
Log.i("ReminderHelper",
String.format("Setting alarm (%s): %s", time, name));
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import android.app.*;
import android.content.*;
import com.activeandroid.*;
import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.notifications.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.*;
import org.isoron.uhabits.widgets.*;
import java.io.*;
/**
* The Android application for Loop Habit Tracker.
*/
public class HabitsApplication extends Application
{
private Context context;
private static AppComponent component;
private WidgetUpdater widgetUpdater;
private ReminderScheduler reminderScheduler;
private NotificationTray notificationTray;
public AppComponent getComponent()
{
return component;
}
public static void setComponent(AppComponent component)
{
HabitsApplication.component = component;
}
public static boolean isTestMode()
{
try
{
Class.forName ("org.isoron.uhabits.BaseAndroidTest");
return true;
}
catch (final ClassNotFoundException e)
{
return false;
}
}
@Override
public void onCreate()
{
super.onCreate();
context = this;
component = DaggerAppComponent
.builder()
.appModule(new AppModule(context))
.build();
if (isTestMode())
{
File db = DatabaseUtils.getDatabaseFile(context);
if (db.exists()) db.delete();
}
try
{
DatabaseUtils.initializeActiveAndroid(context);
}
catch (InvalidDatabaseVersionException e)
{
File db = DatabaseUtils.getDatabaseFile(context);
db.renameTo(new File(db.getAbsolutePath() + ".invalid"));
DatabaseUtils.initializeActiveAndroid(context);
}
widgetUpdater = component.getWidgetUpdater();
widgetUpdater.startListening();
reminderScheduler = component.getReminderScheduler();
reminderScheduler.startListening();
notificationTray = component.getNotificationTray();
notificationTray.startListening();
Preferences prefs = component.getPreferences();
prefs.initialize();
prefs.updateLastAppVersion();
TaskRunner taskRunner = component.getTaskRunner();
taskRunner.execute(() -> {
reminderScheduler.scheduleAll();
widgetUpdater.updateWidgets();
});
}
@Override
public void onTerminate()
{
context = null;
ActiveAndroid.dispose();
reminderScheduler.stopListening();
widgetUpdater.stopListening();
notificationTray.stopListening();
super.onTerminate();
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import android.app.backup.BackupAgentHelper;
import android.app.backup.FileBackupHelper;
import android.app.backup.SharedPreferencesBackupHelper;
/**
* An Android BackupAgentHelper customized for this application.
*/
public class HabitsBackupAgent extends BackupAgentHelper
{
@Override
public void onCreate()
{
addHelper("preferences",
new SharedPreferencesBackupHelper(this, "preferences"));
addHelper("database",
new FileBackupHelper(this, "../databases/uhabits.db"));
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.common.dialogs.*;
import dagger.*;
@ActivityScope
@Component(modules = { ActivityModule.class },
dependencies = { AppComponent.class })
public interface ActivityComponent
{
BaseActivity getActivity();
ColorPickerDialogFactory getColorPickerDialogFactory();
ThemeSwitcher getThemeSwitcher();
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import java.lang.annotation.*;
import javax.inject.*;
@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityContext
{
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import android.content.*;
import dagger.*;
@Module
public class ActivityModule
{
private BaseActivity activity;
public ActivityModule(BaseActivity activity)
{
this.activity = activity;
}
@Provides
public BaseActivity getActivity()
{
return activity;
}
@Provides
@ActivityContext
public Context getContext()
{
return activity;
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import javax.inject.*;
/**
* Scope used by objects that live as long as the activity is alive.
*/
@Scope
public @interface ActivityScope { }

View File

@@ -0,0 +1,138 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import android.content.*;
import android.os.*;
import android.support.annotation.*;
import android.support.v7.app.*;
import android.view.*;
import org.isoron.uhabits.*;
import static android.R.anim.fade_in;
import static android.R.anim.fade_out;
/**
* Base class for all activities in the application.
* <p>
* This class delegates the responsibilities of an Android activity to other
* classes. For example, callbacks related to menus are forwarded to a {@link
* BaseMenu}, while callbacks related to activity results are forwarded to a
* {@link BaseScreen}.
* <p>
* A BaseActivity also installs an {@link java.lang.Thread.UncaughtExceptionHandler}
* to the main thread. By default, this handler is an instance of
* BaseExceptionHandler, which logs the exception to the disk before the application
* crashes. To the default handler, you should override the method
* getExceptionHandler.
*/
abstract public class BaseActivity extends AppCompatActivity
{
@Nullable
private BaseMenu baseMenu;
@Nullable
private BaseScreen screen;
private ActivityComponent component;
public ActivityComponent getComponent()
{
return component;
}
@Override
public boolean onCreateOptionsMenu(@Nullable Menu menu)
{
if (menu == null) return true;
if (baseMenu == null) return true;
baseMenu.onCreate(getMenuInflater(), menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@Nullable MenuItem item)
{
if (item == null) return false;
if (baseMenu == null) return false;
return baseMenu.onItemSelected(item);
}
public void restartWithFade(Class<?> cls)
{
new Handler().postDelayed(() ->
{
finish();
overridePendingTransition(fade_in, fade_out);
startActivity(new Intent(this, cls));
}, 500); // HACK: Let the menu disappear first
}
public void setBaseMenu(@Nullable BaseMenu baseMenu)
{
this.baseMenu = baseMenu;
}
public void setScreen(@Nullable BaseScreen screen)
{
this.screen = screen;
}
public void showDialog(AppCompatDialogFragment dialog, String tag)
{
dialog.show(getSupportFragmentManager(), tag);
}
public void showDialog(AppCompatDialog dialog)
{
dialog.show();
}
@Override
protected void onActivityResult(int request, int result, Intent data)
{
if (screen == null) super.onActivityResult(request, result, data);
else screen.onResult(request, result, data);
}
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Thread.setDefaultUncaughtExceptionHandler(getExceptionHandler());
HabitsApplication app = (HabitsApplication) getApplicationContext();
component = DaggerActivityComponent
.builder()
.activityModule(new ActivityModule(this))
.appComponent(app.getComponent())
.build();
component.getThemeSwitcher().apply();
}
protected Thread.UncaughtExceptionHandler getExceptionHandler()
{
return new BaseExceptionHandler(this);
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import android.support.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.*;
public class BaseExceptionHandler implements Thread.UncaughtExceptionHandler
{
@Nullable
private Thread.UncaughtExceptionHandler originalHandler;
@NonNull
private BaseActivity activity;
public BaseExceptionHandler(@NonNull BaseActivity activity)
{
this.activity = activity;
originalHandler = Thread.getDefaultUncaughtExceptionHandler();
}
@Override
public void uncaughtException(@Nullable Thread thread,
@Nullable Throwable ex)
{
if (ex == null) return;
try
{
ex.printStackTrace();
new BaseSystem(activity).dumpBugReportToFile();
}
catch (Exception e)
{
e.printStackTrace();
}
if (ex.getCause() instanceof InconsistentDatabaseException)
{
HabitsApplication app = (HabitsApplication) activity.getApplication();
HabitList habits = app.getComponent().getHabitList();
habits.repair();
System.exit(0);
}
if (originalHandler != null)
originalHandler.uncaughtException(thread, ex);
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import android.support.annotation.*;
import android.view.*;
import javax.annotation.*;
/**
* Base class for all the menus in the application.
* <p>
* This class receives from BaseActivity all callbacks related to menus, such as
* menu creation and click events. It also handles some implementation details
* of creating menus in Android, such as inflating the resources.
*/
public abstract class BaseMenu
{
@NonNull
private final BaseActivity activity;
public BaseMenu(@NonNull BaseActivity activity)
{
this.activity = activity;
}
/**
* Declare that the menu has changed, and should be recreated.
*/
public void invalidate()
{
activity.invalidateOptionsMenu();
}
/**
* Called when the menu is first displayed.
* <p>
* The given menu is already inflated and ready to receive items. The
* application should override this method and add items to the menu here.
*
* @param menu the menu that is being created.
*/
public void onCreate(@NonNull Menu menu)
{
}
/**
* Called when the menu is first displayed.
* <p>
* This method should not be overridden. The application should override
* the methods onCreate(Menu) and getMenuResourceId instead.
*
* @param inflater a menu inflater, for creating the menu
* @param menu the menu that is being created.
*/
public void onCreate(@NonNull MenuInflater inflater,
@NonNull Menu menu)
{
menu.clear();
inflater.inflate(getMenuResourceId(), menu);
onCreate(menu);
}
/**
* Called whenever an item on the menu is selected.
*
* @param item the item that was selected.
* @return true if the event was consumed, or false otherwise
*/
public boolean onItemSelected(@NonNull MenuItem item)
{
return false;
}
/**
* Returns the id of the resource that should be used to inflate this menu.
*
* @return id of the menu resource.
*/
@Resource
protected abstract int getMenuResourceId();
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import android.content.*;
import android.support.annotation.*;
import android.support.v4.content.res.*;
import android.support.v7.widget.Toolbar;
import android.view.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.utils.*;
import static android.os.Build.VERSION.*;
import static android.os.Build.VERSION_CODES.*;
/**
* Base class for all root views in the application.
* <p>
* A root view is an Android view that is directly attached to an activity. This
* view usually includes a toolbar and a progress bar. This abstract class hides
* some of the complexity of setting these things up, for every version of
* Android.
*/
public abstract class BaseRootView extends FrameLayout
{
@NonNull
private final Context context;
private final ThemeSwitcher themeSwitcher;
public BaseRootView(@NonNull Context context)
{
super(context);
this.context = context;
BaseActivity activity = (BaseActivity) context;
themeSwitcher = activity.getComponent().getThemeSwitcher();
}
public boolean getDisplayHomeAsUp()
{
return false;
}
@NonNull
public abstract Toolbar getToolbar();
public int getToolbarColor()
{
if (SDK_INT < LOLLIPOP && !themeSwitcher.isNightMode())
{
return ResourcesCompat.getColor(context.getResources(),
R.color.grey_900, context.getTheme());
}
StyledResources res = new StyledResources(context);
return res.getColor(R.attr.colorPrimary);
}
protected void initToolbar()
{
if (SDK_INT >= LOLLIPOP)
{
getToolbar().setElevation(InterfaceUtils.dpToPixels(context, 2));
View view = findViewById(R.id.toolbarShadow);
if (view != null) view.setVisibility(GONE);
view = findViewById(R.id.headerShadow);
if(view != null) view.setVisibility(GONE);
}
}
}

View File

@@ -0,0 +1,307 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import android.content.*;
import android.graphics.*;
import android.graphics.drawable.*;
import android.net.*;
import android.os.*;
import android.support.annotation.*;
import android.support.design.widget.*;
import android.support.v4.content.res.*;
import android.support.v7.app.*;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.Toolbar;
import android.view.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.utils.*;
import java.io.*;
import static android.os.Build.VERSION.*;
import static android.os.Build.VERSION_CODES.*;
import static android.support.v4.content.FileProvider.*;
/**
* Base class for all screens in the application.
* <p>
* Screens are responsible for deciding what root views and what menus should be
* attached to the main window. They are also responsible for showing other
* screens and for receiving their results.
*/
public class BaseScreen
{
public static final int REQUEST_CREATE_DOCUMENT = 1;
protected BaseActivity activity;
@Nullable
private BaseRootView rootView;
@Nullable
private BaseSelectionMenu selectionMenu;
protected Snackbar snackbar;
public BaseScreen(@NonNull BaseActivity activity)
{
this.activity = activity;
}
@Deprecated
public static void setupActionBarColor(@NonNull AppCompatActivity activity,
int color)
{
Toolbar toolbar = (Toolbar) activity.findViewById(R.id.toolbar);
if (toolbar == null) return;
activity.setSupportActionBar(toolbar);
ActionBar actionBar = activity.getSupportActionBar();
if (actionBar == null) return;
actionBar.setDisplayHomeAsUpEnabled(true);
ColorDrawable drawable = new ColorDrawable(color);
actionBar.setBackgroundDrawable(drawable);
if (SDK_INT >= LOLLIPOP)
{
int darkerColor = ColorUtils.mixColors(color, Color.BLACK, 0.75f);
activity.getWindow().setStatusBarColor(darkerColor);
toolbar.setElevation(InterfaceUtils.dpToPixels(activity, 2));
View view = activity.findViewById(R.id.toolbarShadow);
if (view != null) view.setVisibility(View.GONE);
view = activity.findViewById(R.id.headerShadow);
if (view != null) view.setVisibility(View.GONE);
}
}
@Deprecated
public static int getDefaultActionBarColor(Context context)
{
if (SDK_INT < LOLLIPOP)
{
return ResourcesCompat.getColor(context.getResources(),
R.color.grey_900, context.getTheme());
}
else
{
StyledResources res = new StyledResources(context);
return res.getColor(R.attr.colorPrimary);
}
}
/**
* Notifies the screen that its contents should be updated.
*/
public void invalidate()
{
if (rootView == null) return;
rootView.invalidate();
}
public void invalidateToolbar()
{
if (rootView == null) return;
activity.runOnUiThread(() -> {
Toolbar toolbar = rootView.getToolbar();
activity.setSupportActionBar(toolbar);
ActionBar actionBar = activity.getSupportActionBar();
if (actionBar == null) return;
actionBar.setDisplayHomeAsUpEnabled(rootView.getDisplayHomeAsUp());
int color = rootView.getToolbarColor();
setActionBarColor(actionBar, color);
setStatusBarColor(color);
});
}
/**
* Called when another Activity has finished, and has returned some result.
*
* @param requestCode the request code originally supplied to {@link
* android.app.Activity#startActivityForResult(Intent,
* int, Bundle)}.
* @param resultCode the result code sent by the other activity.
* @param data an Intent containing extra data sent by the other
* activity.
* @see {@link android.app.Activity#onActivityResult(int, int, Intent)}
*/
public void onResult(int requestCode, int resultCode, Intent data)
{
}
/**
* Sets the menu to be shown by this screen.
* <p>
* This menu will be visible if when there is no active selection operation.
* If the provided menu is null, then no menu will be shown.
*
* @param menu the menu to be shown.
*/
public void setMenu(@Nullable BaseMenu menu)
{
activity.setBaseMenu(menu);
}
/**
* Sets the root view for this screen.
*
* @param rootView the root view for this screen.
*/
public void setRootView(@Nullable BaseRootView rootView)
{
this.rootView = rootView;
activity.setContentView(rootView);
if (rootView == null) return;
invalidateToolbar();
}
/**
* Sets the menu to be shown when a selection is active on the screen.
*
* @param menu the menu to be shown during a selection
*/
public void setSelectionMenu(@Nullable BaseSelectionMenu menu)
{
this.selectionMenu = menu;
}
/**
* Shows a message on the screen.
*
* @param stringId the string resource id for this message.
*/
public void showMessage(@StringRes Integer stringId)
{
if (stringId == null || rootView == null) return;
if (snackbar == null)
{
snackbar = Snackbar.make(rootView, stringId, Snackbar.LENGTH_SHORT);
int tvId = android.support.design.R.id.snackbar_text;
TextView tv = (TextView) snackbar.getView().findViewById(tvId);
tv.setTextColor(Color.WHITE);
}
else snackbar.setText(stringId);
snackbar.show();
}
public void showSendEmailScreen(@StringRes int toId,
@StringRes int subjectId,
String content)
{
String to = activity.getString(toId);
String subject = activity.getString(subjectId);
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setType("message/rfc822");
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{ to });
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
intent.putExtra(Intent.EXTRA_TEXT, content);
activity.startActivity(intent);
}
public void showSendFileScreen(@NonNull String archiveFilename)
{
File file = new File(archiveFilename);
Uri fileUri = getUriForFile(activity, "org.isoron.uhabits", file);
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setType("application/zip");
intent.putExtra(Intent.EXTRA_STREAM, fileUri);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
activity.startActivity(intent);
}
/**
* Instructs the screen to start a selection.
* <p>
* If a selection menu was provided, this menu will be shown instead of the
* regular one.
*/
public void startSelection()
{
activity.startSupportActionMode(new ActionModeWrapper());
}
private void setActionBarColor(@NonNull ActionBar actionBar, int color)
{
ColorDrawable drawable = new ColorDrawable(color);
actionBar.setBackgroundDrawable(drawable);
}
private void setStatusBarColor(int baseColor)
{
if (SDK_INT < LOLLIPOP) return;
int darkerColor = ColorUtils.mixColors(baseColor, Color.BLACK, 0.75f);
activity.getWindow().setStatusBarColor(darkerColor);
}
private class ActionModeWrapper implements ActionMode.Callback
{
@Override
public boolean onActionItemClicked(@Nullable ActionMode mode,
@Nullable MenuItem item)
{
if (item == null || selectionMenu == null) return false;
return selectionMenu.onItemClicked(item);
}
@Override
public boolean onCreateActionMode(@Nullable ActionMode mode,
@Nullable Menu menu)
{
if (selectionMenu == null) return false;
if (mode == null || menu == null) return false;
selectionMenu.onCreate(activity.getMenuInflater(), mode, menu);
return true;
}
@Override
public void onDestroyActionMode(@Nullable ActionMode mode)
{
if (selectionMenu == null) return;
selectionMenu.onFinish();
}
@Override
public boolean onPrepareActionMode(@Nullable ActionMode mode,
@Nullable Menu menu)
{
if (selectionMenu == null || menu == null) return false;
return selectionMenu.onPrepare(menu);
}
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import android.support.annotation.*;
import android.support.v7.view.ActionMode;
import android.view.*;
/**
* Base class for all the selection menus in the application.
* <p>
* A selection menu is a menu that appears when the screen starts a selection
* operation. It contains actions that modify the selected items, such as delete
* or archive. Since it replaces the toolbar, it also has a title.
* <p>
* This class hides many implementation details of creating such menus in
* Android. The interface is supposed to look very similar to {@link BaseMenu},
* with a few additional methods, such as finishing the selection operation.
* Internally, it uses an {@link ActionMode}.
*/
public abstract class BaseSelectionMenu
{
@Nullable
private ActionMode actionMode;
/**
* Finishes the selection operation.
*/
public void finish()
{
if (actionMode != null) actionMode.finish();
}
/**
* Declare that the menu has changed, and should be recreated.
*/
public void invalidate()
{
if (actionMode != null) actionMode.invalidate();
}
/**
* Called when the menu is first displayed.
* <p>
* This method should not be overridden. The application should override
* the methods onCreate(Menu) and getMenuResourceId instead.
*
* @param inflater a menu inflater, for creating the menu
* @param mode the action mode associated with this menu.
* @param menu the menu that is being created.
*/
public void onCreate(@NonNull MenuInflater inflater,
@NonNull ActionMode mode,
@NonNull Menu menu)
{
this.actionMode = mode;
inflater.inflate(getResourceId(), menu);
onCreate(menu);
}
/**
* Called when the selection operation is about to finish.
*/
public void onFinish()
{
}
/**
* Called whenever an item on the menu is selected.
*
* @param item the item that was selected.
* @return true if the event was consumed, or false otherwise
*/
public boolean onItemClicked(@NonNull MenuItem item)
{
return false;
}
/**
* Called whenever the menu is invalidated.
*
* @param menu the menu to be refreshed
* @return true if the menu has changes, false otherwise
*/
public boolean onPrepare(@NonNull Menu menu)
{
return false;
}
/**
* Sets the title of the selection menu.
*
* @param title the new title.
*/
public void setTitle(String title)
{
if (actionMode != null) actionMode.setTitle(title);
}
protected abstract int getResourceId();
/**
* Called when the menu is first created.
*
* @param menu the menu being created
*/
protected void onCreate(@NonNull Menu menu)
{
}
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import android.content.*;
import android.os.*;
import android.support.annotation.*;
import android.view.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.utils.*;
import java.io.*;
import java.lang.Process;
import java.util.*;
import javax.inject.*;
/**
* Base class for all systems class in the application.
* <p>
* Classes derived from BaseSystem are responsible for handling events and
* sending requests to the Android operating system. Examples include capturing
* a bug report, obtaining device information, or requesting runtime
* permissions.
*/
@ActivityScope
public class BaseSystem
{
private Context context;
@Inject
public BaseSystem(@ActivityContext Context context)
{
this.context = context;
}
/**
* Captures a bug report and saves it to a file in the SD card.
* <p>
* The contents of the file are generated by the method {@link
* #getBugReport()}. The file is saved in the apps's external private
* storage.
*
* @return the generated file.
* @throws IOException when I/O errors occur.
*/
@NonNull
public File dumpBugReportToFile() throws IOException
{
String date =
DateFormats.getBackupDateFormat().format(DateUtils.getLocalTime());
if (context == null) throw new RuntimeException(
"application context should not be null");
File dir = FileUtils.getFilesDir(context, "Logs");
if (dir == null) throw new IOException("log dir should not be null");
File logFile =
new File(String.format("%s/Log %s.txt", dir.getPath(), date));
FileWriter output = new FileWriter(logFile);
output.write(getBugReport());
output.close();
return logFile;
}
/**
* Captures and returns a bug report.
* <p>
* The bug report contains some device information and the logcat.
*
* @return a String containing the bug report.
* @throws IOException when any I/O error occur.
*/
@NonNull
public String getBugReport() throws IOException
{
String logcat = getLogcat();
String deviceInfo = getDeviceInfo();
String log = "---------- BUG REPORT BEGINS ----------\n";
log += deviceInfo + "\n" + logcat;
log += "---------- BUG REPORT ENDS ------------\n";
return log;
}
public String getLogcat() throws IOException
{
int maxLineCount = 250;
StringBuilder builder = new StringBuilder();
String[] command = new String[]{ "logcat", "-d" };
Process process = Runtime.getRuntime().exec(command);
InputStreamReader in = new InputStreamReader(process.getInputStream());
BufferedReader bufferedReader = new BufferedReader(in);
LinkedList<String> log = new LinkedList<>();
String line;
while ((line = bufferedReader.readLine()) != null)
{
log.addLast(line);
if (log.size() > maxLineCount) log.removeFirst();
}
for (String l : log)
{
builder.append(l);
builder.append('\n');
}
return builder.toString();
}
private String getDeviceInfo()
{
if (context == null) return "null context\n";
WindowManager wm =
(WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
return
String.format("App Version Name: %s\n", BuildConfig.VERSION_NAME) +
String.format("App Version Code: %s\n", BuildConfig.VERSION_CODE) +
String.format("OS Version: %s (%s)\n",
System.getProperty("os.version"), Build.VERSION.INCREMENTAL) +
String.format("OS API Level: %s\n", Build.VERSION.SDK) +
String.format("Device: %s\n", Build.DEVICE) +
String.format("Model (Product): %s (%s)\n", Build.MODEL,
Build.PRODUCT) +
String.format("Manufacturer: %s\n", Build.MANUFACTURER) +
String.format("Other tags: %s\n", Build.TAGS) +
String.format("Screen Width: %s\n",
wm.getDefaultDisplay().getWidth()) +
String.format("Screen Height: %s\n",
wm.getDefaultDisplay().getHeight()) +
String.format("External storage state: %s\n\n",
Environment.getExternalStorageState());
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import android.support.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.preferences.*;
import javax.inject.*;
@ActivityScope
public class ThemeSwitcher
{
public static final int THEME_DARK = 1;
public static final int THEME_LIGHT = 0;
@NonNull
private final BaseActivity activity;
private Preferences preferences;
@Inject
public ThemeSwitcher(@NonNull BaseActivity activity,
@NonNull Preferences preferences)
{
this.activity = activity;
this.preferences = preferences;
}
public void apply()
{
switch (getTheme())
{
case THEME_DARK:
applyDarkTheme();
break;
case THEME_LIGHT:
default:
applyLightTheme();
break;
}
}
public boolean isNightMode()
{
return getTheme() == THEME_DARK;
}
public void refreshTheme()
{
}
public void toggleNightMode()
{
if (isNightMode()) setTheme(THEME_LIGHT);
else setTheme(THEME_DARK);
}
private void applyDarkTheme()
{
if (preferences.isPureBlackEnabled())
activity.setTheme(R.style.AppBaseThemeDark_PureBlack);
else activity.setTheme(R.style.AppBaseThemeDark);
}
private void applyLightTheme()
{
activity.setTheme(R.style.AppBaseTheme);
}
private int getTheme()
{
return preferences.getTheme();
}
public void setTheme(int theme)
{
preferences.setTheme(theme);
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.about;
import android.os.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.intents.*;
/**
* Activity that allows the user to see information about the app itself.
* Display current version, link to Google Play and list of contributors.
*/
public class AboutActivity extends BaseActivity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
AboutRootView rootView = new AboutRootView(this, new IntentFactory());
BaseScreen screen = new BaseScreen(this);
screen.setRootView(rootView);
setScreen(screen);
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.about;
import android.content.*;
import android.support.annotation.*;
import android.support.v7.widget.Toolbar;
import android.widget.*;
import org.isoron.uhabits.BuildConfig;
import org.isoron.uhabits.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.utils.*;
import butterknife.*;
public class AboutRootView extends BaseRootView
{
@BindView(R.id.tvVersion)
TextView tvVersion;
@BindView(R.id.tvRate)
TextView tvRate;
@BindView(R.id.tvFeedback)
TextView tvFeedback;
@BindView(R.id.tvSource)
TextView tvSource;
@BindView(R.id.toolbar)
Toolbar toolbar;
int developerCountdown = 10;
@Nullable
Preferences prefs;
private final IntentFactory intents;
public AboutRootView(Context context, IntentFactory intents)
{
super(context);
this.intents = intents;
addView(inflate(getContext(), R.layout.about, null));
ButterKnife.bind(this);
tvVersion.setText(
String.format(getResources().getString(R.string.version_n),
BuildConfig.VERSION_NAME));
if (context.getApplicationContext() instanceof HabitsApplication)
{
HabitsApplication app =
(HabitsApplication) context.getApplicationContext();
prefs = app.getComponent().getPreferences();
}
}
@Override
public boolean getDisplayHomeAsUp()
{
return true;
}
@NonNull
@Override
public Toolbar getToolbar()
{
return toolbar;
}
@Override
public int getToolbarColor()
{
StyledResources res = new StyledResources(getContext());
if (!res.getBoolean(R.attr.useHabitColorAsPrimary))
return super.getToolbarColor();
return res.getColor(R.attr.aboutScreenColor);
}
@OnClick(R.id.tvFeedback)
public void onClickFeedback()
{
Intent intent = intents.sendFeedback(getContext());
getContext().startActivity(intent);
}
@OnClick(R.id.tvVersion)
public void onClickIcon()
{
developerCountdown--;
if (developerCountdown <= 0)
{
if (prefs == null) return;
prefs.setDeveloper(true);
String text = "You are now a developer";
Toast.makeText(getContext(), text, Toast.LENGTH_LONG).show();
}
}
@OnClick(R.id.tvRate)
public void onClickRate()
{
Intent intent = intents.rateApp(getContext());
getContext().startActivity(intent);
}
@OnClick(R.id.tvSource)
public void onClickSource()
{
Intent intent = intents.viewSourceCode(getContext());
getContext().startActivity(intent);
}
@OnClick(R.id.tvTranslate)
public void onClickTranslate()
{
Intent intent = intents.helpTranslate(getContext());
getContext().startActivity(intent);
}
@Override
protected void initToolbar()
{
super.initToolbar();
toolbar.setTitle(getResources().getString(R.string.about));
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides activity that shows information about the app.
*/
package org.isoron.uhabits.activities.about;

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs;
import org.isoron.uhabits.utils.*;
/**
* Dialog that allows the user to choose a color.
*/
public class ColorPickerDialog extends com.android.colorpicker.ColorPickerDialog
{
public void setListener(OnColorSelectedListener listener)
{
super.setOnColorSelectedListener(c -> {
c = ColorUtils.colorToPaletteIndex(getContext(), c);
listener.onColorSelected(c);
});
}
public interface OnColorSelectedListener
{
void onColorSelected(int color);
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs;
import android.content.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.utils.*;
import javax.inject.*;
@ActivityScope
public class ColorPickerDialogFactory
{
private final Context context;
@Inject
public ColorPickerDialogFactory(@ActivityContext Context context)
{
this.context = context;
}
public ColorPickerDialog create(int paletteColor)
{
ColorPickerDialog dialog = new ColorPickerDialog();
StyledResources res = new StyledResources(context);
int color = ColorUtils.getColor(context, paletteColor);
dialog.initialize(R.string.color_picker_default_title, res.getPalette(),
color, 4, com.android.colorpicker.ColorPickerDialog.SIZE_SMALL);
return dialog;
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs;
import android.content.*;
import android.support.v7.app.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.*;
import butterknife.*;
/**
* Dialog that asks the user confirmation before executing a delete operation.
*/
@AutoFactory(allowSubclasses = true)
public class ConfirmDeleteDialog extends AlertDialog
{
@BindString(R.string.delete_habits_message)
protected String question;
@BindString(android.R.string.yes)
protected String yes;
@BindString(android.R.string.no)
protected String no;
protected ConfirmDeleteDialog(@Provided @ActivityContext Context context,
Callback callback)
{
super(context);
ButterKnife.bind(this);
setTitle(R.string.delete_habits);
setMessage(question);
setButton(BUTTON_POSITIVE, yes, (dialog, which) -> callback.run());
setButton(BUTTON_NEGATIVE, no, (dialog, which) -> {});
}
public interface Callback
{
void run();
}
}

View File

@@ -0,0 +1,179 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs;
import android.app.*;
import android.content.*;
import android.os.*;
import android.support.annotation.*;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.*;
import android.util.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.*;
import static org.isoron.uhabits.utils.InterfaceUtils.*;
public class HistoryEditorDialog extends AppCompatDialogFragment
implements DialogInterface.OnClickListener, ModelObservable.Listener
{
@Nullable
private Habit habit;
@Nullable
HistoryChart historyChart;
@NonNull
private Controller controller;
private HabitList habitList;
private TaskRunner taskRunner;
public HistoryEditorDialog()
{
this.controller = new Controller() {};
}
@Override
public void onClick(DialogInterface dialog, int which)
{
dismiss();
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState)
{
Context context = getActivity();
HabitsApplication app =
(HabitsApplication) getActivity().getApplicationContext();
habitList = app.getComponent().getHabitList();
taskRunner = app.getComponent().getTaskRunner();
historyChart = new HistoryChart(context);
historyChart.setController(controller);
if (savedInstanceState != null)
{
long id = savedInstanceState.getLong("habit", -1);
if (id > 0) this.habit = habitList.getById(id);
historyChart.onRestoreInstanceState(
savedInstanceState.getParcelable("historyChart"));
}
int padding =
(int) getDimension(getContext(), R.dimen.history_editor_padding);
historyChart.setPadding(padding, 0, padding, 0);
historyChart.setIsEditable(true);
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder
.setTitle(R.string.history)
.setView(historyChart)
.setPositiveButton(android.R.string.ok, this);
return builder.create();
}
@Override
public void onModelChange()
{
refreshData();
}
@Override
public void onPause()
{
habit.getCheckmarks().observable.removeListener(this);
super.onPause();
}
@Override
public void onResume()
{
super.onResume();
DisplayMetrics metrics = getResources().getDisplayMetrics();
int maxHeight = getResources().getDimensionPixelSize(
R.dimen.history_editor_max_height);
int width = metrics.widthPixels;
int height = Math.min(metrics.heightPixels, maxHeight);
getDialog().getWindow().setLayout(width, height);
refreshData();
habit.getCheckmarks().observable.addListener(this);
}
@Override
public void onSaveInstanceState(Bundle outState)
{
outState.putLong("habit", habit.getId());
outState.putParcelable("historyChart", historyChart.onSaveInstanceState());
}
public void setController(@NonNull Controller controller)
{
this.controller = controller;
if (historyChart != null) historyChart.setController(controller);
}
public void setHabit(@Nullable Habit habit)
{
this.habit = habit;
}
private void refreshData()
{
if (habit == null) return;
taskRunner.execute(new RefreshTask());
}
public interface Controller extends HistoryChart.Controller {}
private class RefreshTask implements Task
{
public int[] checkmarks;
@Override
public void doInBackground()
{
checkmarks = habit.getCheckmarks().getAllValues();
}
@Override
public void onPostExecute()
{
if (getContext() == null || habit == null || historyChart == null)
return;
int color = ColorUtils.getColor(getContext(), habit.getColor());
historyChart.setColor(color);
historyChart.setCheckmarks(checkmarks);
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs;
import android.app.*;
import android.content.*;
import android.os.*;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
/**
* Dialog that allows the user to pick one or more days of the week.
*/
public class WeekdayPickerDialog extends AppCompatDialogFragment implements
DialogInterface.OnMultiChoiceClickListener,
DialogInterface.OnClickListener
{
private boolean[] selectedDays;
private OnWeekdaysPickedListener listener;
@Override
public void onClick(DialogInterface dialog, int which, boolean isChecked)
{
selectedDays[which] = isChecked;
}
@Override
public void onClick(DialogInterface dialog, int which)
{
if (listener != null)
listener.onWeekdaysSet(new WeekdayList(selectedDays));
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder
.setTitle(R.string.select_weekdays)
.setMultiChoiceItems(DateUtils.getLongDayNames(), selectedDays,
this)
.setPositiveButton(android.R.string.yes, this)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
dismiss();
});
return builder.create();
}
public void setListener(OnWeekdaysPickedListener listener)
{
this.listener = listener;
}
public void setSelectedDays(WeekdayList days)
{
this.selectedDays = days.toArray();
}
public interface OnWeekdaysPickedListener
{
void onWeekdaysSet(WeekdayList days);
}
}

View File

@@ -0,0 +1,478 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.content.*;
import android.graphics.*;
import android.support.annotation.*;
import android.util.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import java.text.*;
import java.util.*;
import static org.isoron.uhabits.utils.InterfaceUtils.*;
public class BarChart extends ScrollableChart
{
private static final PorterDuffXfermode XFERMODE_CLEAR =
new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
private static final PorterDuffXfermode XFERMODE_SRC =
new PorterDuffXfermode(PorterDuff.Mode.SRC);
private Paint pGrid;
private float em;
private SimpleDateFormat dfMonth;
private SimpleDateFormat dfDay;
private SimpleDateFormat dfYear;
private Paint pText, pGraph;
private RectF rect, prevRect;
private int baseSize;
private int paddingTop;
private float columnWidth;
private int columnHeight;
private int nColumns;
private int textColor;
private int gridColor;
@Nullable
private List<Checkmark> checkmarks;
private int primaryColor;
@Deprecated
private int bucketSize = 7;
private int backgroundColor;
private Bitmap drawingCache;
private Canvas cacheCanvas;
private boolean isTransparencyEnabled;
private int skipYear = 0;
private String previousYearText;
private String previousMonthText;
private double maxValue;
private double target;
public BarChart(Context context)
{
super(context);
init();
}
public BarChart(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}
public void populateWithRandomData()
{
Random random = new Random();
List<Checkmark> checkmarks = new LinkedList<>();
long timestamp = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
for (int i = 1; i < 100; i++)
{
int value = random.nextInt(1000);
checkmarks.add(new Checkmark(timestamp, value));
timestamp -= day;
}
setCheckmarks(checkmarks);
setTarget(0.5);
}
@Deprecated
public void setBucketSize(int bucketSize)
{
this.bucketSize = bucketSize;
postInvalidate();
}
public void setCheckmarks(@NonNull List<Checkmark> checkmarks)
{
this.checkmarks = checkmarks;
maxValue = 1.0;
for (Checkmark c : checkmarks)
maxValue = Math.max(maxValue, c.getValue());
maxValue = Math.ceil(maxValue / 1000 * 1.05) * 1000;
postInvalidate();
}
public void setColor(int primaryColor)
{
this.primaryColor = primaryColor;
postInvalidate();
}
public void setIsTransparencyEnabled(boolean enabled)
{
this.isTransparencyEnabled = enabled;
postInvalidate();
}
public void setTarget(double target)
{
this.target = target;
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
Canvas activeCanvas;
if (isTransparencyEnabled)
{
if (drawingCache == null) initCache(getWidth(), getHeight());
activeCanvas = cacheCanvas;
drawingCache.eraseColor(Color.TRANSPARENT);
}
else
{
activeCanvas = canvas;
}
if (checkmarks == null) return;
rect.set(0, 0, nColumns * columnWidth, columnHeight);
rect.offset(0, paddingTop);
drawGrid(activeCanvas, rect);
pText.setColor(textColor);
pGraph.setColor(primaryColor);
prevRect.setEmpty();
previousMonthText = "";
previousYearText = "";
skipYear = 0;
for (int k = 0; k < nColumns; k++)
{
int offset = nColumns - k - 1 + getDataOffset();
if (offset >= checkmarks.size()) continue;
double value = checkmarks.get(offset).getValue();
long timestamp = checkmarks.get(offset).getTimestamp();
int height = (int) (columnHeight * value / maxValue);
rect.set(0, 0, baseSize, height);
rect.offset(k * columnWidth + (columnWidth - baseSize) / 2,
paddingTop + columnHeight - height);
drawValue(activeCanvas, rect, value);
drawBar(activeCanvas, rect, value);
prevRect.set(rect);
rect.set(0, 0, columnWidth, columnHeight);
rect.offset(k * columnWidth, paddingTop);
drawFooter(activeCanvas, rect, timestamp);
}
if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onSizeChanged(int width,
int height,
int oldWidth,
int oldHeight)
{
if (height < 9) height = 200;
float maxTextSize = getDimension(getContext(), R.dimen.tinyTextSize);
float textSize = height * 0.06f;
pText.setTextSize(Math.min(textSize, maxTextSize));
em = pText.getFontSpacing();
int footerHeight = (int) (3 * em);
paddingTop = (int) (em);
baseSize = (height - footerHeight - paddingTop) / 12;
columnWidth = baseSize;
columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f);
columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
nColumns = (int) (width / columnWidth);
columnWidth = (float) width / nColumns;
setScrollerBucketSize((int) columnWidth);
columnHeight = 12 * baseSize;
float minStrokeWidth = dpToPixels(getContext(), 1);
pGraph.setTextSize(baseSize * 0.5f);
pGraph.setStrokeWidth(baseSize * 0.1f);
pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f));
if (isTransparencyEnabled) initCache(width, height);
}
private void drawBar(Canvas canvas, RectF rect, double value)
{
float margin = baseSize * 0.225f;
int color = textColor;
if (value / 1000 >= target) color = primaryColor;
rect.inset(-margin, 0);
setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
canvas.drawRect(rect, pGraph);
rect.inset(margin, 0);
setModeOrColor(pGraph, XFERMODE_SRC, color);
canvas.drawRect(rect, pGraph);
if (isTransparencyEnabled) pGraph.setXfermode(XFERMODE_SRC);
}
private void drawFooter(Canvas canvas, RectF rect, long currentDate)
{
String yearText = dfYear.format(currentDate);
String monthText = dfMonth.format(currentDate);
String dayText = dfDay.format(currentDate);
GregorianCalendar calendar = DateUtils.getCalendar(currentDate);
pText.setColor(textColor);
String text;
int year = calendar.get(Calendar.YEAR);
boolean shouldPrintYear = true;
if (yearText.equals(previousYearText)) shouldPrintYear = false;
if (bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false;
if (skipYear > 0)
{
skipYear--;
shouldPrintYear = false;
}
if (shouldPrintYear)
{
previousYearText = yearText;
previousMonthText = "";
pText.setTextAlign(Paint.Align.CENTER);
canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f, pText);
skipYear = 1;
}
if (bucketSize < 365)
{
if (!monthText.equals(previousMonthText))
{
previousMonthText = monthText;
text = monthText;
}
else
{
text = dayText;
}
canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f,
pText);
}
}
private void drawGrid(Canvas canvas, RectF rGrid)
{
int nRows = 5;
float rowHeight = rGrid.height() / nRows;
pText.setColor(textColor);
pGrid.setColor(gridColor);
for (int i = 0; i < nRows; i++)
{
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top,
pGrid);
rGrid.offset(0, rowHeight);
}
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
}
private void drawValue(Canvas canvas, RectF rect, double value)
{
if (value == 0) return;
int activeColor = textColor;
if (value / 1000 >= target)
activeColor = primaryColor;
String label = NumberButtonView.formatValue(value / 1000);
Rect rText = new Rect();
pText.getTextBounds(label, 0, label.length(), rText);
float offset = 0.5f * em;
float x = rect.centerX();
float y = rect.top - offset;
int cap = (int) (-0.1f * em);
rText.offset((int) x, (int) y);
rText.offset(-rText.width() / 2, 0);
rText.inset(3 * cap, cap);
setModeOrColor(pText, XFERMODE_CLEAR, backgroundColor);
canvas.drawRect(rText, pText);
setModeOrColor(pText, XFERMODE_SRC, activeColor);
canvas.drawText(label, x, y, pText);
}
private float getMaxDayWidth()
{
float maxDayWidth = 0;
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
for (int i = 0; i < 28; i++)
{
day.set(Calendar.DAY_OF_MONTH, i);
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
maxDayWidth = Math.max(maxDayWidth, monthWidth);
}
return maxDayWidth;
}
private float getMaxMonthWidth()
{
float maxMonthWidth = 0;
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
for (int i = 0; i < 12; i++)
{
day.set(Calendar.MONTH, i);
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
maxMonthWidth = Math.max(maxMonthWidth, monthWidth);
}
return maxMonthWidth;
}
private void init()
{
initPaints();
initColors();
initDateFormats();
initRects();
}
private void initCache(int width, int height)
{
if (drawingCache != null) drawingCache.recycle();
drawingCache =
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
cacheCanvas = new Canvas(drawingCache);
}
private void initColors()
{
StyledResources res = new StyledResources(getContext());
primaryColor = Color.BLACK;
textColor = res.getColor(R.attr.mediumContrastTextColor);
gridColor = res.getColor(R.attr.lowContrastTextColor);
backgroundColor = res.getColor(R.attr.cardBackgroundColor);
}
private void initDateFormats()
{
if (isInEditMode())
{
dfYear = new SimpleDateFormat("yyyy", Locale.US);
dfMonth = new SimpleDateFormat("MMM", Locale.US);
dfDay = new SimpleDateFormat("d", Locale.US);
return;
}
dfYear = DateFormats.fromSkeleton("yyyy");
dfMonth = DateFormats.fromSkeleton("MMM");
dfDay = DateFormats.fromSkeleton("d");
}
private void initPaints()
{
pText = new Paint();
pText.setAntiAlias(true);
pText.setTextAlign(Paint.Align.CENTER);
pGraph = new Paint();
pGraph.setTextAlign(Paint.Align.CENTER);
pGraph.setAntiAlias(true);
pGrid = new Paint();
pGrid.setAntiAlias(true);
}
private void initRects()
{
rect = new RectF();
prevRect = new RectF();
}
private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color)
{
if (isTransparencyEnabled) p.setXfermode(mode);
else p.setColor(color);
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.os.*;
public class BundleSavedState extends android.support.v4.view.AbsSavedState
{
public static final Parcelable.Creator<BundleSavedState> CREATOR =
new Parcelable.Creator<BundleSavedState>()
{
@Override
public BundleSavedState createFromParcel(Parcel source)
{
return new BundleSavedState(source);
}
@Override
public BundleSavedState[] newArray(int size)
{
return new BundleSavedState[size];
}
};
public final Bundle bundle;
public BundleSavedState(Parcelable superState, Bundle bundle)
{
super(superState);
this.bundle = bundle;
}
public BundleSavedState(Parcel source)
{
super(source);
this.bundle = source.readBundle(getClass().getClassLoader());
}
@Override
public void writeToParcel(Parcel out, int flags)
{
super.writeToParcel(out, flags);
out.writeBundle(bundle);
}
}

View File

@@ -0,0 +1,329 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.content.*;
import android.graphics.*;
import android.support.annotation.*;
import android.util.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.utils.*;
import java.text.*;
import java.util.*;
public class FrequencyChart extends ScrollableChart
{
private Paint pGrid;
private float em;
private SimpleDateFormat dfMonth;
private SimpleDateFormat dfYear;
private Paint pText, pGraph;
private RectF rect, prevRect;
private int baseSize;
private int paddingTop;
private float columnWidth;
private int columnHeight;
private int nColumns;
private int textColor;
private int gridColor;
private int[] colors;
private int primaryColor;
private boolean isBackgroundTransparent;
@NonNull
private HashMap<Long, Integer[]> frequency;
private int maxFreq;
public FrequencyChart(Context context)
{
super(context);
init();
}
public FrequencyChart(Context context, AttributeSet attrs)
{
super(context, attrs);
this.frequency = new HashMap<>();
init();
}
public void setColor(int color)
{
this.primaryColor = color;
initColors();
postInvalidate();
}
public void setFrequency(HashMap<Long, Integer[]> frequency)
{
this.frequency = frequency;
maxFreq = getMaxFreq(frequency);
postInvalidate();
}
private int getMaxFreq(HashMap<Long, Integer[]> frequency)
{
int maxValue = 1;
for (Integer[] values : frequency.values())
{
for (Integer value : values)
{
maxValue = Math.max(value, maxValue);
}
}
return maxValue;
}
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
{
this.isBackgroundTransparent = isBackgroundTransparent;
initColors();
}
protected void initPaints()
{
pText = new Paint();
pText.setAntiAlias(true);
pGraph = new Paint();
pGraph.setTextAlign(Paint.Align.CENTER);
pGraph.setAntiAlias(true);
pGrid = new Paint();
pGrid.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
rect.set(0, 0, nColumns * columnWidth, columnHeight);
rect.offset(0, paddingTop);
drawGrid(canvas, rect);
pText.setTextAlign(Paint.Align.CENTER);
pText.setColor(textColor);
pGraph.setColor(primaryColor);
prevRect.setEmpty();
GregorianCalendar currentDate = DateUtils.getStartOfTodayCalendar();
currentDate.set(Calendar.DAY_OF_MONTH, 1);
currentDate.add(Calendar.MONTH, -nColumns + 2 - getDataOffset());
for (int i = 0; i < nColumns - 1; i++)
{
rect.set(0, 0, columnWidth, columnHeight);
rect.offset(i * columnWidth, 0);
drawColumn(canvas, rect, currentDate);
currentDate.add(Calendar.MONTH, 1);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onSizeChanged(int width,
int height,
int oldWidth,
int oldHeight)
{
if (height < 9) height = 200;
baseSize = height / 8;
setScrollerBucketSize(baseSize);
pText.setTextSize(baseSize * 0.4f);
pGraph.setTextSize(baseSize * 0.4f);
pGraph.setStrokeWidth(baseSize * 0.1f);
pGrid.setStrokeWidth(baseSize * 0.05f);
em = pText.getFontSpacing();
columnWidth = baseSize;
columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
columnHeight = 8 * baseSize;
nColumns = (int) (width / columnWidth);
paddingTop = 0;
}
private void drawColumn(Canvas canvas, RectF rect, GregorianCalendar date)
{
Integer values[] = frequency.get(date.getTimeInMillis());
float rowHeight = rect.height() / 8.0f;
prevRect.set(rect);
Integer[] localeWeekdayList = DateUtils.getLocaleWeekdayList();
for (int j = 0; j < localeWeekdayList.length; j++)
{
rect.set(0, 0, baseSize, baseSize);
rect.offset(prevRect.left, prevRect.top + baseSize * j);
int i = DateUtils.javaWeekdayToLoopWeekday(localeWeekdayList[j]);
if (values != null) drawMarker(canvas, rect, values[i]);
rect.offset(0, rowHeight);
}
drawFooter(canvas, rect, date);
}
private void drawFooter(Canvas canvas, RectF rect, GregorianCalendar date)
{
Date time = date.getTime();
canvas.drawText(dfMonth.format(time), rect.centerX(),
rect.centerY() - 0.1f * em, pText);
if (date.get(Calendar.MONTH) == 1)
canvas.drawText(dfYear.format(time), rect.centerX(),
rect.centerY() + 0.9f * em, pText);
}
private void drawGrid(Canvas canvas, RectF rGrid)
{
int nRows = 7;
float rowHeight = rGrid.height() / (nRows + 1);
pText.setTextAlign(Paint.Align.LEFT);
pText.setColor(textColor);
pGrid.setColor(gridColor);
for (String day : DateUtils.getLocaleDayNames(Calendar.SHORT))
{
canvas.drawText(day, rGrid.right - columnWidth,
rGrid.top + rowHeight / 2 + 0.25f * em, pText);
pGrid.setStrokeWidth(1f);
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top,
pGrid);
rGrid.offset(0, rowHeight);
}
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
}
private void drawMarker(Canvas canvas, RectF rect, Integer value)
{
float padding = rect.height() * 0.2f;
// maximal allowed mark radius
float maxRadius = (rect.height() - 2 * padding) / 2.0f;
// the real mark radius is scaled down by a factor depending on the maximal frequency
float scale = 1.0f/maxFreq * value;
float radius = maxRadius * scale;
int colorIndex = Math.round((colors.length-1) * scale);
pGraph.setColor(colors[colorIndex]);
canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph);
}
private float getMaxMonthWidth()
{
float maxMonthWidth = 0;
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
for (int i = 0; i < 12; i++)
{
day.set(Calendar.MONTH, i);
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
maxMonthWidth = Math.max(maxMonthWidth, monthWidth);
}
return maxMonthWidth;
}
private void init()
{
initPaints();
initColors();
initDateFormats();
initRects();
}
private void initColors()
{
StyledResources res = new StyledResources(getContext());
textColor = res.getColor(R.attr.mediumContrastTextColor);
gridColor = res.getColor(R.attr.lowContrastTextColor);
colors = new int[4];
colors[0] = gridColor;
colors[3] = primaryColor;
colors[1] = ColorUtils.mixColors(colors[0], colors[3], 0.66f);
colors[2] = ColorUtils.mixColors(colors[0], colors[3], 0.33f);
}
private void initDateFormats()
{
dfMonth = DateFormats.fromSkeleton("MMM");
dfYear = DateFormats.fromSkeleton("yyyy");
}
private void initRects()
{
rect = new RectF();
prevRect = new RectF();
}
public void populateWithRandomData()
{
GregorianCalendar date = DateUtils.getStartOfTodayCalendar();
date.set(Calendar.DAY_OF_MONTH, 1);
Random rand = new Random();
frequency.clear();
for (int i = 0; i < 40; i++)
{
Integer values[] = new Integer[7];
for (int j = 0; j < 7; j++)
values[j] = rand.nextInt(5);
frequency.put(date.getTimeInMillis(), values);
date.add(Calendar.MONTH, -1);
}
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import org.isoron.uhabits.models.Habit;
public interface HabitChart
{
void setHabit(Habit habit);
void refreshData();
}

View File

@@ -0,0 +1,470 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.content.*;
import android.graphics.*;
import android.graphics.Paint.*;
import android.support.annotation.*;
import android.util.*;
import android.view.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.utils.*;
import java.text.*;
import java.util.*;
import static org.isoron.uhabits.models.Checkmark.*;
import static org.isoron.uhabits.utils.InterfaceUtils.*;
public class HistoryChart extends ScrollableChart
{
private int[] checkmarks;
private int target;
private Paint pSquareBg, pSquareFg, pTextHeader;
private float squareSpacing;
private float squareTextOffset;
private float headerTextOffset;
private float columnWidth;
private float columnHeight;
private int nColumns;
private SimpleDateFormat dfMonth;
private SimpleDateFormat dfYear;
private Calendar baseDate;
private int nDays;
/**
* 0-based-position of today in the column
*/
private int todayPositionInColumn;
private int colors[];
private RectF baseLocation;
private int primaryColor;
private boolean isBackgroundTransparent;
private int textColor;
private int reverseTextColor;
private boolean isEditable;
private String previousMonth;
private String previousYear;
private float headerOverflow = 0;
private boolean isNumerical = false;
@NonNull
private Controller controller;
public HistoryChart(Context context)
{
super(context);
init();
}
public HistoryChart(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}
@Override
public void onLongPress(MotionEvent e)
{
onSingleTapUp(e);
}
@Override
public boolean onSingleTapUp(MotionEvent e)
{
if (!isEditable) return false;
performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
int pointerId = e.getPointerId(0);
float x = e.getX(pointerId);
float y = e.getY(pointerId);
final Long timestamp = positionToTimestamp(x, y);
if (timestamp == null) return false;
int offset = timestampToOffset(timestamp);
if (offset < checkmarks.length)
{
boolean isChecked = checkmarks[offset] == CHECKED_EXPLICITLY;
checkmarks[offset] = (isChecked ? UNCHECKED : CHECKED_EXPLICITLY);
}
controller.onToggleCheckmark(timestamp);
postInvalidate();
return true;
}
public void populateWithRandomData()
{
Random random = new Random();
checkmarks = new int[100];
for (int i = 0; i < 100; i++)
if (random.nextFloat() < 0.3) checkmarks[i] = 2;
for (int i = 0; i < 100 - 7; i++)
{
int count = 0;
for (int j = 0; j < 7; j++)
if (checkmarks[i + j] != 0) count++;
if (count >= 3) checkmarks[i] = Math.max(checkmarks[i], 1);
}
}
public void setCheckmarks(int[] checkmarks)
{
this.checkmarks = checkmarks;
postInvalidate();
}
public void setColor(int color)
{
this.primaryColor = color;
initColors();
postInvalidate();
}
public void setController(@NonNull Controller controller)
{
this.controller = controller;
}
public void setNumerical(boolean numerical)
{
isNumerical = numerical;
}
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
{
this.isBackgroundTransparent = isBackgroundTransparent;
initColors();
}
public void setIsEditable(boolean isEditable)
{
this.isEditable = isEditable;
}
public void setTarget(int target)
{
this.target = target;
postInvalidate();
}
protected void initPaints()
{
pTextHeader = new Paint();
pTextHeader.setTextAlign(Align.LEFT);
pTextHeader.setAntiAlias(true);
pSquareBg = new Paint();
pSquareFg = new Paint();
pSquareFg.setAntiAlias(true);
pSquareFg.setTextAlign(Align.CENTER);
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
baseLocation.set(0, 0, columnWidth - squareSpacing,
columnWidth - squareSpacing);
baseLocation.offset(getPaddingLeft(), getPaddingTop());
headerOverflow = 0;
previousMonth = "";
previousYear = "";
pTextHeader.setColor(textColor);
updateDate();
GregorianCalendar currentDate = (GregorianCalendar) baseDate.clone();
for (int column = 0; column < nColumns - 1; column++)
{
drawColumn(canvas, baseLocation, currentDate, column);
baseLocation.offset(columnWidth, -columnHeight);
}
drawAxis(canvas, baseLocation);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onSizeChanged(int width,
int height,
int oldWidth,
int oldHeight)
{
if (height < 8) height = 200;
float baseSize = height / 8.0f;
setScrollerBucketSize((int) baseSize);
squareSpacing = dpToPixels(getContext(), 1.0f);
float maxTextSize = getDimension(getContext(), R.dimen.regularTextSize);
float textSize = height * 0.06f;
textSize = Math.min(textSize, maxTextSize);
pSquareFg.setTextSize(textSize);
pTextHeader.setTextSize(textSize);
squareTextOffset = pSquareFg.getFontSpacing() * 0.4f;
headerTextOffset = pTextHeader.getFontSpacing() * 0.3f;
float rightLabelWidth = getWeekdayLabelWidth() + headerTextOffset;
float horizontalPadding = getPaddingRight() + getPaddingLeft();
columnWidth = baseSize;
columnHeight = 8 * baseSize;
nColumns =
(int) ((width - rightLabelWidth - horizontalPadding) / baseSize) +
1;
updateDate();
}
private void drawAxis(Canvas canvas, RectF location)
{
float verticalOffset = pTextHeader.getFontSpacing() * 0.4f;
for (String day : DateUtils.getLocaleDayNames(Calendar.SHORT))
{
location.offset(0, columnWidth);
canvas.drawText(day, location.left + headerTextOffset,
location.centerY() + verticalOffset, pTextHeader);
}
}
private void drawColumn(Canvas canvas,
RectF location,
GregorianCalendar date,
int column)
{
drawColumnHeader(canvas, location, date);
location.offset(0, columnWidth);
for (int j = 0; j < 7; j++)
{
if (!(column == nColumns - 2 && getDataOffset() == 0 &&
j > todayPositionInColumn))
{
int checkmarkOffset =
getDataOffset() * 7 + nDays - 7 * (column + 1) +
todayPositionInColumn - j;
drawSquare(canvas, location, date, checkmarkOffset);
}
date.add(Calendar.DAY_OF_MONTH, 1);
location.offset(0, columnWidth);
}
}
private void drawColumnHeader(Canvas canvas,
RectF location,
GregorianCalendar date)
{
String month = dfMonth.format(date.getTime());
String year = dfYear.format(date.getTime());
String text = null;
if (!month.equals(previousMonth)) text = previousMonth = month;
else if (!year.equals(previousYear)) text = previousYear = year;
if (text != null)
{
canvas.drawText(text, location.left + headerOverflow,
location.bottom - headerTextOffset, pTextHeader);
headerOverflow +=
pTextHeader.measureText(text) + columnWidth * 0.2f;
}
headerOverflow = Math.max(0, headerOverflow - columnWidth);
}
private void drawSquare(Canvas canvas,
RectF location,
GregorianCalendar date,
int checkmarkOffset)
{
if (checkmarkOffset >= checkmarks.length) pSquareBg.setColor(colors[0]);
else
{
int checkmark = checkmarks[checkmarkOffset];
if(checkmark == 0) pSquareBg.setColor(colors[0]);
else if(checkmark < target)
{
pSquareBg.setColor(isNumerical ? textColor : colors[1]);
}
else pSquareBg.setColor(colors[2]);
}
pSquareFg.setColor(reverseTextColor);
canvas.drawRect(location, pSquareBg);
String text = Integer.toString(date.get(Calendar.DAY_OF_MONTH));
canvas.drawText(text, location.centerX(),
location.centerY() + squareTextOffset, pSquareFg);
}
private float getWeekdayLabelWidth()
{
float width = 0;
for (String w : DateUtils.getLocaleDayNames(Calendar.SHORT))
width = Math.max(width, pSquareFg.measureText(w));
return width;
}
private void init()
{
isEditable = false;
checkmarks = new int[0];
controller = new Controller() {};
target = 2;
initColors();
initPaints();
initDateFormats();
initRects();
}
private void initColors()
{
StyledResources res = new StyledResources(getContext());
if (isBackgroundTransparent)
primaryColor = ColorUtils.setMinValue(primaryColor, 0.75f);
int red = Color.red(primaryColor);
int green = Color.green(primaryColor);
int blue = Color.blue(primaryColor);
if (isBackgroundTransparent)
{
colors = new int[3];
colors[0] = Color.argb(16, 255, 255, 255);
colors[1] = Color.argb(128, red, green, blue);
colors[2] = primaryColor;
textColor = Color.WHITE;
reverseTextColor = Color.WHITE;
}
else
{
colors = new int[3];
colors[0] = res.getColor(R.attr.lowContrastTextColor);
colors[1] = Color.argb(127, red, green, blue);
colors[2] = primaryColor;
textColor = res.getColor(R.attr.mediumContrastTextColor);
reverseTextColor =
res.getColor(R.attr.highContrastReverseTextColor);
}
}
private void initDateFormats()
{
dfMonth = DateFormats.fromSkeleton("MMM");
dfYear = DateFormats.fromSkeleton("yyyy");
}
private void initRects()
{
baseLocation = new RectF();
}
private Long positionToTimestamp(float x, float y)
{
int col = (int) (x / columnWidth);
int row = (int) (y / columnWidth);
if (row == 0) return null;
if (col == nColumns - 1) return null;
int offset = col * 7 + (row - 1);
Calendar date = (Calendar) baseDate.clone();
date.add(Calendar.DAY_OF_YEAR, offset);
if (DateUtils.getStartOfDay(date.getTimeInMillis()) >
DateUtils.getStartOfToday()) return null;
return date.getTimeInMillis();
}
private int timestampToOffset(Long timestamp)
{
Long day = DateUtils.millisecondsInOneDay;
Long today = DateUtils.getStartOfToday();
return (int) ((today - timestamp) / day);
}
private void updateDate()
{
baseDate = DateUtils.getStartOfTodayCalendar();
baseDate.add(Calendar.DAY_OF_YEAR, -(getDataOffset() - 1) * 7);
nDays = (nColumns - 1) * 7;
int realWeekday =
DateUtils.getStartOfTodayCalendar().get(Calendar.DAY_OF_WEEK);
todayPositionInColumn =
(7 + realWeekday - baseDate.getFirstDayOfWeek()) % 7;
baseDate.add(Calendar.DAY_OF_YEAR, -nDays);
baseDate.add(Calendar.DAY_OF_YEAR, -todayPositionInColumn);
}
public interface Controller
{
default void onToggleCheckmark(long timestamp) {}
}
}

View File

@@ -0,0 +1,256 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.content.*;
import android.graphics.*;
import android.support.annotation.*;
import android.text.*;
import android.util.*;
import android.view.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.utils.*;
import static org.isoron.uhabits.utils.AttributeSetUtils.*;
import static org.isoron.uhabits.utils.InterfaceUtils.*;
public class RingView extends View
{
public static final PorterDuffXfermode XFERMODE_CLEAR =
new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
private int color;
private float precision;
private float percentage;
private int diameter;
private float thickness;
private RectF rect;
private TextPaint pRing;
private Integer backgroundColor;
private Integer inactiveColor;
private float em;
private String text;
private float textSize;
private boolean enableFontAwesome;
@Nullable
private Bitmap drawingCache;
private Canvas cacheCanvas;
private boolean isTransparencyEnabled;
public RingView(Context context)
{
super(context);
percentage = 0.0f;
precision = 0.01f;
color = ColorUtils.getAndroidTestColor(0);
thickness = dpToPixels(getContext(), 2);
text = "";
textSize = getDimension(context, R.dimen.smallTextSize);
init();
}
public RingView(Context ctx, AttributeSet attrs)
{
super(ctx, attrs);
percentage = getFloatAttribute(ctx, attrs, "percentage", 0);
precision = getFloatAttribute(ctx, attrs, "precision", 0.01f);
color = getColorAttribute(ctx, attrs, "color", 0);
backgroundColor = getColorAttribute(ctx, attrs, "backgroundColor", null);
inactiveColor = getColorAttribute(ctx, attrs, "inactiveColor", null);
thickness = getFloatAttribute(ctx, attrs, "thickness", 0);
thickness = dpToPixels(ctx, thickness);
float defaultTextSize = getDimension(ctx, R.dimen.smallTextSize);
textSize = getFloatAttribute(ctx, attrs, "textSize", defaultTextSize);
textSize = spToPixels(ctx, textSize);
text = getAttribute(ctx, attrs, "text", "");
enableFontAwesome =
getBooleanAttribute(ctx, attrs, "enableFontAwesome", false);
init();
}
@Override
public void setBackgroundColor(int backgroundColor)
{
this.backgroundColor = backgroundColor;
postInvalidate();
}
public void setColor(int color)
{
this.color = color;
postInvalidate();
}
public void setIsTransparencyEnabled(boolean isTransparencyEnabled)
{
this.isTransparencyEnabled = isTransparencyEnabled;
}
public void setPercentage(float percentage)
{
this.percentage = percentage;
postInvalidate();
}
public void setPrecision(float precision)
{
this.precision = precision;
postInvalidate();
}
public void setText(String text)
{
this.text = text;
postInvalidate();
}
public void setTextSize(float textSize)
{
this.textSize = textSize;
}
public void setThickness(float thickness)
{
this.thickness = thickness;
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
Canvas activeCanvas;
if (isTransparencyEnabled)
{
if (drawingCache == null) reallocateCache();
activeCanvas = cacheCanvas;
drawingCache.eraseColor(Color.TRANSPARENT);
}
else
{
activeCanvas = canvas;
}
pRing.setColor(color);
rect.set(0, 0, diameter, diameter);
float angle = 360 * Math.round(percentage / precision) * precision;
activeCanvas.drawArc(rect, -90, angle, true, pRing);
pRing.setColor(inactiveColor);
activeCanvas.drawArc(rect, angle - 90, 360 - angle, true, pRing);
if (thickness > 0)
{
if (isTransparencyEnabled) pRing.setXfermode(XFERMODE_CLEAR);
else pRing.setColor(backgroundColor);
rect.inset(thickness, thickness);
activeCanvas.drawArc(rect, 0, 360, true, pRing);
pRing.setXfermode(null);
pRing.setColor(color);
pRing.setTextSize(textSize);
if (enableFontAwesome)
pRing.setTypeface(getFontAwesome(getContext()));
activeCanvas.drawText(text, rect.centerX(),
rect.centerY() + 0.4f * em, pRing);
}
if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
diameter = Math.min(height, width);
pRing.setTextSize(textSize);
em = pRing.measureText("M");
setMeasuredDimension(diameter, diameter);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
if (isTransparencyEnabled) reallocateCache();
}
private void init()
{
pRing = new TextPaint();
pRing.setAntiAlias(true);
pRing.setColor(color);
pRing.setTextAlign(Paint.Align.CENTER);
StyledResources res = new StyledResources(getContext());
if (backgroundColor == null)
backgroundColor = res.getColor(R.attr.cardBackgroundColor);
if (inactiveColor == null)
inactiveColor = res.getColor(R.attr.highContrastTextColor);
inactiveColor = ColorUtils.setAlpha(inactiveColor, 0.1f);
rect = new RectF();
}
private void reallocateCache()
{
if (drawingCache != null) drawingCache.recycle();
drawingCache =
Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888);
cacheCanvas = new Canvas(drawingCache);
}
}

View File

@@ -0,0 +1,442 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.content.*;
import android.graphics.*;
import android.support.annotation.*;
import android.util.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import java.text.*;
import java.util.*;
import static org.isoron.uhabits.utils.InterfaceUtils.*;
public class ScoreChart extends ScrollableChart
{
private static final PorterDuffXfermode XFERMODE_CLEAR =
new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
private static final PorterDuffXfermode XFERMODE_SRC =
new PorterDuffXfermode(PorterDuff.Mode.SRC);
private Paint pGrid;
private float em;
private SimpleDateFormat dfMonth;
private SimpleDateFormat dfDay;
private SimpleDateFormat dfYear;
private Paint pText, pGraph;
private RectF rect, prevRect;
private int baseSize;
private int paddingTop;
private float columnWidth;
private int columnHeight;
private int nColumns;
private int textColor;
private int gridColor;
@Nullable
private List<Score> scores;
private int primaryColor;
@Deprecated
private int bucketSize = 7;
private int backgroundColor;
private Bitmap drawingCache;
private Canvas cacheCanvas;
private boolean isTransparencyEnabled;
private int skipYear = 0;
private String previousYearText;
private String previousMonthText;
public ScoreChart(Context context)
{
super(context);
init();
}
public ScoreChart(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}
public void populateWithRandomData()
{
Random random = new Random();
scores = new LinkedList<>();
double previous = 0.5f;
long timestamp = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
for (int i = 1; i < 100; i++)
{
double step = 0.1f;
double current = previous + random.nextDouble() * step * 2 - step;
current = Math.max(0, Math.min(1.0f, current));
scores.add(new Score(timestamp, current));
previous = current;
timestamp -= day;
}
}
@Deprecated
public void setBucketSize(int bucketSize)
{
this.bucketSize = bucketSize;
postInvalidate();
}
public void setIsTransparencyEnabled(boolean enabled)
{
this.isTransparencyEnabled = enabled;
postInvalidate();
}
public void setColor(int primaryColor)
{
this.primaryColor = primaryColor;
postInvalidate();
}
public void setScores(@NonNull List<Score> scores)
{
this.scores = scores;
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
Canvas activeCanvas;
if (isTransparencyEnabled)
{
if (drawingCache == null) initCache(getWidth(), getHeight());
activeCanvas = cacheCanvas;
drawingCache.eraseColor(Color.TRANSPARENT);
}
else
{
activeCanvas = canvas;
}
if (scores == null) return;
rect.set(0, 0, nColumns * columnWidth, columnHeight);
rect.offset(0, paddingTop);
drawGrid(activeCanvas, rect);
pText.setColor(textColor);
pGraph.setColor(primaryColor);
prevRect.setEmpty();
previousMonthText = "";
previousYearText = "";
skipYear = 0;
for (int k = 0; k < nColumns; k++)
{
int offset = nColumns - k - 1 + getDataOffset();
if (offset >= scores.size()) continue;
double score = scores.get(offset).getValue();
long timestamp = scores.get(offset).getTimestamp();
int height = (int) (columnHeight * score);
rect.set(0, 0, baseSize, baseSize);
rect.offset(k * columnWidth + (columnWidth - baseSize) / 2,
paddingTop + columnHeight - height - baseSize / 2);
if (!prevRect.isEmpty())
{
drawLine(activeCanvas, prevRect, rect);
drawMarker(activeCanvas, prevRect);
}
if (k == nColumns - 1) drawMarker(activeCanvas, rect);
prevRect.set(rect);
rect.set(0, 0, columnWidth, columnHeight);
rect.offset(k * columnWidth, paddingTop);
drawFooter(activeCanvas, rect, timestamp);
}
if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onSizeChanged(int width,
int height,
int oldWidth,
int oldHeight)
{
if (height < 9) height = 200;
float maxTextSize = getDimension(getContext(), R.dimen.tinyTextSize);
float textSize = height * 0.06f;
pText.setTextSize(Math.min(textSize, maxTextSize));
em = pText.getFontSpacing();
int footerHeight = (int) (3 * em);
paddingTop = (int) (em);
baseSize = (height - footerHeight - paddingTop) / 8;
columnWidth = baseSize;
columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f);
columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
nColumns = (int) (width / columnWidth);
columnWidth = (float) width / nColumns;
setScrollerBucketSize((int) columnWidth);
columnHeight = 8 * baseSize;
float minStrokeWidth = dpToPixels(getContext(), 1);
pGraph.setTextSize(baseSize * 0.5f);
pGraph.setStrokeWidth(baseSize * 0.1f);
pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f));
if (isTransparencyEnabled) initCache(width, height);
}
private void drawFooter(Canvas canvas, RectF rect, long currentDate)
{
String yearText = dfYear.format(currentDate);
String monthText = dfMonth.format(currentDate);
String dayText = dfDay.format(currentDate);
GregorianCalendar calendar = DateUtils.getCalendar(currentDate);
String text;
int year = calendar.get(Calendar.YEAR);
boolean shouldPrintYear = true;
if (yearText.equals(previousYearText)) shouldPrintYear = false;
if (bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false;
if (skipYear > 0)
{
skipYear--;
shouldPrintYear = false;
}
if (shouldPrintYear)
{
previousYearText = yearText;
previousMonthText = "";
pText.setTextAlign(Paint.Align.CENTER);
canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f,
pText);
skipYear = 1;
}
if (bucketSize < 365)
{
if (!monthText.equals(previousMonthText))
{
previousMonthText = monthText;
text = monthText;
}
else
{
text = dayText;
}
pText.setTextAlign(Paint.Align.CENTER);
canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f,
pText);
}
}
private void drawGrid(Canvas canvas, RectF rGrid)
{
int nRows = 5;
float rowHeight = rGrid.height() / nRows;
pText.setTextAlign(Paint.Align.LEFT);
pText.setColor(textColor);
pGrid.setColor(gridColor);
for (int i = 0; i < nRows; i++)
{
canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)),
rGrid.left + 0.5f * em, rGrid.top + 1f * em, pText);
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top,
pGrid);
rGrid.offset(0, rowHeight);
}
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
}
private void drawLine(Canvas canvas, RectF rectFrom, RectF rectTo)
{
pGraph.setColor(primaryColor);
canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(),
rectTo.centerX(), rectTo.centerY(), pGraph);
}
private void drawMarker(Canvas canvas, RectF rect)
{
rect.inset(baseSize * 0.225f, baseSize * 0.225f);
setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
canvas.drawOval(rect, pGraph);
rect.inset(baseSize * 0.1f, baseSize * 0.1f);
setModeOrColor(pGraph, XFERMODE_SRC, primaryColor);
canvas.drawOval(rect, pGraph);
// rect.inset(baseSize * 0.1f, baseSize * 0.1f);
// setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
// canvas.drawOval(rect, pGraph);
if (isTransparencyEnabled) pGraph.setXfermode(XFERMODE_SRC);
}
private float getMaxDayWidth()
{
float maxDayWidth = 0;
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
for (int i = 0; i < 28; i++)
{
day.set(Calendar.DAY_OF_MONTH, i);
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
maxDayWidth = Math.max(maxDayWidth, monthWidth);
}
return maxDayWidth;
}
private float getMaxMonthWidth()
{
float maxMonthWidth = 0;
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
for (int i = 0; i < 12; i++)
{
day.set(Calendar.MONTH, i);
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
maxMonthWidth = Math.max(maxMonthWidth, monthWidth);
}
return maxMonthWidth;
}
private void init()
{
initPaints();
initColors();
initDateFormats();
initRects();
}
private void initCache(int width, int height)
{
if (drawingCache != null) drawingCache.recycle();
drawingCache =
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
cacheCanvas = new Canvas(drawingCache);
}
private void initColors()
{
StyledResources res = new StyledResources(getContext());
primaryColor = Color.BLACK;
textColor = res.getColor(R.attr.mediumContrastTextColor);
gridColor = res.getColor(R.attr.lowContrastTextColor);
backgroundColor = res.getColor(R.attr.cardBackgroundColor);
}
private void initDateFormats()
{
dfYear = DateFormats.fromSkeleton("yyyy");
dfMonth = DateFormats.fromSkeleton("MMM");
dfDay = DateFormats.fromSkeleton("d");
}
private void initPaints()
{
pText = new Paint();
pText.setAntiAlias(true);
pGraph = new Paint();
pGraph.setTextAlign(Paint.Align.CENTER);
pGraph.setAntiAlias(true);
pGrid = new Paint();
pGrid.setAntiAlias(true);
}
private void initRects()
{
rect = new RectF();
prevRect = new RectF();
}
private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color)
{
if (isTransparencyEnabled) p.setXfermode(mode);
else p.setColor(color);
}
}

View File

@@ -0,0 +1,232 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.animation.*;
import android.content.*;
import android.os.*;
import android.util.*;
import android.view.*;
import android.widget.*;
public abstract class ScrollableChart extends View
implements GestureDetector.OnGestureListener,
ValueAnimator.AnimatorUpdateListener
{
private int dataOffset;
private int scrollerBucketSize = 1;
private int direction = 1;
private GestureDetector detector;
private Scroller scroller;
private ValueAnimator scrollAnimator;
private ScrollController scrollController;
private int maxDataOffset = 10000;
public ScrollableChart(Context context)
{
super(context);
init(context);
}
public ScrollableChart(Context context, AttributeSet attrs)
{
super(context, attrs);
init(context);
}
public int getDataOffset()
{
return dataOffset;
}
@Override
public void onAnimationUpdate(ValueAnimator animation)
{
if (!scroller.isFinished())
{
scroller.computeScrollOffset();
updateDataOffset();
}
else
{
scrollAnimator.cancel();
}
}
@Override
public boolean onDown(MotionEvent e)
{
return true;
}
@Override
public boolean onFling(MotionEvent e1,
MotionEvent e2,
float velocityX,
float velocityY)
{
scroller.fling(scroller.getCurrX(), scroller.getCurrY(),
direction * ((int) velocityX) / 2, 0, 0, getMaxX(), 0, 0);
invalidate();
scrollAnimator.setDuration(scroller.getDuration());
scrollAnimator.start();
return false;
}
private int getMaxX()
{
return maxDataOffset * scrollerBucketSize;
}
@Override
public void onRestoreInstanceState(Parcelable state)
{
BundleSavedState bss = (BundleSavedState) state;
int x = bss.bundle.getInt("x");
int y = bss.bundle.getInt("y");
direction = bss.bundle.getInt("direction");
dataOffset = bss.bundle.getInt("dataOffset");
maxDataOffset = bss.bundle.getInt("maxDataOffset");
scroller.startScroll(0, 0, x, y, 0);
scroller.computeScrollOffset();
super.onRestoreInstanceState(bss.getSuperState());
}
@Override
public Parcelable onSaveInstanceState()
{
Parcelable superState = super.onSaveInstanceState();
Bundle bundle = new Bundle();
bundle.putInt("x", scroller.getCurrX());
bundle.putInt("y", scroller.getCurrY());
bundle.putInt("dataOffset", dataOffset);
bundle.putInt("direction", direction);
bundle.putInt("maxDataOffset", maxDataOffset);
return new BundleSavedState(superState, bundle);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy)
{
if (scrollerBucketSize == 0) return false;
if (Math.abs(dx) > Math.abs(dy))
{
ViewParent parent = getParent();
if (parent != null) parent.requestDisallowInterceptTouchEvent(true);
}
dx = - direction * dx;
dx = Math.min(dx, getMaxX() - scroller.getCurrX());
scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), (int) dx,
(int) dy, 0);
scroller.computeScrollOffset();
updateDataOffset();
return true;
}
@Override
public void onShowPress(MotionEvent e)
{
}
@Override
public boolean onSingleTapUp(MotionEvent e)
{
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event)
{
return detector.onTouchEvent(event);
}
public void setDirection(int direction)
{
if (direction != 1 && direction != -1)
throw new IllegalArgumentException();
this.direction = direction;
}
@Override
public void onLongPress(MotionEvent e)
{
}
public void setMaxDataOffset(int maxDataOffset)
{
this.maxDataOffset = maxDataOffset;
this.dataOffset = Math.min(dataOffset, maxDataOffset);
scrollController.onDataOffsetChanged(this.dataOffset);
postInvalidate();
}
public void setScrollController(ScrollController scrollController)
{
this.scrollController = scrollController;
}
public void setScrollerBucketSize(int scrollerBucketSize)
{
this.scrollerBucketSize = scrollerBucketSize;
}
private void init(Context context)
{
detector = new GestureDetector(context, this);
scroller = new Scroller(context, null, true);
scrollAnimator = ValueAnimator.ofFloat(0, 1);
scrollAnimator.addUpdateListener(this);
scrollController = new ScrollController() {};
}
private void updateDataOffset()
{
int newDataOffset = scroller.getCurrX() / scrollerBucketSize;
newDataOffset = Math.max(0, newDataOffset);
newDataOffset = Math.min(maxDataOffset, newDataOffset);
if (newDataOffset != dataOffset)
{
dataOffset = newDataOffset;
scrollController.onDataOffsetChanged(dataOffset);
postInvalidate();
}
}
public interface ScrollController
{
default void onDataOffsetChanged(int newDataOffset) {}
}
}

View File

@@ -0,0 +1,298 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.content.*;
import android.graphics.*;
import android.util.*;
import android.view.*;
import android.view.ViewGroup.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import java.text.*;
import java.util.*;
import static android.view.View.MeasureSpec.*;
import static org.isoron.uhabits.utils.InterfaceUtils.getDimension;
public class StreakChart extends View
{
private Paint paint;
private long minLength;
private long maxLength;
private int[] colors;
private RectF rect;
private int baseSize;
private int primaryColor;
private List<Streak> streaks;
private boolean isBackgroundTransparent;
private DateFormat dateFormat;
private int width;
private float em;
private float maxLabelWidth;
private float textMargin;
private boolean shouldShowLabels;
private int textColor;
private int reverseTextColor;
public StreakChart(Context context)
{
super(context);
init();
}
public StreakChart(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}
/**
* Returns the maximum number of streaks this view is able to show, given
* its current size.
*
* @return max number of visible streaks
*/
public int getMaxStreakCount()
{
return (int) Math.floor(getMeasuredHeight() / baseSize);
}
public void populateWithRandomData()
{
long day = DateUtils.millisecondsInOneDay;
long start = DateUtils.getStartOfToday();
LinkedList<Streak> streaks = new LinkedList<>();
for (int i = 0; i < 10; i++)
{
int length = new Random().nextInt(100);
long end = start + length * day;
streaks.add(new Streak(start, end));
start = end + day;
}
setStreaks(streaks);
}
public void setColor(int color)
{
this.primaryColor = color;
postInvalidate();
}
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
{
this.isBackgroundTransparent = isBackgroundTransparent;
initColors();
}
public void setStreaks(List<Streak> streaks)
{
this.streaks = streaks;
initColors();
updateMaxMinLengths();
requestLayout();
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
if (streaks.size() == 0) return;
rect.set(0, 0, width, baseSize);
for (Streak s : streaks)
{
drawRow(canvas, s, rect);
rect.offset(0, baseSize);
}
}
@Override
protected void onMeasure(int widthSpec, int heightSpec)
{
LayoutParams params = getLayoutParams();
if (params != null && params.height == LayoutParams.WRAP_CONTENT)
{
int width = getSize(widthSpec);
int height = streaks.size() * baseSize;
heightSpec = makeMeasureSpec(height, EXACTLY);
widthSpec = makeMeasureSpec(width, EXACTLY);
}
setMeasuredDimension(widthSpec, heightSpec);
}
@Override
protected void onSizeChanged(int width,
int height,
int oldWidth,
int oldHeight)
{
this.width = width;
Context context = getContext();
float minTextSize = getDimension(context, R.dimen.tinyTextSize);
float maxTextSize = getDimension(context, R.dimen.regularTextSize);
float textSize = baseSize * 0.5f;
paint.setTextSize(
Math.max(Math.min(textSize, maxTextSize), minTextSize));
em = paint.getFontSpacing();
textMargin = 0.5f * em;
updateMaxMinLengths();
}
private void drawRow(Canvas canvas, Streak streak, RectF rect)
{
if (maxLength == 0) return;
float percentage = (float) streak.getLength() / maxLength;
float availableWidth = width - 2 * maxLabelWidth;
if (shouldShowLabels) availableWidth -= 2 * textMargin;
float barWidth = percentage * availableWidth;
float minBarWidth =
paint.measureText(Long.toString(streak.getLength())) + em;
barWidth = Math.max(barWidth, minBarWidth);
float gap = (width - barWidth) / 2;
float paddingTopBottom = baseSize * 0.05f;
paint.setColor(percentageToColor(percentage));
canvas.drawRect(rect.left + gap, rect.top + paddingTopBottom,
rect.right - gap, rect.bottom - paddingTopBottom, paint);
float yOffset = rect.centerY() + 0.3f * em;
paint.setColor(reverseTextColor);
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(Long.toString(streak.getLength()), rect.centerX(),
yOffset, paint);
if (shouldShowLabels)
{
String startLabel = dateFormat.format(new Date(streak.getStart()));
String endLabel = dateFormat.format(new Date(streak.getEnd()));
paint.setColor(textColor);
paint.setTextAlign(Paint.Align.RIGHT);
canvas.drawText(startLabel, gap - textMargin, yOffset, paint);
paint.setTextAlign(Paint.Align.LEFT);
canvas.drawText(endLabel, width - gap + textMargin, yOffset, paint);
}
}
private void init()
{
initPaints();
initColors();
streaks = Collections.emptyList();
dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
rect = new RectF();
baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize);
}
private void initColors()
{
int red = Color.red(primaryColor);
int green = Color.green(primaryColor);
int blue = Color.blue(primaryColor);
StyledResources res = new StyledResources(getContext());
colors = new int[4];
colors[3] = primaryColor;
colors[2] = Color.argb(192, red, green, blue);
colors[1] = Color.argb(96, red, green, blue);
colors[0] = res.getColor(R.attr.lowContrastTextColor);
textColor = res.getColor(R.attr.mediumContrastTextColor);
reverseTextColor = res.getColor(R.attr.highContrastReverseTextColor);
}
private void initPaints()
{
paint = new Paint();
paint.setTextAlign(Paint.Align.CENTER);
paint.setAntiAlias(true);
}
private int percentageToColor(float percentage)
{
if (percentage >= 1.0f) return colors[3];
if (percentage >= 0.8f) return colors[2];
if (percentage >= 0.5f) return colors[1];
return colors[0];
}
private void updateMaxMinLengths()
{
maxLength = 0;
minLength = Long.MAX_VALUE;
shouldShowLabels = true;
for (Streak s : streaks)
{
maxLength = Math.max(maxLength, s.getLength());
minLength = Math.min(minLength, s.getLength());
float lw1 =
paint.measureText(dateFormat.format(new Date(s.getStart())));
float lw2 =
paint.measureText(dateFormat.format(new Date(s.getEnd())));
maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2));
}
if (width - 2 * maxLabelWidth < width * 0.25f)
{
maxLabelWidth = 0;
shouldShowLabels = false;
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides views that are used across the app, such as RingView.
*/
package org.isoron.uhabits.activities.common.views;

View File

@@ -0,0 +1,260 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.edit;
import android.content.*;
import android.os.*;
import android.support.annotation.*;
import android.support.v7.app.*;
import android.text.format.*;
import android.view.*;
import com.android.datetimepicker.time.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.common.dialogs.*;
import org.isoron.uhabits.activities.habits.edit.views.*;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*;
import butterknife.*;
import static android.view.View.*;
public class EditHabitDialog extends AppCompatDialogFragment
{
public static final String BUNDLE_HABIT_ID = "habitId";
public static final String BUNDLE_HABIT_TYPE = "habitType";
protected Habit originalHabit;
protected Preferences prefs;
protected CommandRunner commandRunner;
protected HabitList habitList;
protected AppComponent component;
protected ModelFactory modelFactory;
@BindView(R.id.namePanel)
NameDescriptionPanel namePanel;
@BindView(R.id.reminderPanel)
ReminderPanel reminderPanel;
@BindView(R.id.frequencyPanel)
FrequencyPanel frequencyPanel;
@BindView(R.id.targetPanel)
TargetPanel targetPanel;
private ColorPickerDialogFactory colorPickerDialogFactory;
@Override
public int getTheme()
{
return R.style.DialogWithTitle;
}
@Override
public void onActivityCreated(Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
BaseActivity activity = (BaseActivity) getActivity();
colorPickerDialogFactory =
activity.getComponent().getColorPickerDialogFactory();
}
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState)
{
View view;
view = inflater.inflate(R.layout.edit_habit, container, false);
initDependencies();
ButterKnife.bind(this, view);
originalHabit = parseHabitFromArguments();
getDialog().setTitle(getTitle());
populateForm();
setupReminderController();
setupNameController();
return view;
}
protected int getTitle()
{
if (originalHabit != null) return R.string.edit_habit;
else return R.string.create_habit;
}
protected void saveHabit(@NonNull Habit habit)
{
if (originalHabit == null)
{
commandRunner.execute(component
.getCreateHabitCommandFactory()
.create(habitList, habit), null);
}
else
{
commandRunner.execute(component.getEditHabitCommandFactory().
create(habitList, originalHabit, habit), originalHabit.getId());
}
}
private int getTypeFromArguments()
{
return getArguments().getInt(BUNDLE_HABIT_TYPE);
}
private void initDependencies()
{
Context appContext = getContext().getApplicationContext();
HabitsApplication app = (HabitsApplication) appContext;
component = app.getComponent();
prefs = component.getPreferences();
habitList = component.getHabitList();
commandRunner = component.getCommandRunner();
modelFactory = component.getModelFactory();
}
@OnClick(R.id.buttonDiscard)
void onButtonDiscardClick()
{
dismiss();
}
@OnClick(R.id.buttonSave)
void onSaveButtonClick()
{
int type = getTypeFromArguments();
if (!namePanel.validate()) return;
if (type == Habit.YES_NO_HABIT && !frequencyPanel.validate()) return;
if (type == Habit.NUMBER_HABIT && !targetPanel.validate()) return;
Habit habit = modelFactory.buildHabit();
habit.setName(namePanel.getName());
habit.setDescription(namePanel.getDescription());
habit.setColor(namePanel.getColor());
habit.setReminder(reminderPanel.getReminder());
habit.setFrequency(frequencyPanel.getFrequency());
habit.setUnit(targetPanel.getUnit());
habit.setTargetValue(targetPanel.getTargetValue());
habit.setType(type);
saveHabit(habit);
dismiss();
}
@Nullable
private Habit parseHabitFromArguments()
{
Bundle arguments = getArguments();
if (arguments == null) return null;
Long id = (Long) arguments.get(BUNDLE_HABIT_ID);
if (id == null) return null;
Habit habit = habitList.getById(id);
if (habit == null) throw new IllegalStateException();
return habit;
}
private void populateForm()
{
Habit habit = modelFactory.buildHabit();
habit.setFrequency(Frequency.DAILY);
habit.setColor(prefs.getDefaultHabitColor(habit.getColor()));
habit.setType(getTypeFromArguments());
if (originalHabit != null) habit.copyFrom(originalHabit);
if (habit.isNumerical()) frequencyPanel.setVisibility(GONE);
else targetPanel.setVisibility(GONE);
namePanel.populateFrom(habit);
frequencyPanel.setFrequency(habit.getFrequency());
targetPanel.setTargetValue(habit.getTargetValue());
targetPanel.setUnit(habit.getUnit());
if (habit.hasReminder()) reminderPanel.setReminder(habit.getReminder());
}
private void setupNameController()
{
namePanel.setController(new NameDescriptionPanel.Controller()
{
@Override
public void onColorPickerClicked(int previousColor)
{
ColorPickerDialog picker =
colorPickerDialogFactory.create(previousColor);
picker.setListener(c ->
{
prefs.setDefaultHabitColor(c);
namePanel.setColor(c);
});
picker.show(getFragmentManager(), "picker");
}
});
}
private void setupReminderController()
{
reminderPanel.setController(new ReminderPanel.Controller()
{
@Override
public void onTimeClicked(int currentHour, int currentMin)
{
TimePickerDialog timePicker;
boolean is24HourMode = DateFormat.is24HourFormat(getContext());
timePicker =
TimePickerDialog.newInstance(reminderPanel, currentHour,
currentMin, is24HourMode);
timePicker.show(getFragmentManager(), "timePicker");
}
@Override
public void onWeekdayClicked(WeekdayList currentDays)
{
WeekdayPickerDialog dialog = new WeekdayPickerDialog();
dialog.setListener(reminderPanel);
dialog.setSelectedDays(currentDays);
dialog.show(getFragmentManager(), "weekdayPicker");
}
});
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.edit;
import android.os.*;
import android.support.annotation.*;
import org.isoron.uhabits.models.*;
import javax.inject.*;
import static org.isoron.uhabits.activities.habits.edit.EditHabitDialog.*;
public class EditHabitDialogFactory
{
@Inject
public EditHabitDialogFactory()
{
}
public EditHabitDialog createBoolean()
{
EditHabitDialog dialog = new EditHabitDialog();
Bundle args = new Bundle();
args.putInt(BUNDLE_HABIT_TYPE, Habit.YES_NO_HABIT);
dialog.setArguments(args);
return dialog;
}
public EditHabitDialog createNumerical()
{
EditHabitDialog dialog = new EditHabitDialog();
Bundle args = new Bundle();
args.putInt(BUNDLE_HABIT_TYPE, Habit.NUMBER_HABIT);
dialog.setArguments(args);
return dialog;
}
public EditHabitDialog edit(@NonNull Habit habit)
{
if (habit.getId() == null)
throw new IllegalArgumentException("habit not saved");
EditHabitDialog dialog = new EditHabitDialog();
Bundle args = new Bundle();
args.putLong(BUNDLE_HABIT_ID, habit.getId());
args.putInt(BUNDLE_HABIT_TYPE, habit.getType());
dialog.setArguments(args);
return dialog;
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides dialogs for editing habits and related classes.
*/
package org.isoron.uhabits.activities.habits.edit;

View File

@@ -0,0 +1,114 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.edit.views;
import android.content.*;
import android.support.annotation.*;
import android.text.*;
import android.util.*;
import android.view.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.utils.*;
import static org.isoron.uhabits.utils.AttributeSetUtils.*;
/**
* An EditText that shows an example usage when there is no text
* currently set. The example disappears when the widget gains focus.
*/
public class ExampleEditText extends EditText
implements View.OnFocusChangeListener
{
private String example;
private String realText;
private int color;
private int exampleColor;
private int inputType;
public ExampleEditText(Context context, @Nullable AttributeSet attrs)
{
super(context, attrs);
if (attrs != null)
example = getAttribute(context, attrs, "example", "");
inputType = getInputType();
realText = getText().toString();
color = getCurrentTextColor();
init();
}
public String getRealText()
{
if(hasFocus()) return getText().toString();
else return realText;
}
@Override
public void onFocusChange(View v, boolean hasFocus)
{
if (!hasFocus) realText = getText().toString();
updateText();
}
public void setExample(String example)
{
this.example = example;
updateText();
}
public void setRealText(String realText)
{
this.realText = realText;
updateText();
}
private void init()
{
StyledResources sr = new StyledResources(getContext());
exampleColor = sr.getColor(R.attr.mediumContrastTextColor);
setOnFocusChangeListener(this);
updateText();
}
private void updateText()
{
if (realText.isEmpty() && !isFocused())
{
setTextColor(exampleColor);
setText(example);
setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
}
else
{
setText(realText);
setTextColor(color);
setInputType(inputType);
}
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.edit.views;
import android.annotation.*;
import android.content.*;
import android.content.res.*;
import android.support.annotation.*;
import android.util.*;
import android.view.*;
import android.widget.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.*;
import butterknife.*;
import static org.isoron.uhabits.R.id.*;
public class FrequencyPanel extends FrameLayout
{
@BindView(numerator)
TextView tvNumerator;
@BindView(R.id.denominator)
TextView tvDenominator;
@BindView(R.id.spinner)
Spinner spinner;
@BindView(R.id.customFreqPanel)
ViewGroup customFreqPanel;
public FrequencyPanel(@NonNull Context context,
@Nullable AttributeSet attrs)
{
super(context, attrs);
View view = inflate(context, R.layout.edit_habit_frequency, null);
ButterKnife.bind(this, view);
addView(view);
}
@NonNull
public Frequency getFrequency()
{
String freqNum = tvNumerator.getText().toString();
String freqDen = tvDenominator.getText().toString();
if (!freqNum.isEmpty() && !freqDen.isEmpty())
{
int numerator = Integer.parseInt(freqNum);
int denominator = Integer.parseInt(freqDen);
return new Frequency(numerator, denominator);
}
return Frequency.DAILY;
}
@SuppressLint("SetTextI18n")
public void setFrequency(@NonNull Frequency freq)
{
int position = getQuickSelectPosition(freq);
if (position >= 0) showSimplifiedFrequency(position);
else showCustomFrequency();
tvNumerator.setText(Integer.toString(freq.getNumerator()));
tvDenominator.setText(Integer.toString(freq.getDenominator()));
}
@OnItemSelected(R.id.spinner)
public void onFrequencySelected(int position)
{
if (position < 0 || position > 4) throw new IllegalArgumentException();
int freqNums[] = { 1, 1, 2, 5, 3 };
int freqDens[] = { 1, 7, 7, 7, 7 };
setFrequency(new Frequency(freqNums[position], freqDens[position]));
}
public boolean validate()
{
boolean valid = true;
Resources res = getResources();
String freqNum = tvNumerator.getText().toString();
String freqDen = tvDenominator.getText().toString();
if (freqDen.isEmpty())
{
tvDenominator.setError(
res.getString(R.string.validation_show_not_be_blank));
valid = false;
}
if (freqNum.isEmpty())
{
tvNumerator.setError(
res.getString(R.string.validation_show_not_be_blank));
valid = false;
}
if (!valid) return false;
int numerator = Integer.parseInt(freqNum);
int denominator = Integer.parseInt(freqDen);
if (numerator <= 0)
{
tvNumerator.setError(
res.getString(R.string.validation_number_should_be_positive));
valid = false;
}
if (numerator > denominator)
{
tvNumerator.setError(
res.getString(R.string.validation_at_most_one_rep_per_day));
valid = false;
}
return valid;
}
private int getQuickSelectPosition(@NonNull Frequency freq)
{
if (freq.equals(Frequency.DAILY)) return 0;
if (freq.equals(Frequency.WEEKLY)) return 1;
if (freq.equals(Frequency.TWO_TIMES_PER_WEEK)) return 2;
if (freq.equals(Frequency.FIVE_TIMES_PER_WEEK)) return 3;
return -1;
}
private void showCustomFrequency()
{
spinner.setVisibility(View.GONE);
customFreqPanel.setVisibility(View.VISIBLE);
}
private void showSimplifiedFrequency(int quickSelectPosition)
{
spinner.setVisibility(View.VISIBLE);
spinner.setSelection(quickSelectPosition);
customFreqPanel.setVisibility(View.GONE);
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.edit.views;
import android.content.*;
import android.content.res.*;
import android.os.*;
import android.support.annotation.*;
import android.util.*;
import android.view.*;
import android.widget.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import butterknife.*;
public class NameDescriptionPanel extends FrameLayout
{
@BindView(R.id.tvName)
EditText tvName;
@BindView(R.id.tvDescription)
ExampleEditText tvDescription;
private int color;
@NonNull
private Controller controller;
public NameDescriptionPanel(@NonNull Context context,
@Nullable AttributeSet attrs)
{
super(context, attrs);
View view = inflate(context, R.layout.edit_habit_name, null);
ButterKnife.bind(this, view);
addView(view);
controller = new Controller() {};
}
public int getColor()
{
return color;
}
public void setColor(int color)
{
this.color = color;
tvName.setTextColor(ColorUtils.getColor(getContext(), color));
}
@NonNull
public String getDescription()
{
return tvDescription.getRealText().trim();
}
@NonNull
public String getName()
{
return tvName.getText().toString().trim();
}
public void populateFrom(@NonNull Habit habit)
{
Resources res = getResources();
if(habit.isNumerical())
tvDescription.setExample(res.getString(R.string.example_question_numerical));
else
tvDescription.setExample(res.getString(R.string.example_question_boolean));
setColor(habit.getColor());
tvName.setText(habit.getName());
tvDescription.setRealText(habit.getDescription());
}
public boolean validate()
{
Resources res = getResources();
if (getName().isEmpty())
{
tvName.setError(
res.getString(R.string.validation_name_should_not_be_blank));
return false;
}
return true;
}
@Override
protected void onRestoreInstanceState(Parcelable state)
{
BundleSavedState bss = (BundleSavedState) state;
setColor(bss.bundle.getInt("color"));
super.onRestoreInstanceState(bss.getSuperState());
}
@Override
protected Parcelable onSaveInstanceState()
{
Parcelable superState = super.onSaveInstanceState();
Bundle bundle = new Bundle();
bundle.putInt("color", color);
return new BundleSavedState(superState, bundle);
}
@OnClick(R.id.buttonPickColor)
void showColorPicker()
{
controller.onColorPickerClicked(color);
}
public void setController(@NonNull Controller controller)
{
this.controller = controller;
}
public interface Controller
{
/**
* Called when the user has clicked the widget to select a new
* color for the habit.
*
* @param previousColor the color previously selected
*/
default void onColorPickerClicked(int previousColor) {}
}
}

View File

@@ -0,0 +1,196 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.edit.views;
import android.content.*;
import android.os.*;
import android.support.annotation.*;
import android.util.*;
import android.view.*;
import android.widget.*;
import com.android.datetimepicker.time.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.common.dialogs.*;
import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.models.*;
import butterknife.*;
import static org.isoron.uhabits.utils.DateUtils.*;
public class ReminderPanel extends FrameLayout
implements TimePickerDialog.OnTimeSetListener,
WeekdayPickerDialog.OnWeekdaysPickedListener
{
@BindView(R.id.tvReminderTime)
TextView tvReminderTime;
@BindView(R.id.llReminderDays)
ViewGroup llReminderDays;
@BindView(R.id.tvReminderDays)
TextView tvReminderDays;
@Nullable
private Reminder reminder;
@NonNull
private Controller controller;
public ReminderPanel(@NonNull Context context, @Nullable AttributeSet attrs)
{
super(context, attrs);
View view = inflate(context, R.layout.edit_habit_reminder, null);
ButterKnife.bind(this, view);
addView(view);
controller = new Controller() {};
setReminder(null);
}
@Nullable
public Reminder getReminder()
{
return reminder;
}
public void setReminder(@Nullable Reminder reminder)
{
this.reminder = reminder;
if (reminder == null)
{
tvReminderTime.setText(R.string.reminder_off);
llReminderDays.setVisibility(View.GONE);
return;
}
Context ctx = getContext();
String time = formatTime(ctx, reminder.getHour(), reminder.getMinute());
tvReminderTime.setText(time);
llReminderDays.setVisibility(View.VISIBLE);
boolean weekdays[] = reminder.getDays().toArray();
tvReminderDays.setText(formatWeekdayList(ctx, weekdays));
}
@Override
public void onTimeCleared(RadialPickerLayout view)
{
setReminder(null);
}
@Override
public void onTimeSet(RadialPickerLayout view, int hour, int minute)
{
WeekdayList days = WeekdayList.EVERY_DAY;
if (reminder != null) days = reminder.getDays();
setReminder(new Reminder(hour, minute, days));
}
@Override
public void onWeekdaysSet(WeekdayList selectedDays)
{
if (reminder == null) return;
if (selectedDays.isEmpty()) selectedDays = WeekdayList.EVERY_DAY;
setReminder(new Reminder(reminder.getHour(), reminder.getMinute(),
selectedDays));
}
public void setController(@NonNull Controller controller)
{
this.controller = controller;
}
@Override
protected void onRestoreInstanceState(Parcelable state)
{
BundleSavedState bss = (BundleSavedState) state;
if (!bss.bundle.isEmpty())
{
int days = bss.bundle.getInt("days");
int hour = bss.bundle.getInt("hour");
int minute = bss.bundle.getInt("minute");
reminder = new Reminder(hour, minute, new WeekdayList(days));
setReminder(reminder);
}
super.onRestoreInstanceState(bss.getSuperState());
}
@Override
protected Parcelable onSaveInstanceState()
{
Parcelable superState = super.onSaveInstanceState();
Bundle bundle = new Bundle();
if (reminder != null)
{
bundle.putInt("days", reminder.getDays().toInteger());
bundle.putInt("hour", reminder.getHour());
bundle.putInt("minute", reminder.getMinute());
}
return new BundleSavedState(superState, bundle);
}
@OnClick(R.id.tvReminderTime)
void onDateSpinnerClick()
{
int hour = 8;
int min = 0;
if (reminder != null)
{
hour = reminder.getHour();
min = reminder.getMinute();
}
controller.onTimeClicked(hour, min);
}
@OnClick(R.id.tvReminderDays)
void onWeekdayClicked()
{
if (reminder == null) return;
controller.onWeekdayClicked(reminder.getDays());
}
public interface Controller
{
/**
* Called when the user has clicked the widget to change the time of
* the reminder.
*
* @param currentHour hour previously picked by the user
* @param currentMin minute previously picked by the user
*/
default void onTimeClicked(int currentHour, int currentMin) {}
/**
* Called when the used has clicked the widget to change the days
* of the reminder.
*
* @param currentDays days previously selected by the user.
*/
default void onWeekdayClicked(WeekdayList currentDays) {}
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.edit.views;
import android.content.*;
import android.content.res.*;
import android.support.annotation.*;
import android.util.*;
import android.view.*;
import android.widget.*;
import org.isoron.uhabits.R;
import java.text.DecimalFormat;
import butterknife.*;
public class TargetPanel extends FrameLayout
{
private DecimalFormat valueFormatter = new DecimalFormat("#.##");
@BindView(R.id.tvUnit)
ExampleEditText tvUnit;
@BindView(R.id.tvTargetCount)
TextView tvTargetValue;
public TargetPanel(@NonNull Context context, @Nullable AttributeSet attrs)
{
super(context, attrs);
View view = inflate(context, R.layout.edit_habit_target, null);
ButterKnife.bind(this, view);
addView(view);
}
public double getTargetValue()
{
String sValue = tvTargetValue.getText().toString();
return Double.parseDouble(sValue);
}
public void setTargetValue(double targetValue)
{
tvTargetValue.setText(valueFormatter.format(targetValue));
}
public String getUnit()
{
return tvUnit.getRealText();
}
public void setUnit(String unit)
{
tvUnit.setRealText(unit);
}
public boolean validate()
{
Resources res = getResources();
String sValue = tvTargetValue.getText().toString();
double value = Double.parseDouble(sValue);
if (value <= 0)
{
tvTargetValue.setError(
res.getString(R.string.validation_number_should_be_positive));
return false;
}
return true;
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list;
import android.content.*;
import android.os.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.habits.list.model.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.sync.*;
import org.isoron.uhabits.utils.*;
/**
* Activity that allows the user to see and modify the list of habits.
*/
public class ListHabitsActivity extends BaseActivity
{
private HabitCardListAdapter adapter;
private ListHabitsRootView rootView;
private ListHabitsScreen screen;
private ListHabitsComponent component;
private boolean pureBlack;
private Preferences prefs;
private MidnightTimer midnightTimer;
public ListHabitsComponent getListHabitsComponent()
{
return component;
}
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
HabitsApplication app = (HabitsApplication) getApplicationContext();
component = DaggerListHabitsComponent
.builder()
.appComponent(app.getComponent())
.activityModule(new ActivityModule(this))
.build();
ListHabitsMenu menu = component.getMenu();
ListHabitsSelectionMenu selectionMenu = component.getSelectionMenu();
ListHabitsController controller = component.getController();
adapter = component.getAdapter();
rootView = component.getRootView();
screen = component.getScreen();
prefs = app.getComponent().getPreferences();
pureBlack = prefs.isPureBlackEnabled();
screen.setMenu(menu);
screen.setController(controller);
screen.setSelectionMenu(selectionMenu);
rootView.setController(controller, selectionMenu);
midnightTimer = component.getMidnightTimer();
if(prefs.isSyncFeatureEnabled())
startService(new Intent(this, SyncService.class));
setScreen(screen);
controller.onStartup();
}
@Override
protected void onPause()
{
midnightTimer.onPause();
screen.onDettached();
adapter.cancelRefresh();
super.onPause();
}
@Override
protected void onResume()
{
adapter.refresh();
screen.onAttached();
rootView.postInvalidate();
midnightTimer.onResume();
if (prefs.getTheme() == ThemeSwitcher.THEME_DARK &&
prefs.isPureBlackEnabled() != pureBlack)
{
restartWithFade(ListHabitsActivity.class);
}
super.onResume();
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.activities.habits.list.model.*;
import org.isoron.uhabits.utils.*;
import dagger.*;
@ActivityScope
@Component(modules = { ActivityModule.class },
dependencies = { AppComponent.class })
public interface ListHabitsComponent
{
HabitCardListAdapter getAdapter();
CheckmarkButtonControllerFactory getCheckmarkButtonControllerFactory();
ListHabitsController getController();
ListHabitsMenu getMenu();
MidnightTimer getMidnightTimer();
NumberButtonControllerFactory getNumberButtonControllerFactory();
ListHabitsRootView getRootView();
ListHabitsScreen getScreen();
ListHabitsSelectionMenu getSelectionMenu();
}

View File

@@ -0,0 +1,243 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list;
import android.support.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.activities.habits.list.model.*;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.*;
import org.isoron.uhabits.widgets.*;
import java.io.*;
import java.util.*;
import javax.inject.*;
@ActivityScope
public class ListHabitsController
implements HabitCardListController.HabitListener
{
@NonNull
private final ListHabitsScreen screen;
@NonNull
private final BaseSystem system;
@NonNull
private final HabitList habitList;
@NonNull
private final HabitCardListAdapter adapter;
@NonNull
private final Preferences prefs;
@NonNull
private final CommandRunner commandRunner;
@NonNull
private final TaskRunner taskRunner;
private ReminderScheduler reminderScheduler;
private WidgetUpdater widgetUpdater;
private ImportDataTaskFactory importTaskFactory;
private ExportCSVTaskFactory exportCSVFactory;
private ExportDBTaskFactory exportDBFactory;
@Inject
public ListHabitsController(@NonNull BaseSystem system,
@NonNull CommandRunner commandRunner,
@NonNull HabitList habitList,
@NonNull HabitCardListAdapter adapter,
@NonNull ListHabitsScreen screen,
@NonNull Preferences prefs,
@NonNull ReminderScheduler reminderScheduler,
@NonNull TaskRunner taskRunner,
@NonNull WidgetUpdater widgetUpdater,
@NonNull ImportDataTaskFactory importTaskFactory,
@NonNull ExportCSVTaskFactory exportCSVFactory,
@NonNull ExportDBTaskFactory exportDBFactory)
{
this.adapter = adapter;
this.commandRunner = commandRunner;
this.habitList = habitList;
this.prefs = prefs;
this.screen = screen;
this.system = system;
this.taskRunner = taskRunner;
this.reminderScheduler = reminderScheduler;
this.widgetUpdater = widgetUpdater;
this.importTaskFactory = importTaskFactory;
this.exportCSVFactory = exportCSVFactory;
this.exportDBFactory = exportDBFactory;
}
public void onExportCSV()
{
List<Habit> selected = new LinkedList<>();
for (Habit h : habitList) selected.add(h);
taskRunner.execute(exportCSVFactory.create(selected, filename -> {
if (filename != null) screen.showSendFileScreen(filename);
else screen.showMessage(R.string.could_not_export);
}));
}
public void onExportDB()
{
taskRunner.execute(exportDBFactory.create(filename -> {
if (filename != null) screen.showSendFileScreen(filename);
else screen.showMessage(R.string.could_not_export);
}));
}
@Override
public void onHabitClick(@NonNull Habit h)
{
screen.showHabitScreen(h);
}
@Override
public void onHabitReorder(@NonNull Habit from, @NonNull Habit to)
{
taskRunner.execute(() -> habitList.reorder(from, to));
}
public void onImportData(@NonNull File file,
@NonNull OnFinishedListener finishedListener)
{
taskRunner.execute(importTaskFactory.create(file, result -> {
switch (result)
{
case ImportDataTask.SUCCESS:
adapter.refresh();
screen.showMessage(R.string.habits_imported);
break;
case ImportDataTask.NOT_RECOGNIZED:
screen.showMessage(R.string.file_not_recognized);
break;
default:
screen.showMessage(R.string.could_not_import);
break;
}
finishedListener.onFinish();
}));
}
@Override
public void onInvalidEdit()
{
screen.showMessage(R.string.long_press_to_edit);
}
@Override
public void onEdit(@NonNull Habit habit, long timestamp)
{
CheckmarkList checkmarks = habit.getCheckmarks();
double oldValue = checkmarks.getValues(timestamp, timestamp)[0];
screen.showNumberPicker(oldValue / 1000, habit.getUnit(), newValue -> {
newValue = Math.round(newValue * 1000);
commandRunner.execute(
new CreateRepetitionCommand(habit, timestamp, (int) newValue),
habit.getId());
});
}
@Override
public void onInvalidToggle()
{
screen.showMessage(R.string.long_press_to_toggle);
}
public void onRepairDB()
{
taskRunner.execute(() -> {
habitList.repair();
screen.showMessage(R.string.database_repaired);
});
}
public void onSendBugReport()
{
try
{
system.dumpBugReportToFile();
}
catch (IOException e)
{
// ignored
}
try
{
String log = system.getBugReport();
int to = R.string.bugReportTo;
int subject = R.string.bugReportSubject;
screen.showSendEmailScreen(to, subject, log);
}
catch (IOException e)
{
e.printStackTrace();
screen.showMessage(R.string.bug_report_failed);
}
}
public void onStartup()
{
prefs.incrementLaunchCount();
if (prefs.isFirstRun()) onFirstRun();
}
@Override
public void onToggle(@NonNull Habit habit, long timestamp)
{
commandRunner.execute(new ToggleRepetitionCommand(habit, timestamp),
habit.getId());
}
private void onFirstRun()
{
prefs.setFirstRun(false);
prefs.updateLastHint(-1, DateUtils.getStartOfToday());
screen.showIntroScreen();
}
public interface OnFinishedListener
{
void onFinish();
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list;
import android.support.annotation.*;
import android.view.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.habits.list.model.*;
import org.isoron.uhabits.preferences.*;
import javax.inject.*;
@ActivityScope
public class ListHabitsMenu extends BaseMenu
{
@NonNull
private final ListHabitsScreen screen;
private final HabitCardListAdapter adapter;
private boolean showArchived;
private boolean showCompleted;
private final Preferences preferences;
private ThemeSwitcher themeSwitcher;
@Inject
public ListHabitsMenu(@NonNull BaseActivity activity,
@NonNull ListHabitsScreen screen,
@NonNull HabitCardListAdapter adapter,
@NonNull Preferences preferences,
@NonNull ThemeSwitcher themeSwitcher)
{
super(activity);
this.screen = screen;
this.adapter = adapter;
this.preferences = preferences;
this.themeSwitcher = themeSwitcher;
showCompleted = preferences.getShowCompleted();
showArchived = preferences.getShowArchived();
updateAdapterFilter();
}
@Override
public void onCreate(@NonNull Menu menu)
{
MenuItem nightModeItem = menu.findItem(R.id.actionToggleNightMode);
nightModeItem.setChecked(themeSwitcher.isNightMode());
MenuItem hideArchivedItem = menu.findItem(R.id.actionHideArchived);
hideArchivedItem.setChecked(!showArchived);
MenuItem hideCompletedItem = menu.findItem(R.id.actionHideCompleted);
hideCompletedItem.setChecked(!showCompleted);
}
@Override
public boolean onItemSelected(@NonNull MenuItem item)
{
switch (item.getItemId())
{
case R.id.actionToggleNightMode:
screen.toggleNightMode();
return true;
case R.id.actionAdd:
screen.showCreateHabitScreen();
return true;
case R.id.actionFAQ:
screen.showFAQScreen();
return true;
case R.id.actionAbout:
screen.showAboutScreen();
return true;
case R.id.actionSettings:
screen.showSettingsScreen();
return true;
case R.id.actionHideArchived:
toggleShowArchived();
invalidate();
return true;
case R.id.actionHideCompleted:
toggleShowCompleted();
invalidate();
return true;
case R.id.actionSortColor:
adapter.setOrder(HabitList.Order.BY_COLOR);
return true;
case R.id.actionSortManual:
adapter.setOrder(HabitList.Order.BY_POSITION);
return true;
case R.id.actionSortName:
adapter.setOrder(HabitList.Order.BY_NAME);
return true;
case R.id.actionSortScore:
adapter.setOrder(HabitList.Order.BY_SCORE);
return true;
default:
return false;
}
}
@Override
protected int getMenuResourceId()
{
return R.menu.list_habits;
}
private void toggleShowArchived()
{
showArchived = !showArchived;
preferences.setShowArchived(showArchived);
updateAdapterFilter();
}
private void toggleShowCompleted()
{
showCompleted = !showCompleted;
preferences.setShowCompleted(showCompleted);
updateAdapterFilter();
}
private void updateAdapterFilter()
{
adapter.setFilter(new HabitMatcherBuilder()
.setArchivedAllowed(showArchived)
.setCompletedAllowed(showCompleted)
.build());
adapter.refresh();
}
}

View File

@@ -0,0 +1,197 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list;
import android.content.*;
import android.support.annotation.*;
import android.support.v7.widget.Toolbar;
import android.view.*;
import android.widget.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.activities.habits.list.model.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.*;
import javax.inject.*;
import butterknife.*;
import static org.isoron.uhabits.utils.InterfaceUtils.*;
@ActivityScope
public class ListHabitsRootView extends BaseRootView
implements ModelObservable.Listener, TaskRunner.Listener
{
public static final int MAX_CHECKMARK_COUNT = 60;
@BindView(R.id.listView)
HabitCardListView listView;
@BindView(R.id.llEmpty)
ViewGroup llEmpty;
@BindView(R.id.tvStarEmpty)
TextView tvStarEmpty;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.progressBar)
ProgressBar progressBar;
@BindView(R.id.hintView)
HintView hintView;
@BindView(R.id.header)
HeaderView header;
@NonNull
private final HabitCardListAdapter listAdapter;
private final TaskRunner runner;
@Inject
public ListHabitsRootView(@ActivityContext Context context,
@NonNull HintListFactory hintListFactory,
@NonNull HabitCardListAdapter listAdapter,
@NonNull TaskRunner runner)
{
super(context);
addView(inflate(getContext(), R.layout.list_habits, null));
ButterKnife.bind(this);
this.listAdapter = listAdapter;
listView.setAdapter(listAdapter);
listAdapter.setListView(listView);
this.runner = runner;
progressBar.setIndeterminate(true);
tvStarEmpty.setTypeface(InterfaceUtils.getFontAwesome(getContext()));
String hints[] =
getContext().getResources().getStringArray(R.array.hints);
HintList hintList = hintListFactory.create(hints);
hintView.setHints(hintList);
initToolbar();
}
@NonNull
@Override
public Toolbar getToolbar()
{
return toolbar;
}
@Override
public void onModelChange()
{
updateEmptyView();
}
@Override
public void onTaskFinished(Task task)
{
updateProgressBar();
}
@Override
public void onTaskStarted(Task task)
{
updateProgressBar();
}
public void setController(@NonNull ListHabitsController controller,
@NonNull ListHabitsSelectionMenu menu)
{
HabitCardListController listController =
new HabitCardListController(listAdapter);
listController.setHabitListener(controller);
listController.setSelectionListener(menu);
listView.setController(listController);
menu.setListController(listController);
header.setScrollController(new ScrollableChart.ScrollController() {
@Override
public void onDataOffsetChanged(int newDataOffset)
{
listView.setDataOffset(newDataOffset);
}
});
}
@Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
runner.addListener(this);
updateProgressBar();
listAdapter.getObservable().addListener(this);
}
@Override
protected void onDetachedFromWindow()
{
listAdapter.getObservable().removeListener(this);
runner.removeListener(this);
super.onDetachedFromWindow();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
int count = getCheckmarkCount();
header.setButtonCount(count);
header.setMaxDataOffset(Math.max(MAX_CHECKMARK_COUNT - count, 0));
listView.setCheckmarkCount(count);
super.onSizeChanged(w, h, oldw, oldh);
}
private int getCheckmarkCount()
{
float nameWidth = getDimension(getContext(), R.dimen.habitNameWidth);
float labelWidth = Math.max(getMeasuredWidth() / 3, nameWidth);
float buttonWidth = getDimension(getContext(), R.dimen.checkmarkWidth);
return Math.min(MAX_CHECKMARK_COUNT, Math.max(0,
(int) ((getMeasuredWidth() - labelWidth) / buttonWidth)));
}
private void updateEmptyView()
{
llEmpty.setVisibility(
listAdapter.getItemCount() > 0 ? View.GONE : View.VISIBLE);
}
private void updateProgressBar()
{
postDelayed(() -> {
int activeTaskCount = runner.getActiveTaskCount();
int newVisibility = activeTaskCount > 0 ? VISIBLE : GONE;
if (progressBar.getVisibility() != newVisibility)
progressBar.setVisibility(newVisibility);
}, 500);
}
}

View File

@@ -0,0 +1,373 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list;
import android.app.*;
import android.content.*;
import android.net.*;
import android.support.annotation.*;
import android.support.v7.app.AlertDialog;
import android.text.*;
import android.view.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.common.dialogs.*;
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialog.*;
import org.isoron.uhabits.activities.habits.edit.*;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.utils.*;
import java.io.*;
import java.lang.reflect.*;
import javax.inject.*;
import static android.content.DialogInterface.*;
import static android.view.inputmethod.EditorInfo.*;
@ActivityScope
public class ListHabitsScreen extends BaseScreen
implements CommandRunner.Listener
{
public static final int REQUEST_OPEN_DOCUMENT = 6;
public static final int REQUEST_SETTINGS = 7;
public static final int RESULT_BUG_REPORT = 4;
public static final int RESULT_EXPORT_CSV = 2;
public static final int RESULT_EXPORT_DB = 3;
public static final int RESULT_IMPORT_DATA = 1;
public static final int RESULT_REPAIR_DB = 5;
@Nullable
private ListHabitsController controller;
@NonNull
private final IntentFactory intentFactory;
@NonNull
private final CommandRunner commandRunner;
@NonNull
private final ConfirmDeleteDialogFactory confirmDeleteDialogFactory;
@NonNull
private final ColorPickerDialogFactory colorPickerFactory;
@NonNull
private final EditHabitDialogFactory editHabitDialogFactory;
@NonNull
private final ThemeSwitcher themeSwitcher;
@NonNull
private Preferences prefs;
@Inject
public ListHabitsScreen(@NonNull BaseActivity activity,
@NonNull CommandRunner commandRunner,
@NonNull ListHabitsRootView rootView,
@NonNull IntentFactory intentFactory,
@NonNull ThemeSwitcher themeSwitcher,
@NonNull ConfirmDeleteDialogFactory confirmDeleteDialogFactory,
@NonNull ColorPickerDialogFactory colorPickerFactory,
@NonNull EditHabitDialogFactory editHabitDialogFactory,
@NonNull Preferences prefs)
{
super(activity);
setRootView(rootView);
this.prefs = prefs;
this.colorPickerFactory = colorPickerFactory;
this.commandRunner = commandRunner;
this.confirmDeleteDialogFactory = confirmDeleteDialogFactory;
this.editHabitDialogFactory = editHabitDialogFactory;
this.intentFactory = intentFactory;
this.themeSwitcher = themeSwitcher;
}
public void onAttached()
{
commandRunner.addListener(this);
}
@Override
public void onCommandExecuted(@NonNull Command command,
@Nullable Long refreshKey)
{
if(command.isRemote()) return;
showMessage(command.getExecuteStringId());
}
public void onDettached()
{
commandRunner.removeListener(this);
}
@Override
public void onResult(int requestCode, int resultCode, Intent data)
{
if (requestCode == REQUEST_OPEN_DOCUMENT)
onOpenDocumentResult(resultCode, data);
if (requestCode == REQUEST_SETTINGS) onSettingsResult(resultCode);
}
public void setController(@Nullable ListHabitsController controller)
{
this.controller = controller;
}
public void showAboutScreen()
{
Intent intent = intentFactory.startAboutActivity(activity);
activity.startActivity(intent);
}
/**
* Displays a {@link ColorPickerDialog} to the user.
* <p>
* The selected color on the dialog is the color of the given habit.
*
* @param habit the habit
* @param callback
*/
public void showColorPicker(@NonNull Habit habit,
@NonNull OnColorSelectedListener callback)
{
ColorPickerDialog picker = colorPickerFactory.create(habit.getColor());
picker.setListener(callback);
activity.showDialog(picker, "picker");
}
public void showCreateHabitScreen()
{
if(!prefs.isNumericalHabitsFeatureEnabled())
{
showCreateBooleanHabitScreen();
return;
}
Dialog dialog = new AlertDialog.Builder(activity)
.setTitle("Type of habit")
.setItems(R.array.habitTypes, (d, which) -> {
if(which == 0) showCreateBooleanHabitScreen();
else showCreateNumericalHabitScreen();
})
.create();
dialog.show();
}
private void showCreateNumericalHabitScreen()
{
EditHabitDialog dialog;
dialog = editHabitDialogFactory.createNumerical();
activity.showDialog(dialog, "editHabit");
}
public void showCreateBooleanHabitScreen()
{
EditHabitDialog dialog;
dialog = editHabitDialogFactory.createBoolean();
activity.showDialog(dialog, "editHabit");
}
public void showDeleteConfirmationScreen(ConfirmDeleteDialog.Callback callback)
{
activity.showDialog(confirmDeleteDialogFactory.create(callback));
}
public void showEditHabitScreen(Habit habit)
{
EditHabitDialog dialog;
dialog = editHabitDialogFactory.edit(habit);
activity.showDialog(dialog, "editNumericalHabit");
}
public void showFAQScreen()
{
Intent intent = intentFactory.viewFAQ(activity);
activity.startActivity(intent);
}
public void showHabitScreen(@NonNull Habit habit)
{
Intent intent = intentFactory.startShowHabitActivity(activity, habit);
activity.startActivity(intent);
}
public void showImportScreen()
{
Intent intent = intentFactory.openDocument();
activity.startActivityForResult(intent, REQUEST_OPEN_DOCUMENT);
}
public void showIntroScreen()
{
Intent intent = intentFactory.startIntroActivity(activity);
activity.startActivity(intent);
}
public void showNumberPicker(double value,
@NonNull String unit,
@NonNull NumberPickerCallback callback)
{
LayoutInflater inflater = activity.getLayoutInflater();
View view = inflater.inflate(R.layout.number_picker_dialog, null);
final NumberPicker picker;
final NumberPicker picker2;
final TextView tvUnit;
picker = (NumberPicker) view.findViewById(R.id.picker);
picker2 = (NumberPicker) view.findViewById(R.id.picker2);
tvUnit = (TextView) view.findViewById(R.id.tvUnit);
int intValue = (int) Math.round(value * 100);
picker.setMinValue(0);
picker.setMaxValue(Integer.MAX_VALUE / 100);
picker.setValue(intValue / 100);
picker.setWrapSelectorWheel(false);
picker2.setMinValue(0);
picker2.setMaxValue(19);
picker2.setFormatter(v -> String.format("%02d", 5 * v));
picker2.setValue((intValue % 100) / 5);
refreshInitialValue(picker2);
tvUnit.setText(unit);
AlertDialog dialog = new AlertDialog.Builder(activity)
.setView(view)
.setTitle(R.string.change_value)
.setPositiveButton(android.R.string.ok, (d, which) ->
{
picker.clearFocus();
double v = picker.getValue() + 0.05 * picker2.getValue();
callback.onNumberPicked(v);
})
.create();
InterfaceUtils.setupEditorAction(picker, (v, actionId, event) ->
{
if (actionId == IME_ACTION_DONE)
dialog.getButton(BUTTON_POSITIVE).performClick();
return false;
});
dialog.show();
}
private void refreshInitialValue(NumberPicker picker2)
{
// Workaround for a bug on Android:
// https://code.google.com/p/android/issues/detail?id=35482
try
{
Field f = NumberPicker.class.getDeclaredField("mInputText");
f.setAccessible(true);
EditText inputText = (EditText) f.get(picker2);
inputText.setFilters(new InputFilter[0]);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
public void showSettingsScreen()
{
Intent intent = intentFactory.startSettingsActivity(activity);
activity.startActivityForResult(intent, REQUEST_SETTINGS);
}
public void toggleNightMode()
{
themeSwitcher.toggleNightMode();
activity.restartWithFade(ListHabitsActivity.class);
}
private void onOpenDocumentResult(int resultCode, Intent data)
{
if (controller == null) return;
if (resultCode != Activity.RESULT_OK) return;
try
{
Uri uri = data.getData();
ContentResolver cr = activity.getContentResolver();
InputStream is = cr.openInputStream(uri);
File cacheDir = activity.getExternalCacheDir();
File tempFile = File.createTempFile("import", "", cacheDir);
FileUtils.copy(is, tempFile);
controller.onImportData(tempFile, () -> tempFile.delete());
}
catch (IOException e)
{
showMessage(R.string.could_not_import);
e.printStackTrace();
}
}
private void onSettingsResult(int resultCode)
{
if (controller == null) return;
switch (resultCode)
{
case RESULT_IMPORT_DATA:
showImportScreen();
break;
case RESULT_EXPORT_CSV:
controller.onExportCSV();
break;
case RESULT_EXPORT_DB:
controller.onExportDB();
break;
case RESULT_BUG_REPORT:
controller.onSendBugReport();
break;
case RESULT_REPAIR_DB:
controller.onRepairDB();
break;
}
}
public interface NumberPickerCallback
{
void onNumberPicked(double newValue);
}
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list;
import android.support.annotation.*;
import android.view.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.activities.habits.list.model.*;
import java.util.*;
import javax.inject.*;
@ActivityScope
public class ListHabitsSelectionMenu extends BaseSelectionMenu
implements HabitCardListController.SelectionListener
{
@NonNull
private final ListHabitsScreen screen;
@NonNull
CommandRunner commandRunner;
@NonNull
private final HabitCardListAdapter listAdapter;
@Nullable
private HabitCardListController listController;
@NonNull
private final HabitList habitList;
@Inject
public ListHabitsSelectionMenu(@NonNull HabitList habitList,
@NonNull ListHabitsScreen screen,
@NonNull HabitCardListAdapter listAdapter,
@NonNull CommandRunner commandRunner)
{
this.habitList = habitList;
this.screen = screen;
this.listAdapter = listAdapter;
this.commandRunner = commandRunner;
}
@Override
public void onFinish()
{
if (listController != null) listController.onSelectionFinished();
super.onFinish();
}
@Override
public boolean onItemClicked(@NonNull MenuItem item)
{
List<Habit> selected = listAdapter.getSelected();
if (selected.isEmpty()) return false;
Habit firstHabit = selected.get(0);
switch (item.getItemId())
{
case R.id.action_edit_habit:
showEditScreen(firstHabit);
finish();
return true;
case R.id.action_archive_habit:
performArchive(selected);
finish();
return true;
case R.id.action_unarchive_habit:
performUnarchive(selected);
finish();
return true;
case R.id.action_delete:
performDelete(selected);
return true;
case R.id.action_color:
showColorPicker(selected, firstHabit);
return true;
default:
return false;
}
}
@Override
public boolean onPrepare(@NonNull Menu menu)
{
List<Habit> selected = listAdapter.getSelected();
boolean showEdit = (selected.size() == 1);
boolean showArchive = true;
boolean showUnarchive = true;
for (Habit h : selected)
{
if (h.isArchived()) showArchive = false;
else showUnarchive = false;
}
MenuItem itemEdit = menu.findItem(R.id.action_edit_habit);
MenuItem itemColor = menu.findItem(R.id.action_color);
MenuItem itemArchive = menu.findItem(R.id.action_archive_habit);
MenuItem itemUnarchive = menu.findItem(R.id.action_unarchive_habit);
itemColor.setVisible(true);
itemEdit.setVisible(showEdit);
itemArchive.setVisible(showArchive);
itemUnarchive.setVisible(showUnarchive);
setTitle(Integer.toString(selected.size()));
return true;
}
@Override
public void onSelectionChange()
{
invalidate();
}
@Override
public void onSelectionFinish()
{
finish();
}
@Override
public void onSelectionStart()
{
screen.startSelection();
}
public void setListController(HabitCardListController listController)
{
this.listController = listController;
}
@Override
protected int getResourceId()
{
return R.menu.list_habits_selection;
}
private void performArchive(@NonNull List<Habit> selected)
{
commandRunner.execute(new ArchiveHabitsCommand(habitList, selected),
null);
}
private void performDelete(@NonNull List<Habit> selected)
{
screen.showDeleteConfirmationScreen(() -> {
listAdapter.performRemove(selected);
commandRunner.execute(new DeleteHabitsCommand(habitList, selected),
null);
finish();
});
}
private void performUnarchive(@NonNull List<Habit> selected)
{
commandRunner.execute(new UnarchiveHabitsCommand(habitList, selected),
null);
}
private void showColorPicker(@NonNull List<Habit> selected,
@NonNull Habit firstHabit)
{
screen.showColorPicker(firstHabit, color -> {
commandRunner.execute(
new ChangeHabitColorCommand(habitList, selected, color), null);
finish();
});
}
private void showEditScreen(@NonNull Habit firstHabit)
{
screen.showEditHabitScreen(firstHabit);
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.controllers;
import android.support.annotation.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*;
@AutoFactory
public class CheckmarkButtonController
{
@Nullable
private CheckmarkButtonView view;
@Nullable
private Listener listener;
@NonNull
private final Preferences prefs;
@NonNull
private Habit habit;
private long timestamp;
public CheckmarkButtonController(@Provided @NonNull Preferences prefs,
@NonNull Habit habit,
long timestamp)
{
this.habit = habit;
this.timestamp = timestamp;
this.prefs = prefs;
}
public void onClick()
{
if (prefs.isShortToggleEnabled()) performToggle();
else performInvalidToggle();
}
public boolean onLongClick()
{
performToggle();
return true;
}
public void performInvalidToggle()
{
if (listener != null) listener.onInvalidToggle();
}
public void performToggle()
{
if (view != null) view.toggle();
if (listener != null) listener.onToggle(habit, timestamp);
}
public void setListener(@Nullable Listener listener)
{
this.listener = listener;
}
public void setView(@Nullable CheckmarkButtonView view)
{
this.view = view;
}
public interface Listener
{
/**
* Called when the user's attempt to perform a toggle is rejected.
*/
void onInvalidToggle();
void onToggle(@NonNull Habit habit, long timestamp);
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.controllers;
import android.support.annotation.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.models.*;
public class HabitCardController implements HabitCardView.Controller
{
@Nullable
private HabitCardView view;
@Nullable
private Listener listener;
@Override
public void onEdit(@NonNull Habit habit, long timestamp)
{
if(listener != null) listener.onEdit(habit, timestamp);
}
@Override
public void onInvalidEdit()
{
if(listener != null) listener.onInvalidEdit();
}
@Override
public void onInvalidToggle()
{
if (listener != null) listener.onInvalidToggle();
}
@Override
public void onToggle(@NonNull Habit habit, long timestamp)
{
if (view != null) view.triggerRipple(timestamp);
if (listener != null) listener.onToggle(habit, timestamp);
}
public void setListener(@Nullable Listener listener)
{
this.listener = listener;
}
public void setView(@Nullable HabitCardView view)
{
this.view = view;
}
public interface Listener extends CheckmarkButtonController.Listener,
NumberButtonController.Listener
{
}
}

View File

@@ -0,0 +1,313 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.controllers;
import android.support.annotation.*;
import org.isoron.uhabits.activities.habits.list.model.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.models.*;
/**
* Controller responsible for receiving and processing the events generated by a
* HabitListView. These include selecting and reordering items, toggling
* checkmarks and clicking habits.
*/
public class HabitCardListController implements HabitCardListView.Controller
{
private final Mode NORMAL_MODE = new NormalMode();
private final Mode SELECTION_MODE = new SelectionMode();
@NonNull
private final HabitCardListAdapter adapter;
@Nullable
private HabitListener habitListener;
@Nullable
private SelectionListener selectionListener;
@NonNull
private Mode activeMode;
public HabitCardListController(@NonNull HabitCardListAdapter adapter)
{
this.adapter = adapter;
this.activeMode = new NormalMode();
}
/**
* Called when the user drags a habit and drops it somewhere. Note that the
* dragging operation is already complete.
*
* @param from the original position of the habit
* @param to the position where the habit was released
*/
@Override
public void drop(int from, int to)
{
if (from == to) return;
cancelSelection();
Habit habitFrom = adapter.getItem(from);
Habit habitTo = adapter.getItem(to);
adapter.performReorder(from, to);
if (habitListener != null)
habitListener.onHabitReorder(habitFrom, habitTo);
}
@Override
public void onEdit(@NonNull Habit habit, long timestamp)
{
if (habitListener != null) habitListener.onEdit(habit, timestamp);
}
@Override
public void onInvalidEdit()
{
if (habitListener != null) habitListener.onInvalidEdit();
}
/**
* Called when the user attempts to perform a toggle, but attempt is
* rejected.
*/
@Override
public void onInvalidToggle()
{
if (habitListener != null) habitListener.onInvalidToggle();
}
/**
* Called when the user clicks at some item.
*
* @param position the position of the clicked item
*/
@Override
public void onItemClick(int position)
{
activeMode.onItemClick(position);
}
/**
* Called when the user long clicks at some item.
*
* @param position the position of the clicked item
*/
@Override
public void onItemLongClick(int position)
{
activeMode.onItemLongClick(position);
}
/**
* Called when the selection operation is cancelled externally, by something
* other than this controller. This happens, for example, when the user
* presses the back button.
*/
public void onSelectionFinished()
{
cancelSelection();
}
/**
* Called when the user wants to toggle a checkmark.
*
* @param habit the habit of the checkmark
* @param timestamp the timestamps of the checkmark
*/
@Override
public void onToggle(@NonNull Habit habit, long timestamp)
{
if (habitListener != null) habitListener.onToggle(habit, timestamp);
}
public void setHabitListener(@Nullable HabitListener habitListener)
{
this.habitListener = habitListener;
}
public void setSelectionListener(@Nullable SelectionListener listener)
{
this.selectionListener = listener;
}
/**
* Called when the user starts dragging an item.
*
* @param position the position of the habit dragged
*/
@Override
public void startDrag(int position)
{
activeMode.startDrag(position);
}
/**
* Selects or deselects the item at a given position
*
* @param position the position of the item to be selected/deselected
*/
protected void toggleSelection(int position)
{
adapter.toggleSelection(position);
activeMode = adapter.isSelectionEmpty() ? NORMAL_MODE : SELECTION_MODE;
}
/**
* Marks all items as not selected and finishes the selection operation.
*/
private void cancelSelection()
{
adapter.clearSelection();
activeMode = new NormalMode();
if (selectionListener != null) selectionListener.onSelectionFinish();
}
public interface HabitListener extends CheckmarkButtonController.Listener,
NumberButtonController.Listener
{
/**
* Called when the user clicks a habit.
*
* @param habit the habit clicked
*/
void onHabitClick(@NonNull Habit habit);
/**
* Called when the user wants to change the position of a habit on the
* list.
*
* @param from habit to be moved
* @param to habit that currently occupies the desired position
*/
void onHabitReorder(@NonNull Habit from, @NonNull Habit to);
}
/**
* A Mode describes the behaviour of the list upon clicking, long clicking
* and dragging an item. This depends on whether some items are already
* selected or not.
*/
private interface Mode
{
void onItemClick(int position);
boolean onItemLongClick(int position);
void startDrag(int position);
}
public interface SelectionListener
{
/**
* Called when the user changes the list of selected item. This is only
* called if there were previously selected items. If the selection was
* previously empty, then onHabitSelectionStart is called instead.
*/
void onSelectionChange();
/**
* Called when the user deselects all items or cancels the selection.
*/
void onSelectionFinish();
/**
* Called after the user selects the first item.
*/
void onSelectionStart();
}
/**
* Mode activated when there are no items selected. Clicks trigger habit
* click. Long clicks start selection.
*/
class NormalMode implements Mode
{
@Override
public void onItemClick(int position)
{
Habit habit = adapter.getItem(position);
if (habitListener != null) habitListener.onHabitClick(habit);
}
@Override
public boolean onItemLongClick(int position)
{
startSelection(position);
return true;
}
@Override
public void startDrag(int position)
{
startSelection(position);
}
protected void startSelection(int position)
{
toggleSelection(position);
activeMode = SELECTION_MODE;
if (selectionListener != null) selectionListener.onSelectionStart();
}
}
/**
* Mode activated when some items are already selected.
* <p>
* Clicks toggle item selection. Long clicks select more items.
*/
class SelectionMode implements Mode
{
@Override
public void onItemClick(int position)
{
toggleSelection(position);
notifyListener();
}
@Override
public boolean onItemLongClick(int position)
{
toggleSelection(position);
notifyListener();
return true;
}
@Override
public void startDrag(int position)
{
toggleSelection(position);
notifyListener();
}
protected void notifyListener()
{
if (selectionListener == null) return;
if (activeMode == SELECTION_MODE)
selectionListener.onSelectionChange();
else selectionListener.onSelectionFinish();
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.controllers;
import android.support.annotation.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*;
@AutoFactory
public class NumberButtonController
{
@Nullable
private NumberButtonView view;
@Nullable
private Listener listener;
@NonNull
private final Preferences prefs;
@NonNull
private Habit habit;
private long timestamp;
public NumberButtonController(@Provided @NonNull Preferences prefs,
@NonNull Habit habit,
long timestamp)
{
this.habit = habit;
this.timestamp = timestamp;
this.prefs = prefs;
}
public void onClick()
{
if (prefs.isShortToggleEnabled()) performEdit();
else performInvalidToggle();
}
public boolean onLongClick()
{
performEdit();
return true;
}
public void performInvalidToggle()
{
if (listener != null) listener.onInvalidEdit();
}
public void performEdit()
{
if (listener != null) listener.onEdit(habit, timestamp);
}
public void setListener(@Nullable Listener listener)
{
this.listener = listener;
}
public void setView(@Nullable NumberButtonView view)
{
this.view = view;
}
public interface Listener
{
/**
* Called when the user's attempt to edit the value is rejected.
*/
void onInvalidEdit();
/**
* Called when a the user's attempt to edit the value has been accepted.
* @param habit the habit being edited
* @param timestamp the timestamp being edited
*/
void onEdit(@NonNull Habit habit, long timestamp);
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides controllers that are specific for {@link org.isoron.uhabits.activities.habits.list.ListHabitsActivity}.
*/
package org.isoron.uhabits.activities.habits.list.controllers;

View File

@@ -0,0 +1,322 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.model;
import android.support.annotation.*;
import android.support.v7.widget.*;
import android.view.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.habits.list.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.utils.*;
import java.util.*;
import javax.inject.*;
/**
* Provides data that backs a {@link HabitCardListView}.
* <p>
* The data if fetched and cached by a {@link HabitCardListCache}. This adapter
* also holds a list of items that have been selected.
*/
@ActivityScope
public class HabitCardListAdapter
extends RecyclerView.Adapter<HabitCardViewHolder>
implements HabitCardListCache.Listener, MidnightTimer.MidnightListener
{
@NonNull
private ModelObservable observable;
@Nullable
private HabitCardListView listView;
@NonNull
private final LinkedList<Habit> selected;
@NonNull
private final HabitCardListCache cache;
@NonNull
private Preferences preferences;
private final MidnightTimer midnightTimer;
@Inject
public HabitCardListAdapter(@NonNull HabitCardListCache cache,
@NonNull Preferences preferences,
@NonNull MidnightTimer midnightTimer)
{
this.preferences = preferences;
this.selected = new LinkedList<>();
this.observable = new ModelObservable();
this.cache = cache;
this.midnightTimer = midnightTimer;
cache.setListener(this);
cache.setCheckmarkCount(ListHabitsRootView.MAX_CHECKMARK_COUNT);
cache.setOrder(preferences.getDefaultOrder());
setHasStableIds(true);
}
@Override
public void atMidnight()
{
cache.refreshAllHabits();
}
public void cancelRefresh()
{
cache.cancelTasks();
}
/**
* Sets all items as not selected.
*/
public void clearSelection()
{
selected.clear();
notifyDataSetChanged();
}
/**
* Returns the item that occupies a certain position on the list
*
* @param position position of the item
* @return the item at given position
* @throws IndexOutOfBoundsException if position is not valid
*/
@Deprecated
@NonNull
public Habit getItem(int position)
{
return cache.getHabitByPosition(position);
}
@Override
public int getItemCount()
{
return cache.getHabitCount();
}
@Override
public long getItemId(int position)
{
return getItem(position).getId();
}
@NonNull
public ModelObservable getObservable()
{
return observable;
}
@NonNull
public List<Habit> getSelected()
{
return new LinkedList<>(selected);
}
/**
* Returns whether list of selected items is empty.
*
* @return true if selection is empty, false otherwise
*/
public boolean isSelectionEmpty()
{
return selected.isEmpty();
}
public boolean isSortable()
{
return cache.getOrder() == HabitList.Order.BY_POSITION;
}
/**
* Notify the adapter that it has been attached to a ListView.
*/
public void onAttached()
{
cache.onAttached();
midnightTimer.addListener(this);
}
@Override
public void onBindViewHolder(@Nullable HabitCardViewHolder holder,
int position)
{
if (holder == null) return;
if (listView == null) return;
Habit habit = cache.getHabitByPosition(position);
double score = cache.getScore(habit.getId());
int checkmarks[] = cache.getCheckmarks(habit.getId());
boolean selected = this.selected.contains(habit);
listView.bindCardView(holder, habit, score, checkmarks, selected);
}
@Override
public void onViewAttachedToWindow(@Nullable HabitCardViewHolder holder)
{
if (listView == null) return;
listView.attachCardView(holder);
}
@Override
public void onViewDetachedFromWindow(@Nullable HabitCardViewHolder holder)
{
if (listView == null) return;
listView.detachCardView(holder);
}
@Override
public HabitCardViewHolder onCreateViewHolder(ViewGroup parent,
int viewType)
{
if (listView == null) return null;
View view = listView.createCardView();
return new HabitCardViewHolder(view);
}
/**
* Notify the adapter that it has been detached from a ListView.
*/
public void onDetached()
{
cache.onDetached();
midnightTimer.removeListener(this);
}
@Override
public void onItemChanged(int position)
{
notifyItemChanged(position);
observable.notifyListeners();
}
@Override
public void onItemInserted(int position)
{
notifyItemInserted(position);
observable.notifyListeners();
}
@Override
public void onItemMoved(int fromPosition, int toPosition)
{
notifyItemMoved(fromPosition, toPosition);
observable.notifyListeners();
}
@Override
public void onItemRemoved(int position)
{
notifyItemRemoved(position);
observable.notifyListeners();
}
@Override
public void onRefreshFinished()
{
observable.notifyListeners();
}
/**
* Removes a list of habits from the adapter.
* <p>
* Note that this only has effect on the adapter cache. The database is not
* modified, and the change is lost when the cache is refreshed. This method
* is useful for making the ListView more responsive: while we wait for the
* database operation to finish, the cache can be modified to reflect the
* changes immediately.
*
* @param habits list of habits to be removed
*/
public void performRemove(List<Habit> habits)
{
for (Habit h : habits)
cache.remove(h.getId());
}
/**
* Changes the order of habits on the adapter.
* <p>
* Note that this only has effect on the adapter cache. The database is not
* modified, and the change is lost when the cache is refreshed. This method
* is useful for making the ListView more responsive: while we wait for the
* database operation to finish, the cache can be modified to reflect the
* changes immediately.
*
* @param from the habit that should be moved
* @param to the habit that currently occupies the desired position
*/
public void performReorder(int from, int to)
{
cache.reorder(from, to);
}
public void refresh()
{
cache.refreshAllHabits();
}
public void setFilter(HabitMatcher matcher)
{
cache.setFilter(matcher);
}
/**
* Sets the HabitCardListView that this adapter will provide data for.
* <p>
* This object will be used to generated new HabitCardViews, upon demand.
*
* @param listView the HabitCardListView associated with this adapter
*/
public void setListView(@Nullable HabitCardListView listView)
{
this.listView = listView;
}
public void setOrder(HabitList.Order order)
{
cache.setOrder(order);
preferences.setDefaultOrder(order);
}
/**
* Selects or deselects the item at a given position.
*
* @param position position of the item to be toggled
*/
public void toggleSelection(int position)
{
Habit h = getItem(position);
int k = selected.indexOf(h);
if (k < 0) selected.add(h);
else selected.remove(h);
notifyDataSetChanged();
}
}

View File

@@ -0,0 +1,408 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.model;
import android.support.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.*;
import java.util.*;
import javax.inject.*;
/**
* A HabitCardListCache fetches and keeps a cache of all the data necessary to
* render a HabitCardListView.
* <p>
* This is needed since performing database lookups during scrolling can make
* the ListView very slow. It also registers itself as an observer of the
* models, in order to update itself automatically.
* <p>
* Note that this class is singleton-scoped, therefore it is shared among all
* activities.
*/
@AppScope
public class HabitCardListCache implements CommandRunner.Listener
{
private int checkmarkCount;
private Task currentFetchTask;
@NonNull
private Listener listener;
@NonNull
private CacheData data;
@NonNull
private HabitList allHabits;
@NonNull
private HabitList filteredHabits;
private final TaskRunner taskRunner;
private final CommandRunner commandRunner;
@Inject
public HabitCardListCache(@NonNull HabitList allHabits,
@NonNull CommandRunner commandRunner,
@NonNull TaskRunner taskRunner)
{
this.allHabits = allHabits;
this.commandRunner = commandRunner;
this.filteredHabits = allHabits;
this.taskRunner = taskRunner;
this.listener = new Listener() {};
data = new CacheData();
}
public void cancelTasks()
{
if (currentFetchTask != null) currentFetchTask.cancel();
}
public int[] getCheckmarks(long habitId)
{
return data.checkmarks.get(habitId);
}
/**
* Returns the habits that occupies a certain position on the list.
*
* @param position the position of the habit
* @return the habit at given position
* @throws IndexOutOfBoundsException if position is not valid
*/
@NonNull
public Habit getHabitByPosition(int position)
{
return data.habits.get(position);
}
public int getHabitCount()
{
return data.habits.size();
}
public HabitList.Order getOrder()
{
return filteredHabits.getOrder();
}
public double getScore(long habitId)
{
return data.scores.get(habitId);
}
public void onAttached()
{
refreshAllHabits();
commandRunner.addListener(this);
}
@Override
public void onCommandExecuted(@NonNull Command command,
@Nullable Long refreshKey)
{
if (refreshKey == null) refreshAllHabits();
else refreshHabit(refreshKey);
}
public void onDetached()
{
commandRunner.removeListener(this);
}
public void refreshAllHabits()
{
if (currentFetchTask != null) currentFetchTask.cancel();
currentFetchTask = new RefreshTask();
taskRunner.execute(currentFetchTask);
}
public void refreshHabit(long id)
{
taskRunner.execute(new RefreshTask(id));
}
public void remove(@NonNull Long id)
{
Habit h = data.id_to_habit.get(id);
if (h == null) return;
int position = data.habits.indexOf(h);
data.habits.remove(position);
data.id_to_habit.remove(id);
data.checkmarks.remove(id);
data.scores.remove(id);
listener.onItemRemoved(position);
}
public void reorder(int from, int to)
{
Habit fromHabit = data.habits.get(from);
data.habits.remove(from);
data.habits.add(to, fromHabit);
listener.onItemMoved(from, to);
}
public void setCheckmarkCount(int checkmarkCount)
{
this.checkmarkCount = checkmarkCount;
}
public void setFilter(HabitMatcher matcher)
{
filteredHabits = allHabits.getFiltered(matcher);
}
public void setListener(@NonNull Listener listener)
{
this.listener = listener;
}
public void setOrder(HabitList.Order order)
{
allHabits.setOrder(order);
filteredHabits.setOrder(order);
refreshAllHabits();
}
/**
* Interface definition for a callback to be invoked when the data on the
* cache has been modified.
*/
public interface Listener
{
default void onItemChanged(int position) {}
default void onItemInserted(int position) {}
default void onItemMoved(int oldPosition, int newPosition) {}
default void onItemRemoved(int position) {}
default void onRefreshFinished() {}
}
private class CacheData
{
@NonNull
public HashMap<Long, Habit> id_to_habit;
@NonNull
public List<Habit> habits;
@NonNull
public HashMap<Long, int[]> checkmarks;
@NonNull
public HashMap<Long, Double> scores;
/**
* Creates a new CacheData without any content.
*/
public CacheData()
{
id_to_habit = new HashMap<>();
habits = new LinkedList<>();
checkmarks = new HashMap<>();
scores = new HashMap<>();
}
public void copyCheckmarksFrom(@NonNull CacheData oldData)
{
int[] empty = new int[checkmarkCount];
for (Long id : id_to_habit.keySet())
{
if (oldData.checkmarks.containsKey(id))
checkmarks.put(id, oldData.checkmarks.get(id));
else checkmarks.put(id, empty);
}
}
public void copyScoresFrom(@NonNull CacheData oldData)
{
for (Long id : id_to_habit.keySet())
{
if (oldData.scores.containsKey(id))
scores.put(id, oldData.scores.get(id));
else scores.put(id, 0.0);
}
}
public void fetchHabits()
{
for (Habit h : filteredHabits)
{
habits.add(h);
id_to_habit.put(h.getId(), h);
}
}
}
private class RefreshTask implements Task
{
@NonNull
private CacheData newData;
@Nullable
private Long targetId;
private boolean isCancelled;
private TaskRunner runner;
public RefreshTask()
{
newData = new CacheData();
targetId = null;
isCancelled = false;
}
public RefreshTask(long targetId)
{
newData = new CacheData();
this.targetId = targetId;
}
@Override
public void cancel()
{
isCancelled = true;
}
@Override
public void doInBackground()
{
newData.fetchHabits();
newData.copyScoresFrom(data);
newData.copyCheckmarksFrom(data);
long day = DateUtils.millisecondsInOneDay;
long dateTo = DateUtils.getStartOfDay(DateUtils.getLocalTime());
long dateFrom = dateTo - (checkmarkCount - 1) * day;
runner.publishProgress(this, -1);
for (int position = 0; position < newData.habits.size(); position++)
{
if (isCancelled) return;
Habit habit = newData.habits.get(position);
Long id = habit.getId();
if (targetId != null && !targetId.equals(id)) continue;
newData.scores.put(id, habit.getScores().getTodayValue());
newData.checkmarks.put(id,
habit.getCheckmarks().getValues(dateFrom, dateTo));
runner.publishProgress(this, position);
}
}
@Override
public void onAttached(@NonNull TaskRunner runner)
{
this.runner = runner;
}
@Override
public void onPostExecute()
{
currentFetchTask = null;
listener.onRefreshFinished();
}
@Override
public void onProgressUpdate(int currentPosition)
{
if (currentPosition < 0) processRemovedHabits();
else processPosition(currentPosition);
}
private void performInsert(Habit habit, int position)
{
Long id = habit.getId();
data.habits.add(position, habit);
data.id_to_habit.put(id, habit);
data.scores.put(id, newData.scores.get(id));
data.checkmarks.put(id, newData.checkmarks.get(id));
listener.onItemInserted(position);
}
private void performMove(Habit habit, int fromPosition, int toPosition)
{
data.habits.remove(fromPosition);
data.habits.add(toPosition, habit);
listener.onItemMoved(fromPosition, toPosition);
}
private void performUpdate(Long id, int position)
{
double oldScore = data.scores.get(id);
int[] oldCheckmarks = data.checkmarks.get(id);
double newScore = newData.scores.get(id);
int[] newCheckmarks = newData.checkmarks.get(id);
boolean unchanged = true;
if (oldScore != newScore) unchanged = false;
if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false;
if (unchanged) return;
data.scores.put(id, newScore);
data.checkmarks.put(id, newCheckmarks);
listener.onItemChanged(position);
}
private void processPosition(int currentPosition)
{
Habit habit = newData.habits.get(currentPosition);
Long id = habit.getId();
int prevPosition = data.habits.indexOf(habit);
if (prevPosition < 0) performInsert(habit, currentPosition);
else if (prevPosition == currentPosition)
performUpdate(id, currentPosition);
else performMove(habit, prevPosition, currentPosition);
}
private void processRemovedHabits()
{
Set<Long> before = data.id_to_habit.keySet();
Set<Long> after = newData.id_to_habit.keySet();
Set<Long> removed = new TreeSet<>(before);
removed.removeAll(after);
for (Long id : removed) remove(id);
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.model;
import android.support.v7.widget.*;
import android.view.*;
public class HabitCardViewHolder extends RecyclerView.ViewHolder
{
public HabitCardViewHolder(View itemView)
{
super(itemView);
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.model;
import android.support.annotation.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.utils.*;
/**
* Provides a list of hints to be shown at the application startup, and takes
* care of deciding when a new hint should be shown.
*/
@AutoFactory
public class HintList
{
private final Preferences prefs;
@NonNull
private final String[] hints;
/**
* Constructs a new list containing the provided hints.
*
* @param hints initial list of hints
*/
public HintList(@Provided @NonNull Preferences prefs,
@NonNull String hints[])
{
this.prefs = prefs;
this.hints = hints;
}
/**
* Returns a new hint to be shown to the user.
* <p>
* The hint returned is marked as read on the list, and will not be returned
* again. In case all hints have already been read, and there is nothing
* left, returns null.
*
* @return the next hint to be shown, or null if none
*/
public String pop()
{
int next = prefs.getLastHintNumber() + 1;
if (next >= hints.length) return null;
prefs.updateLastHint(next, DateUtils.getStartOfToday());
return hints[next];
}
/**
* Returns whether it is time to show a new hint or not.
*
* @return true if hint should be shown, false otherwise
*/
public boolean shouldShow()
{
long lastHintTimestamp = prefs.getLastHintTimestamp();
return (DateUtils.getStartOfToday() > lastHintTimestamp);
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides models that are specific for {@link org.isoron.uhabits.activities.habits.list.ListHabitsActivity}.
*/
package org.isoron.uhabits.activities.habits.list.model;

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides acitivity for listing habits and related classes.
*/
package org.isoron.uhabits.activities.habits.list;

View File

@@ -0,0 +1,145 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.content.*;
import android.content.res.*;
import android.graphics.*;
import android.support.annotation.*;
import android.text.*;
import android.util.*;
import android.view.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.utils.*;
import static android.view.View.MeasureSpec.*;
import static org.isoron.uhabits.models.Checkmark.*;
import static org.isoron.uhabits.utils.AttributeSetUtils.*;
import static org.isoron.uhabits.utils.InterfaceUtils.getDimension;
import static org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome;
public class CheckmarkButtonView extends View
{
private int color;
private int value;
private StyledResources styledRes;
private TextPaint paint;
private int lowContrastColor;
private RectF rect;
public CheckmarkButtonView(@Nullable Context context)
{
super(context);
init();
}
public CheckmarkButtonView(@Nullable Context ctx, @Nullable AttributeSet attrs)
{
super(ctx, attrs);
init();
if(ctx == null) throw new IllegalStateException();
if(attrs == null) throw new IllegalStateException();
int paletteColor = getIntAttribute(ctx, attrs, "color", 0);
setColor(ColorUtils.getAndroidTestColor(paletteColor));
int value = getIntAttribute(ctx, attrs, "value", 0);
setValue(value);
}
public void setColor(int color)
{
this.color = color;
postInvalidate();
}
public void setController(final CheckmarkButtonController controller)
{
setOnClickListener(v -> controller.onClick());
setOnLongClickListener(v -> controller.onLongClick());
}
public void setValue(int value)
{
this.value = value;
postInvalidate();
}
public void toggle()
{
value = (value == CHECKED_EXPLICITLY ? UNCHECKED : CHECKED_EXPLICITLY);
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
Resources resources = getResources();
paint.setColor(value == CHECKED_EXPLICITLY ? color : lowContrastColor);
int id = (value == UNCHECKED ? R.string.fa_times : R.string.fa_check);
String label = resources.getString(id);
float em = paint.measureText("m");
rect.set(0, 0, getWidth(), getHeight());
rect.offset(0, 0.4f * em);
canvas.drawText(label, rect.centerX(), rect.centerY(), paint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
Resources res = getResources();
int height = res.getDimensionPixelSize(R.dimen.checkmarkHeight);
int width = res.getDimensionPixelSize(R.dimen.checkmarkWidth);
widthMeasureSpec = makeMeasureSpec(width, EXACTLY);
heightMeasureSpec = makeMeasureSpec(height, EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void init()
{
setFocusable(false);
styledRes = new StyledResources(getContext());
paint = new TextPaint();
paint.setTypeface(getFontAwesome(getContext()));
paint.setAntiAlias(true);
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(getDimension(getContext(), R.dimen.smallTextSize));
rect = new RectF();
color = Color.BLACK;
lowContrastColor = styledRes.getColor(R.attr.lowContrastTextColor);
}
}

View File

@@ -0,0 +1,245 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.content.*;
import android.support.annotation.*;
import android.util.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.habits.list.*;
import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.utils.*;
import java.util.*;
import static android.view.View.MeasureSpec.*;
import static org.isoron.uhabits.utils.AttributeSetUtils.*;
import static org.isoron.uhabits.utils.ColorUtils.*;
import static org.isoron.uhabits.utils.InterfaceUtils.getDimension;
public class CheckmarkPanelView extends LinearLayout
implements Preferences.Listener
{
private static final int LEFT_TO_RIGHT = 0;
private static final int RIGHT_TO_LEFT = 1;
@Nullable
private Preferences prefs;
private int values[];
private int nButtons;
private int color;
private Controller controller;
@NonNull
private Habit habit;
private int dataOffset;
public CheckmarkPanelView(Context context)
{
super(context);
init();
}
public CheckmarkPanelView(Context ctx, AttributeSet attrs)
{
super(ctx, attrs);
init();
if (ctx != null && attrs != null)
{
int paletteColor = getIntAttribute(ctx, attrs, "color", 0);
setColor(getAndroidTestColor(paletteColor));
setButtonCount(getIntAttribute(ctx, attrs, "button_count", 5));
}
if (isInEditMode()) initEditMode();
}
public CheckmarkButtonView indexToButton(int i)
{
int position = i;
if (getCheckmarkOrder() == RIGHT_TO_LEFT) position = nButtons - i - 1;
return (CheckmarkButtonView) getChildAt(position);
}
@Override
public void onCheckmarkOrderChanged()
{
setupButtons();
}
public void setButtonCount(int newButtonCount)
{
if (nButtons != newButtonCount)
{
nButtons = newButtonCount;
addButtons();
}
setupButtons();
}
public void setColor(int color)
{
this.color = color;
setupButtons();
}
public void setController(Controller controller)
{
this.controller = controller;
setupButtons();
}
public void setDataOffset(int dataOffset)
{
this.dataOffset = dataOffset;
setupButtons();
}
public void setHabit(@NonNull Habit habit)
{
this.habit = habit;
setupButtons();
}
public void setValues(int[] values)
{
this.values = values;
setupButtons();
}
@Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
if (prefs != null) prefs.addListener(this);
}
@Override
protected void onDetachedFromWindow()
{
if (prefs != null) prefs.removeListener(this);
super.onDetachedFromWindow();
}
@Override
protected void onMeasure(int widthSpec, int heightSpec)
{
float buttonWidth = getDimension(getContext(), R.dimen.checkmarkWidth);
float buttonHeight = getDimension(getContext(), R.dimen.checkmarkHeight);
float width = buttonWidth * nButtons;
widthSpec = makeMeasureSpec((int) width, EXACTLY);
heightSpec = makeMeasureSpec((int) buttonHeight, EXACTLY);
super.onMeasure(widthSpec, heightSpec);
}
private void addButtons()
{
removeAllViews();
for (int i = 0; i < nButtons; i++)
addView(new CheckmarkButtonView(getContext()));
}
private int getCheckmarkOrder()
{
if (prefs == null) return LEFT_TO_RIGHT;
return prefs.shouldReverseCheckmarks() ? RIGHT_TO_LEFT : LEFT_TO_RIGHT;
}
private void init()
{
Context appContext = getContext().getApplicationContext();
if (appContext instanceof HabitsApplication)
{
HabitsApplication app = (HabitsApplication) appContext;
prefs = app.getComponent().getPreferences();
}
setWillNotDraw(false);
values = new int[0];
}
private void initEditMode()
{
int values[] = new int[nButtons];
for (int i = 0; i < nButtons; i++)
values[i] = Math.min(2, new Random().nextInt(4));
setValues(values);
}
private void setupButtonControllers(long timestamp,
CheckmarkButtonView buttonView)
{
if (controller == null) return;
if (!(getContext() instanceof ListHabitsActivity)) return;
ListHabitsActivity activity = (ListHabitsActivity) getContext();
CheckmarkButtonControllerFactory buttonControllerFactory = activity
.getListHabitsComponent()
.getCheckmarkButtonControllerFactory();
CheckmarkButtonController buttonController =
buttonControllerFactory.create(habit, timestamp);
buttonController.setListener(controller);
buttonController.setView(buttonView);
buttonView.setController(buttonController);
}
private void setupButtons()
{
long timestamp = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
timestamp -= day * dataOffset;
for (int i = 0; i < nButtons; i++)
{
CheckmarkButtonView buttonView = indexToButton(i);
if (i + dataOffset >= values.length) break;
buttonView.setValue(values[i + dataOffset]);
buttonView.setColor(color);
setupButtonControllers(timestamp, buttonView);
timestamp -= day;
}
}
public interface Controller extends CheckmarkButtonController.Listener
{
}
}

View File

@@ -0,0 +1,268 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.content.*;
import android.os.*;
import android.support.annotation.*;
import android.support.v7.widget.*;
import android.support.v7.widget.helper.*;
import android.util.*;
import android.view.*;
import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.activities.habits.list.model.*;
import org.isoron.uhabits.models.*;
import java.util.*;
public class HabitCardListView extends RecyclerView
{
@Nullable
private HabitCardListAdapter adapter;
@Nullable
private Controller controller;
private final ItemTouchHelper touchHelper;
private int checkmarkCount;
private int dataOffset;
private LinkedList<HabitCardViewHolder> attachedHolders;
public HabitCardListView(Context context, AttributeSet attrs)
{
super(context, attrs);
setLongClickable(true);
setHasFixedSize(true);
setLayoutManager(new LinearLayoutManager(getContext()));
TouchHelperCallback callback = new TouchHelperCallback();
touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(this);
attachedHolders = new LinkedList<>();
}
public void attachCardView(HabitCardViewHolder holder)
{
attachedHolders.add(holder);
}
/**
* Builds a new HabitCardView to be eventually added to this list,
* containing the given data.
*
* @param holder the ViewHolder containing the HabitCardView that should
* be built
* @param habit the habit for this card
* @param score the current score for the habit
* @param checkmarks the list of checkmark values to be included in the
* card
* @param selected true if the card is selected, false otherwise
* @return the HabitCardView generated
*/
public View bindCardView(@NonNull HabitCardViewHolder holder,
@NonNull Habit habit,
double score,
int[] checkmarks,
boolean selected)
{
HabitCardView cardView = (HabitCardView) holder.itemView;
cardView.setHabit(habit);
cardView.setSelected(selected);
cardView.setValues(checkmarks);
cardView.setButtonCount(checkmarkCount);
cardView.setDataOffset(dataOffset);
cardView.setScore(score);
cardView.setUnit(habit.getUnit());
cardView.setThreshold(habit.getTargetValue());
if (controller != null) setupCardViewController(holder);
return cardView;
}
public View createCardView()
{
return new HabitCardView(getContext());
}
public void detachCardView(HabitCardViewHolder holder)
{
attachedHolders.remove(holder);
}
@Override
public void setAdapter(RecyclerView.Adapter adapter)
{
this.adapter = (HabitCardListAdapter) adapter;
super.setAdapter(adapter);
}
public void setCheckmarkCount(int checkmarkCount)
{
this.checkmarkCount = checkmarkCount;
}
public void setController(@Nullable Controller controller)
{
this.controller = controller;
}
public void setDataOffset(int dataOffset)
{
this.dataOffset = dataOffset;
for (HabitCardViewHolder holder : attachedHolders)
{
HabitCardView cardView = (HabitCardView) holder.itemView;
cardView.setDataOffset(dataOffset);
}
}
@Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
if (adapter != null) adapter.onAttached();
}
@Override
protected void onDetachedFromWindow()
{
if (adapter != null) adapter.onDetached();
super.onDetachedFromWindow();
}
@Override
protected void onRestoreInstanceState(Parcelable state)
{
BundleSavedState bss = (BundleSavedState) state;
dataOffset = bss.bundle.getInt("dataOffset");
super.onRestoreInstanceState(bss.getSuperState());
}
@Override
protected Parcelable onSaveInstanceState()
{
Parcelable superState = super.onSaveInstanceState();
Bundle bundle = new Bundle();
bundle.putInt("dataOffset", dataOffset);
return new BundleSavedState(superState, bundle);
}
protected void setupCardViewController(@NonNull HabitCardViewHolder holder)
{
HabitCardView cardView = (HabitCardView) holder.itemView;
HabitCardController cardController = new HabitCardController();
cardController.setListener(controller);
cardView.setController(cardController);
cardController.setView(cardView);
GestureDetector detector = new GestureDetector(getContext(),
new CardViewGestureDetector(holder));
cardView.setOnTouchListener((v, ev) -> {
detector.onTouchEvent(ev);
return true;
});
}
public interface Controller
extends CheckmarkButtonController.Listener, HabitCardController.Listener
{
void drop(int from, int to);
void onItemClick(int pos);
void onItemLongClick(int pos);
void startDrag(int position);
}
private class CardViewGestureDetector
extends GestureDetector.SimpleOnGestureListener
{
@NonNull
private final HabitCardViewHolder holder;
public CardViewGestureDetector(@NonNull HabitCardViewHolder holder)
{
this.holder = holder;
}
@Override
public void onLongPress(MotionEvent e)
{
int position = holder.getAdapterPosition();
if (controller != null) controller.onItemLongClick(position);
if (adapter.isSortable()) touchHelper.startDrag(holder);
}
@Override
public boolean onSingleTapUp(MotionEvent e)
{
int position = holder.getAdapterPosition();
if (controller != null) controller.onItemClick(position);
return true;
}
}
class TouchHelperCallback extends ItemTouchHelper.Callback
{
@Override
public int getMovementFlags(RecyclerView recyclerView,
ViewHolder viewHolder)
{
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean isItemViewSwipeEnabled()
{
return false;
}
@Override
public boolean isLongPressDragEnabled()
{
return false;
}
@Override
public boolean onMove(RecyclerView recyclerView,
ViewHolder from,
ViewHolder to)
{
if (controller == null) return false;
controller.drop(from.getAdapterPosition(), to.getAdapterPosition());
return true;
}
@Override
public void onSwiped(ViewHolder viewHolder, int direction)
{
// NOP
}
}
}

View File

@@ -0,0 +1,352 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.annotation.*;
import android.content.*;
import android.graphics.drawable.*;
import android.os.*;
import android.support.annotation.*;
import android.text.*;
import android.util.*;
import android.view.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import java.util.*;
import static android.os.Build.VERSION.*;
import static android.os.Build.VERSION_CODES.*;
import static android.view.ViewGroup.LayoutParams.*;
import static org.isoron.uhabits.utils.InterfaceUtils.*;
public class HabitCardView extends FrameLayout
implements ModelObservable.Listener
{
private static final String EDIT_MODE_HABITS[] = {
"Wake up early",
"Wash dishes",
"Exercise",
"Meditate",
"Play guitar",
"Wash clothes",
"Get a haircut"
};
CheckmarkPanelView checkmarkPanel;
NumberPanelView numberPanel;
LinearLayout innerFrame;
TextView label;
RingView scoreRing;
private final Context context = getContext();
private StyledResources res;
@Nullable
private Habit habit;
private int dataOffset;
public HabitCardView(Context context)
{
super(context);
init();
}
public HabitCardView(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}
@Override
public void onModelChange()
{
new Handler(Looper.getMainLooper()).post(() -> refresh());
}
public void setButtonCount(int buttonCount)
{
checkmarkPanel.setButtonCount(buttonCount);
numberPanel.setButtonCount(buttonCount);
}
public void setController(Controller controller)
{
checkmarkPanel.setController(null);
numberPanel.setController(null);
if (controller == null) return;
checkmarkPanel.setController(controller);
numberPanel.setController(controller);
}
public void setDataOffset(int dataOffset)
{
this.dataOffset = dataOffset;
checkmarkPanel.setDataOffset(dataOffset);
numberPanel.setDataOffset(dataOffset);
}
public void setHabit(@NonNull Habit habit)
{
if (isAttachedToWindow()) stopListening();
this.habit = habit;
checkmarkPanel.setHabit(habit);
numberPanel.setHabit(habit);
refresh();
if (isAttachedToWindow()) startListening();
postInvalidate();
}
public void setScore(double score)
{
float percentage = (float) score;
scoreRing.setPercentage(percentage);
scoreRing.setPrecision(1.0f / 16);
postInvalidate();
}
@Override
public void setSelected(boolean isSelected)
{
super.setSelected(isSelected);
updateBackground(isSelected);
}
public void setThreshold(double threshold)
{
numberPanel.setThreshold(threshold);
}
public void setUnit(String unit)
{
numberPanel.setUnit(unit);
}
public void setValues(int values[])
{
double dvalues[] = new double[values.length];
for (int i = 0; i < values.length; i++)
dvalues[i] = (double) values[i] / 1000;
checkmarkPanel.setValues(values);
numberPanel.setValues(dvalues);
numberPanel.setThreshold(10);
postInvalidate();
}
public void triggerRipple(long timestamp)
{
long today = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
int offset = (int) ((today - timestamp) / day) - dataOffset;
CheckmarkButtonView button = checkmarkPanel.indexToButton(offset);
float y = button.getHeight() / 2.0f;
float x = checkmarkPanel.getX() + button.getX() + button.getWidth() / 2;
triggerRipple(x, y);
}
@Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
stopListening();
}
@Override
protected void onDetachedFromWindow()
{
startListening();
super.onDetachedFromWindow();
}
private int getActiveColor(Habit habit)
{
int mediumContrastColor = res.getColor(R.attr.mediumContrastTextColor);
int activeColor = ColorUtils.getColor(context, habit.getColor());
if (habit.isArchived()) activeColor = mediumContrastColor;
return activeColor;
}
private void init()
{
res = new StyledResources(getContext());
setLayoutParams(new LayoutParams(MATCH_PARENT, WRAP_CONTENT));
setClipToPadding(false);
int margin = (int) dpToPixels(context, 3);
setPadding(margin, 0, margin, margin);
initInnerFrame();
initScoreRing();
initLabel();
checkmarkPanel = new CheckmarkPanelView(context);
numberPanel = new NumberPanelView(context);
numberPanel.setVisibility(GONE);
innerFrame.addView(scoreRing);
innerFrame.addView(label);
innerFrame.addView(checkmarkPanel);
innerFrame.addView(numberPanel);
addView(innerFrame);
innerFrame.setOnTouchListener((v, event) ->
{
if (SDK_INT >= LOLLIPOP)
v.getBackground().setHotspot(event.getX(), event.getY());
return false;
});
if (isInEditMode()) initEditMode();
}
@SuppressLint("SetTextI18n")
private void initEditMode()
{
Random rand = new Random();
int color = ColorUtils.getAndroidTestColor(rand.nextInt(10));
label.setText(EDIT_MODE_HABITS[rand.nextInt(EDIT_MODE_HABITS.length)]);
label.setTextColor(color);
scoreRing.setColor(color);
scoreRing.setPercentage(rand.nextFloat());
checkmarkPanel.setColor(color);
numberPanel.setColor(color);
checkmarkPanel.setButtonCount(5);
}
private void initInnerFrame()
{
LinearLayout.LayoutParams params;
params = new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
innerFrame = new LinearLayout(context);
innerFrame.setLayoutParams(params);
innerFrame.setOrientation(LinearLayout.HORIZONTAL);
innerFrame.setGravity(Gravity.CENTER_VERTICAL);
if (SDK_INT >= LOLLIPOP)
innerFrame.setElevation(dpToPixels(context, 1));
}
private void initLabel()
{
LinearLayout.LayoutParams params;
params = new LinearLayout.LayoutParams(0, WRAP_CONTENT, 1);
label = new TextView(context);
label.setLayoutParams(params);
label.setMaxLines(2);
label.setEllipsize(TextUtils.TruncateAt.END);
if (SDK_INT >= M)
label.setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED);
}
private void initScoreRing()
{
scoreRing = new RingView(context);
int ringSize = (int) dpToPixels(context, 15);
int margin = (int) dpToPixels(context, 8);
float thickness = dpToPixels(context, 3);
LinearLayout.LayoutParams params;
params = new LinearLayout.LayoutParams(ringSize, ringSize);
params.setMargins(margin, 0, margin, 0);
params.gravity = Gravity.CENTER;
scoreRing.setLayoutParams(params);
scoreRing.setThickness(thickness);
}
private void refresh()
{
int color = getActiveColor(habit);
label.setText(habit.getName());
label.setTextColor(color);
scoreRing.setColor(color);
checkmarkPanel.setColor(color);
numberPanel.setColor(color);
boolean isNumerical = habit.isNumerical();
checkmarkPanel.setVisibility(isNumerical ? GONE : VISIBLE);
numberPanel.setVisibility(isNumerical ? VISIBLE : GONE);
postInvalidate();
}
private void startListening()
{
if (habit != null) habit.getObservable().removeListener(this);
}
private void stopListening()
{
if (habit != null) habit.getObservable().addListener(this);
}
private void triggerRipple(final float x, final float y)
{
final Drawable background = innerFrame.getBackground();
if (SDK_INT >= LOLLIPOP) background.setHotspot(x, y);
background.setState(new int[]{
android.R.attr.state_pressed, android.R.attr.state_enabled
});
new Handler().postDelayed(() -> background.setState(new int[]{}), 25);
}
private void updateBackground(boolean isSelected)
{
if (SDK_INT >= LOLLIPOP)
{
if (isSelected)
innerFrame.setBackgroundResource(R.drawable.selected_box);
else innerFrame.setBackgroundResource(R.drawable.ripple);
}
else
{
Drawable background;
if (isSelected)
background = res.getDrawable(R.attr.selectedBackground);
else background = res.getDrawable(R.attr.cardBackground);
innerFrame.setBackgroundDrawable(background);
}
}
public interface Controller
extends CheckmarkPanelView.Controller, NumberPanelView.Controller
{
}
}

View File

@@ -0,0 +1,198 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.content.*;
import android.graphics.*;
import android.support.annotation.*;
import android.text.*;
import android.util.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.activities.habits.list.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.utils.*;
import java.util.*;
import static org.isoron.uhabits.utils.InterfaceUtils.*;
public class HeaderView extends ScrollableChart
implements Preferences.Listener, MidnightTimer.MidnightListener
{
private int buttonCount;
@Nullable
private Preferences prefs;
@Nullable
private MidnightTimer midnightTimer;
private TextPaint paint;
private RectF rect;
public HeaderView(@NonNull Context context,
@NonNull Preferences prefs,
@NonNull MidnightTimer midnightTimer)
{
super(context);
this.prefs = prefs;
this.midnightTimer = midnightTimer;
init();
}
public HeaderView(Context context, @Nullable AttributeSet attrs)
{
super(context, attrs);
Context appContext = context.getApplicationContext();
if (appContext instanceof HabitsApplication)
{
HabitsApplication app = (HabitsApplication) appContext;
prefs = app.getComponent().getPreferences();
}
if (context instanceof ListHabitsActivity)
{
ListHabitsComponent component =
((ListHabitsActivity) context).getListHabitsComponent();
midnightTimer = component.getMidnightTimer();
}
init();
}
@Override
public void atMidnight()
{
post(() -> invalidate());
}
@Override
public void onCheckmarkOrderChanged()
{
updateDirection();
postInvalidate();
}
public void setButtonCount(int buttonCount)
{
this.buttonCount = buttonCount;
postInvalidate();
}
@Override
protected void onAttachedToWindow()
{
updateDirection();
super.onAttachedToWindow();
if (prefs != null) prefs.addListener(this);
if (midnightTimer != null) midnightTimer.addListener(this);
}
@Override
protected void onDetachedFromWindow()
{
if (midnightTimer != null) midnightTimer.removeListener(this);
if (prefs != null) prefs.removeListener(this);
super.onDetachedFromWindow();
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
float width = getDimension(getContext(), R.dimen.checkmarkWidth);
float height = getDimension(getContext(), R.dimen.checkmarkHeight);
boolean reverse = shouldReverseCheckmarks();
boolean isRtl = InterfaceUtils.isLayoutRtl(this);
day.add(GregorianCalendar.DAY_OF_MONTH, -getDataOffset());
float em = paint.measureText("m");
for (int i = 0; i < buttonCount; i++)
{
rect.set(0, 0, width, height);
rect.offset(canvas.getWidth(), 0);
if (reverse) rect.offset(-(i + 1) * width, 0);
else rect.offset((i - buttonCount) * width, 0);
if (isRtl) rect.set(canvas.getWidth() - rect.right, rect.top,
canvas.getWidth() - rect.left, rect.bottom);
String text = DateUtils.formatHeaderDate(day).toUpperCase();
String[] lines = text.split("\n");
int y1 = (int) (rect.centerY() - 0.25 * em);
int y2 = (int) (rect.centerY() + 1.25 * em);
canvas.drawText(lines[0], rect.centerX(), y1, paint);
canvas.drawText(lines[1], rect.centerX(), y2, paint);
day.add(GregorianCalendar.DAY_OF_MONTH, -1);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = (int) getDimension(getContext(), R.dimen.checkmarkHeight);
setMeasuredDimension(width, height);
}
private void init()
{
setScrollerBucketSize(
(int) getDimension(getContext(), R.dimen.checkmarkWidth));
StyledResources sr = new StyledResources(getContext());
paint = new TextPaint();
paint.setColor(Color.BLACK);
paint.setAntiAlias(true);
paint.setTextSize(getDimension(getContext(), R.dimen.tinyTextSize));
paint.setTextAlign(Paint.Align.CENTER);
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setColor(sr.getColor(R.attr.mediumContrastTextColor));
rect = new RectF();
if (isInEditMode()) setButtonCount(5);
}
private boolean shouldReverseCheckmarks()
{
if (prefs == null) return false;
return prefs.shouldReverseCheckmarks();
}
private void updateDirection()
{
int direction = -1;
if (shouldReverseCheckmarks()) direction *= -1;
if (InterfaceUtils.isLayoutRtl(this)) direction *= -1;
setDirection(direction);
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.habits.list.model.HintList;
import java.util.Random;
import butterknife.BindView;
import butterknife.ButterKnife;
public class HintView extends FrameLayout
{
@BindView(R.id.hintContent)
TextView hintContent;
@Nullable
private HintList hintList;
public HintView(Context context)
{
super(context);
init();
}
public HintView(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}
@Override
public void onAttachedToWindow()
{
super.onAttachedToWindow();
showNext();
}
/**
* Sets the list of hints to be shown
*
* @param hintList the list of hints to be shown
*/
public void setHints(@Nullable HintList hintList)
{
this.hintList = hintList;
}
private void dismiss()
{
animate().alpha(0f).setDuration(500).setListener(new DismissAnimator());
}
private void init()
{
addView(inflate(getContext(), R.layout.list_habits_hint, null));
ButterKnife.bind(this);
setVisibility(GONE);
setClickable(true);
setOnClickListener(v -> dismiss());
if (isInEditMode()) initEditMode();
}
@SuppressLint("SetTextI18n")
private void initEditMode()
{
String hints[] = {
"Cats are the most popular pet in the United States: There " +
"are 88 million pet cats and 74 million dogs.",
"A cat has been mayor of Talkeetna, Alaska, for 15 years. " +
"His name is Stubbs.",
"Cats cant taste sweetness."
};
int k = new Random().nextInt(hints.length);
hintContent.setText(hints[k]);
setVisibility(VISIBLE);
setAlpha(1.0f);
}
protected void showNext()
{
if (hintList == null) return;
if (!hintList.shouldShow()) return;
String hint = hintList.pop();
if (hint == null) return;
hintContent.setText(hint);
requestLayout();
setAlpha(0.0f);
setVisibility(View.VISIBLE);
animate().alpha(1f).setDuration(500);
}
private class DismissAnimator extends AnimatorListenerAdapter
{
@Override
public void onAnimationEnd(android.animation.Animator animation)
{
setVisibility(View.GONE);
}
}
}

View File

@@ -0,0 +1,192 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.content.*;
import android.graphics.*;
import android.support.annotation.*;
import android.text.*;
import android.util.*;
import android.view.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.utils.*;
import java.text.*;
import static org.isoron.uhabits.utils.AttributeSetUtils.*;
import static org.isoron.uhabits.utils.ColorUtils.*;
import static org.isoron.uhabits.utils.InterfaceUtils.*;
public class NumberButtonView extends View
{
private static Typeface BOLD_TYPEFACE =
Typeface.create("sans-serif-condensed", Typeface.BOLD);
private static Typeface NORMAL_TYPEFACE =
Typeface.create("sans-serif-condensed", Typeface.NORMAL);
private int color;
private double value;
private double threshold;
private String unit;
private RectF rect;
private TextPaint pRegular;
private TextPaint pBold;
private int lightGrey;
private float em;
private int darkGrey;
public NumberButtonView(@Nullable Context context)
{
super(context);
init();
}
public NumberButtonView(@Nullable Context ctx, @Nullable AttributeSet attrs)
{
super(ctx, attrs);
init();
if (ctx != null && attrs != null)
{
int color = getIntAttribute(ctx, attrs, "color", 0);
int value = getIntAttribute(ctx, attrs, "value", 0);
int threshold = getIntAttribute(ctx, attrs, "threshold", 1);
String unit = getAttribute(ctx, attrs, "unit", "min");
setColor(getAndroidTestColor(color));
setThreshold(threshold);
setValue(value);
setUnit(unit);
}
}
/**
* @param v
* @return
*/
public static String formatValue(double v)
{
if (v >= 1e9) return String.format("%.1fG", v / 1e9);
if (v >= 1e8) return String.format("%.0fM", v / 1e6);
if (v >= 1e7) return String.format("%.1fM", v / 1e6);
if (v >= 1e6) return String.format("%.1fM", v / 1e6);
if (v >= 1e5) return String.format("%.0fk", v / 1e3);
if (v >= 1e4) return String.format("%.1fk", v / 1e3);
if (v >= 1e3) return String.format("%.1fk", v / 1e3);
if (v >= 1e2) return new DecimalFormat("#").format(v);
if (v >= 1e1) return new DecimalFormat("#.#").format(v);
return new DecimalFormat("#.##").format(v);
}
public void setColor(int color)
{
this.color = color;
postInvalidate();
}
public void setController(final NumberButtonController controller)
{
setOnClickListener(v -> controller.onClick());
setOnLongClickListener(v ->
{
controller.onLongClick();
return true;
});
}
public void setThreshold(double threshold)
{
this.threshold = threshold;
postInvalidate();
}
public void setUnit(String unit)
{
this.unit = unit;
postInvalidate();
}
public void setValue(double value)
{
this.value = value;
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas)
{
int activeColor = lightGrey;
if (value > 0 && value < threshold) activeColor = darkGrey;
if (value >= threshold) activeColor = color;
pRegular.setColor(activeColor);
pBold.setColor(activeColor);
String fv = formatValue(value);
rect.set(0, 0, getWidth(), getHeight());
canvas.drawText(fv, rect.centerX(), rect.centerY(), pBold);
rect.offset(0, 1.2f * em);
canvas.drawText(unit, rect.centerX(), rect.centerY(), pRegular);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int width = (int) getDimension(getContext(), R.dimen.checkmarkWidth);
int height = (int) getDimension(getContext(), R.dimen.checkmarkHeight);
setMeasuredDimension(width, height);
}
private void init()
{
StyledResources sr = new StyledResources(getContext());
rect = new RectF();
pRegular = new TextPaint();
pRegular.setTextSize(
getDimension(getContext(), R.dimen.smallerTextSize));
pRegular.setTypeface(NORMAL_TYPEFACE);
pRegular.setAntiAlias(true);
pRegular.setTextAlign(Paint.Align.CENTER);
pBold = new TextPaint();
pBold.setTextSize(getDimension(getContext(), R.dimen.smallTextSize));
pBold.setTypeface(BOLD_TYPEFACE);
pBold.setAntiAlias(true);
pBold.setTextAlign(Paint.Align.CENTER);
em = pBold.measureText("m");
lightGrey = sr.getColor(R.attr.lowContrastTextColor);
darkGrey = sr.getColor(R.attr.mediumContrastTextColor);
}
}

View File

@@ -0,0 +1,262 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.content.*;
import android.support.annotation.*;
import android.util.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.habits.list.*;
import org.isoron.uhabits.activities.habits.list.controllers.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.preferences.*;
import org.isoron.uhabits.utils.*;
import java.util.*;
import static android.view.View.MeasureSpec.*;
import static org.isoron.uhabits.utils.AttributeSetUtils.*;
import static org.isoron.uhabits.utils.ColorUtils.*;
import static org.isoron.uhabits.utils.InterfaceUtils.*;
public class NumberPanelView extends LinearLayout
implements Preferences.Listener
{
private static final int LEFT_TO_RIGHT = 0;
private static final int RIGHT_TO_LEFT = 1;
@Nullable
private Preferences prefs;
private double values[];
private double threshold;
private int nButtons;
private int color;
private Controller controller;
private String unit;
@NonNull
private Habit habit;
private int dataOffset;
public NumberPanelView(Context context)
{
super(context);
init();
}
public NumberPanelView(Context ctx, AttributeSet attrs)
{
super(ctx, attrs);
init();
if (ctx != null && attrs != null)
{
int paletteColor = getIntAttribute(ctx, attrs, "color", 0);
setColor(getAndroidTestColor(paletteColor));
setButtonCount(getIntAttribute(ctx, attrs, "button_count", 5));
setThreshold(getIntAttribute(ctx, attrs, "threshold", 1));
setUnit(getAttribute(ctx, attrs, "unit", "min"));
}
if(isInEditMode()) initEditMode();
}
public void setUnit(String unit)
{
this.unit = unit;
setupButtons();
}
public void initEditMode()
{
double values[] = new double[nButtons];
for(int i = 0; i < nButtons; i++)
values[i] = new Random().nextDouble() * (threshold * 3);
setValues(values);
}
public NumberButtonView indexToButton(int i)
{
int position = i;
if (getCheckmarkOrder() == RIGHT_TO_LEFT) position = nButtons - i - 1;
return (NumberButtonView) getChildAt(position);
}
@Override
public void onCheckmarkOrderChanged()
{
setupButtons();
}
public void setButtonCount(int newButtonCount)
{
if (nButtons != newButtonCount)
{
nButtons = newButtonCount;
addButtons();
}
setupButtons();
}
public void setColor(int color)
{
this.color = color;
setupButtons();
}
public void setController(Controller controller)
{
this.controller = controller;
setupButtons();
}
public void setDataOffset(int dataOffset)
{
this.dataOffset = dataOffset;
setupButtons();
}
public void setHabit(@NonNull Habit habit)
{
this.habit = habit;
setupButtons();
}
public void setThreshold(double threshold)
{
this.threshold = threshold;
setupButtons();
}
public void setValues(double[] values)
{
this.values = values;
setupButtons();
}
@Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
if (prefs != null) prefs.addListener(this);
}
@Override
protected void onDetachedFromWindow()
{
if (prefs != null) prefs.removeListener(this);
super.onDetachedFromWindow();
}
@Override
protected void onMeasure(int widthSpec, int heightSpec)
{
Context context = getContext();
float buttonWidth = getDimension(context, R.dimen.checkmarkWidth);
float buttonHeight = getDimension(context, R.dimen.checkmarkHeight);
float width = buttonWidth * nButtons;
widthSpec = makeMeasureSpec((int) width, EXACTLY);
heightSpec = makeMeasureSpec((int) buttonHeight, EXACTLY);
super.onMeasure(widthSpec, heightSpec);
}
private void addButtons()
{
removeAllViews();
for (int i = 0; i < nButtons; i++)
addView(new NumberButtonView(getContext()));
}
private int getCheckmarkOrder()
{
if (prefs == null) return LEFT_TO_RIGHT;
return prefs.shouldReverseCheckmarks() ? RIGHT_TO_LEFT : LEFT_TO_RIGHT;
}
private void init()
{
Context appContext = getContext().getApplicationContext();
if (appContext instanceof HabitsApplication)
{
HabitsApplication app = (HabitsApplication) appContext;
prefs = app.getComponent().getPreferences();
}
setWillNotDraw(false);
values = new double[0];
}
private void setupButtonControllers(long timestamp,
NumberButtonView buttonView)
{
if (controller == null) return;
if (!(getContext() instanceof ListHabitsActivity)) return;
ListHabitsActivity activity = (ListHabitsActivity) getContext();
NumberButtonControllerFactory buttonControllerFactory = activity
.getListHabitsComponent()
.getNumberButtonControllerFactory();
NumberButtonController buttonController =
buttonControllerFactory.create(habit, timestamp);
buttonController.setListener(controller);
buttonController.setView(buttonView);
buttonView.setController(buttonController);
}
private void setupButtons()
{
long timestamp = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
timestamp -= day * dataOffset;
for (int i = 0; i < nButtons; i++)
{
NumberButtonView buttonView = indexToButton(i);
if (i + dataOffset >= values.length) break;
buttonView.setValue(values[i + dataOffset]);
buttonView.setColor(color);
buttonView.setThreshold(threshold);
buttonView.setUnit(unit);
setupButtonControllers(timestamp, buttonView);
timestamp -= day;
}
}
public interface Controller extends NumberButtonController.Listener
{
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.show;
import android.content.*;
import android.net.*;
import android.os.*;
import android.support.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.models.*;
/**
* Activity that allows the user to see more information about a single habit.
* <p>
* Shows all the metadata for the habit, in addition to several charts.
*/
public class ShowHabitActivity extends BaseActivity
{
private HabitList habits;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
HabitsApplication app = (HabitsApplication) getApplicationContext();
habits = app.getComponent().getHabitList();
Habit habit = getHabitFromIntent();
ShowHabitComponent component = DaggerShowHabitComponent
.builder()
.appComponent(app.getComponent())
.showHabitModule(new ShowHabitModule(this, habit))
.build();
ShowHabitRootView rootView = component.getRootView();
ShowHabitScreen screen = component.getScreen();
setScreen(screen);
screen.setMenu(component.getMenu());
screen.setController(component.getController());
rootView.setController(component.getController());
screen.reattachDialogs();
}
@NonNull
private Habit getHabitFromIntent()
{
Uri data = getIntent().getData();
Habit habit = habits.getById(ContentUris.parseId(data));
if (habit == null) throw new RuntimeException("habit not found");
return habit;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.show;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.*;
import dagger.*;
@ActivityScope
@Component(modules = { ShowHabitModule.class },
dependencies = { AppComponent.class })
public interface ShowHabitComponent
{
ShowHabitController getController();
ShowHabitsMenu getMenu();
ShowHabitRootView getRootView();
ShowHabitScreen getScreen();
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.show;
import android.support.annotation.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.common.dialogs.*;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.models.*;
import javax.inject.*;
@ActivityScope
public class ShowHabitController
implements ShowHabitRootView.Controller, HistoryEditorDialog.Controller
{
@NonNull
private final ShowHabitScreen screen;
@NonNull
private final Habit habit;
@NonNull
private final CommandRunner commandRunner;
@Inject
public ShowHabitController(@NonNull ShowHabitScreen screen,
@NonNull CommandRunner commandRunner,
@NonNull Habit habit)
{
this.screen = screen;
this.habit = habit;
this.commandRunner = commandRunner;
}
@Override
public void onEditHistoryButtonClick()
{
screen.showEditHistoryDialog();
}
@Override
public void onToggleCheckmark(long timestamp)
{
commandRunner.execute(new ToggleRepetitionCommand(habit, timestamp),
null);
}
@Override
public void onToolbarChanged()
{
screen.invalidateToolbar();
}
}

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