diff options
author | James Kung <kingkung@google.com> | 2013-03-08 10:58:25 -0800 |
---|---|---|
committer | James Kung <kingkung@google.com> | 2013-04-01 19:46:50 -0700 |
commit | 3e9818e0267619fecebd55095ab26c53eff92e93 (patch) | |
tree | 5d56d7a6f784078412ed876ed63ffd80b2af7fcb /src/com | |
parent | b8f95646fc0510eebfeaa27864023d630f34090f (diff) | |
download | datetimepicker-3e9818e0267619fecebd55095ab26c53eff92e93.tar.gz |
DatePicker initial commit
Change-Id: I5e2dda38b06b76454b974e7240befd6a9316364f
Diffstat (limited to 'src/com')
9 files changed, 1746 insertions, 1 deletions
diff --git a/src/com/android/datetimepicker/Utils.java b/src/com/android/datetimepicker/Utils.java index 8772fb8..9bc5952 100644 --- a/src/com/android/datetimepicker/Utils.java +++ b/src/com/android/datetimepicker/Utils.java @@ -17,9 +17,79 @@ package com.android.datetimepicker; import android.os.Build; +import android.text.format.Time; +import java.util.Calendar; + +/** + * Utility helper functions for time and date pickers. + */ public class Utils { + + public static final int MONDAY_BEFORE_JULIAN_EPOCH = Time.EPOCH_JULIAN_DAY - 3; + + static final String SHARED_PREFS_NAME = "com.android.calendar_preferences"; + public static boolean isJellybeanOrLater() { - return Build.VERSION.SDK_INT >= 16; + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + } + + 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; } } diff --git a/src/com/android/datetimepicker/date/DatePickerController.java b/src/com/android/datetimepicker/date/DatePickerController.java new file mode 100644 index 0000000..f92d687 --- /dev/null +++ b/src/com/android/datetimepicker/date/DatePickerController.java @@ -0,0 +1,39 @@ +/* + * 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.SimpleMonthAdapter.CalendarDay; + +/** + * Controller class to communicate among the various components of the date picker dialog. + */ +public interface DatePickerController { + + public void onYearPickerSelectionChanged(int year); + + public void onMonthPickerSelectionChanged(int month); + + public void onDayPickerSelectionChanged(int year, int month, int day); + + public CalendarDay getSelectedDay(); + + public int getFirstDayOfWeek(); + + public int getMinYear(); + + public int getMaxYear(); +} diff --git a/src/com/android/datetimepicker/date/DatePickerDialog.java b/src/com/android/datetimepicker/date/DatePickerDialog.java new file mode 100644 index 0000000..7c46f69 --- /dev/null +++ b/src/com/android/datetimepicker/date/DatePickerDialog.java @@ -0,0 +1,371 @@ +/* + * 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.app.Activity; +import android.app.DialogFragment; +import android.os.Bundle; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v4.view.ViewPager.OnPageChangeListener; +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.widget.Button; +import android.widget.TextView; + +import com.android.datetimepicker.R; +import com.android.datetimepicker.Utils; +import com.android.datetimepicker.date.SimpleMonthAdapter.CalendarDay; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; + +/** + * Dialog allowing users to select a date. + */ +public class DatePickerDialog extends DialogFragment implements + OnPageChangeListener, OnClickListener, DatePickerController { + + private static final String TAG = "DatePickerDialog"; + + private static final int TOTAL_VIEWS = 3; + + private static final int MONTH_VIEW = 0; + private static final int DAY_VIEW = 1; + private static final int YEAR_VIEW = 2; + + private static String KEY_SELECTED_YEAR = "year"; + private static String KEY_SELECTED_MONTH = "month"; + private static String KEY_SELECTED_DAY = "day"; + private static String KEY_LIST_POSITION = "position"; + private static String KEY_WEEK_START = "week_start"; + private static String KEY_YEAR_START = "year_start"; + private static String KEY_YEAR_END = "year_end"; + + private static final int DEFAULT_START_YEAR = 1900; + private static final int DEFAULT_END_YEAR = 2100; + + 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 ViewPager mPager; + + private TextView mDayOfWeekView; + private TextView mMonthView; + private TextView mDayOfMonthView; + private TextView mYearView; + private MonthPickerView mMonthPickerView; + private DayPickerView mDayPickerView; + private YearPickerView mYearPickerView; + private Button mDoneButton; + + private int mWeekStart = mCalendar.getFirstDayOfWeek(); + private int mMinYear = DEFAULT_START_YEAR; + private int mMaxYear = DEFAULT_END_YEAR; + + private final View[] mViews = new View[TOTAL_VIEWS]; + + /** + * 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); + } + + 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); + 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_LIST_POSITION, mDayPickerView.getFirstVisiblePosition()); + outState.putInt(KEY_WEEK_START, mWeekStart); + outState.putInt(KEY_YEAR_START, mMinYear); + outState.putInt(KEY_YEAR_END, mMaxYear); + } + + @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); + mMonthView = (TextView) view.findViewById(R.id.date_picker_month); + mMonthView.setOnClickListener(this); + mDayOfMonthView = (TextView) view.findViewById(R.id.date_picker_day); + mDayOfMonthView.setOnClickListener(this); + mYearView = (TextView) view.findViewById(R.id.date_picker_year); + mYearView.setOnClickListener(this); + final Activity activity = getActivity(); + + mMonthPickerView = new MonthPickerView(activity, this); + mDayPickerView = new DayPickerView(activity, this); + if (savedInstanceState != null) { + Log.d(TAG, + "Setting first visible position: " + + savedInstanceState.getInt(KEY_LIST_POSITION)); + mDayPickerView.setSelectionFromTop(savedInstanceState.getInt(KEY_LIST_POSITION), + DayPickerView.LIST_TOP_OFFSET); + mWeekStart = savedInstanceState.getInt(KEY_WEEK_START); + mMinYear = savedInstanceState.getInt(KEY_YEAR_START); + mMaxYear = savedInstanceState.getInt(KEY_YEAR_END); + } + mYearPickerView = new YearPickerView(activity, this); + + mViews[MONTH_VIEW] = mMonthPickerView; + mViews[DAY_VIEW] = mDayPickerView; + mViews[YEAR_VIEW] = mYearPickerView; + + mPager = (ViewPager) view.findViewById(R.id.pager); + mPager.setAdapter(new DatePickerPagerAdapter()); + mPager.setOnPageChangeListener(this); + mPager.setCurrentItem(DAY_VIEW); + + mDoneButton = (Button) view.findViewById(R.id.done); + mDoneButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (mCallBack != null) { + mCallBack.onDateSet(DatePickerDialog.this, mCalendar.get(Calendar.YEAR), + mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH)); + } + dismiss(); + } + }); + + updateDisplay(); + + return view; + } + + private void updateDisplay() { + mDayOfWeekView.setText(mCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, + Locale.getDefault()).toUpperCase(Locale.getDefault())); + mMonthView.setText(mCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, + Locale.getDefault()).toUpperCase(Locale.getDefault())); + mDayOfMonthView.setText(DAY_FORMAT.format(mCalendar.getTime())); + mYearView.setText(YEAR_FORMAT.format(mCalendar.getTime())); + } + + 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(); + mYearPickerView.onChange(); + } + } + + public void setOnDateSetListener(OnDateSetListener listener) { + mCallBack = listener; + } + + private class DatePickerPagerAdapter extends PagerAdapter { + + @Override + public Object instantiateItem(ViewGroup container, int position) { + container.addView(mViews[position]); + return mViews[position]; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object view) { + container.removeView((View) view); + } + + @Override + public int getCount() { + return mViews.length; + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + } + + // 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 onPageScrollStateChanged(int arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void onPageScrolled(int arg0, float arg1, int arg2) { + // TODO Auto-generated method stub + } + + @Override + public void onPageSelected(int index) { + switch (index) { + case MONTH_VIEW: + mMonthView.setSelected(true); + mDayOfMonthView.setSelected(false); + mYearView.setSelected(false); + break; + case DAY_VIEW: + mMonthView.setSelected(false); + mDayOfMonthView.setSelected(true); + mYearView.setSelected(false); + break; + case YEAR_VIEW: + mMonthView.setSelected(false); + mDayOfMonthView.setSelected(false); + mYearView.setSelected(true); + break; + } + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.date_picker_year) { + mPager.setCurrentItem(YEAR_VIEW); + } else if (v.getId() == R.id.date_picker_month) { + mPager.setCurrentItem(MONTH_VIEW); + } else if (v.getId() == R.id.date_picker_day) { + mPager.setCurrentItem(DAY_VIEW); + } + } + + @Override + public void onYearPickerSelectionChanged(int year) { + adjustDayInMonthIfNeeded(mCalendar.get(Calendar.MONTH), year); + mCalendar.set(Calendar.YEAR, year); + mDayPickerView.setCalendarDate(getSelectedDay()); + updateDisplay(); + } + + @Override + public void onMonthPickerSelectionChanged(int month) { + adjustDayInMonthIfNeeded(month, mCalendar.get(Calendar.YEAR)); + mCalendar.set(Calendar.MONTH, month); + mDayPickerView.setCalendarDate(getSelectedDay()); + mPager.setCurrentItem(DAY_VIEW); + updateDisplay(); + } + + @Override + public void onDayPickerSelectionChanged(int year, int month, int day) { + mCalendar.set(Calendar.YEAR, year); + mCalendar.set(Calendar.MONTH, month); + mCalendar.set(Calendar.DAY_OF_MONTH, day); + mYearPickerView.setValue(mCalendar.get(Calendar.YEAR)); + updateDisplay(); + } + + + @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; + } +} diff --git a/src/com/android/datetimepicker/date/DayPickerView.java b/src/com/android/datetimepicker/date/DayPickerView.java new file mode 100644 index 0000000..76dcf4f --- /dev/null +++ b/src/com/android/datetimepicker/date/DayPickerView.java @@ -0,0 +1,394 @@ +/* + * 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.database.DataSetObserver; +import android.os.Handler; +import android.text.format.Time; +import android.util.Log; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.ListView; + +import com.android.datetimepicker.date.SimpleMonthAdapter.CalendarDay; + +import java.util.Calendar; +import java.util.Locale; + +/** + * This displays a list of months in a calendar format with selectable days. + */ +public class DayPickerView extends ListView implements OnScrollListener { + + 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; + + // These affect the scroll speed and feel + protected float mFriction = 1.0f; + + protected Context mContext; + protected Handler mHandler; + + protected float mMinimumFlingVelocity; + + // highlighted time + protected CalendarDay mSelectedDay = new CalendarDay(); + protected SimpleMonthAdapter mAdapter; + + protected CalendarDay mTempDay = new CalendarDay(); + + private static float mScale = 0; + // When the week starts; numbered like Time.<WEEKDAY> (e.g. SUNDAY=0). + protected int mFirstDayOfWeek; + // The first day that is visible in the view + protected Time mFirstVisibleDay = new Time(); + // 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 final DatePickerController mController; + + // This causes an update of the view at midnight + protected Runnable mTodayUpdater = new Runnable() { + @Override + public void run() { + Time midnight = new Time(mFirstVisibleDay.timezone); + midnight.setToNow(); + long currentMillis = midnight.toMillis(true); + + midnight.hour = 0; + midnight.minute = 0; + midnight.second = 0; + midnight.monthDay++; + long millisToMidnight = midnight.normalize(true) - currentMillis; + mHandler.postDelayed(this, millisToMidnight); + + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + } + }; + + // This allows us to update our position when a day is tapped + protected DataSetObserver mObserver = new DataSetObserver() { + @Override + public void onChanged() { + CalendarDay day = mAdapter.getSelectedDay(); + if (day.year != mSelectedDay.year || day.day != mSelectedDay.day) { + goTo(day, true, true, false); + } + } + }; + + public DayPickerView(Context context, DatePickerController controller) { + super(context); + mHandler = new Handler(); + mController = controller; + setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + setDrawSelectorOnTop(false); + init(context); + setCalendarDate(controller.getSelectedDay()); + } + + public void setCalendarDate(CalendarDay day) { + goTo(day, false, true, true); + } + + public void init(Context context) { + mContext = context; + String tz = Time.getCurrentTimezone(); + ViewConfiguration viewConfig = ViewConfiguration.get(context); + mMinimumFlingVelocity = viewConfig.getScaledMinimumFlingVelocity(); + + mFirstVisibleDay.timezone = tz; + mFirstVisibleDay.normalize(true); + + setUpListView(); + setUpAdapter(); + setAdapter(mAdapter); + } + + public void onChange() { + setUpAdapter(); + setAdapter(mAdapter); + } + + /** + * Creates a new adapter if necessary and sets up its parameters. Override + * this method to provide a custom adapter. + */ + protected void setUpAdapter() { + if (mAdapter == null) { + mAdapter = new SimpleMonthAdapter(getContext(), mController); + mAdapter.registerDataSetObserver(mObserver); + } else { + mAdapter.setSelectedDay(mSelectedDay); + mAdapter.notifyDataSetChanged(); + } + // refresh the view with the new parameters + mAdapter.notifyDataSetChanged(); + } + + /* + * 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); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + setUpAdapter(); + doResumeUpdates(); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mHandler.removeCallbacks(mTodayUpdater); + } + + // @Override + // public void onSaveInstanceState(Bundle outState) { + // outState.putLong(KEY_CURRENT_TIME, mSelectedDay.toMillis(true)); + // } + + /** + * Updates the user preference fields. Override this to use a different + * preference space. + */ + protected void doResumeUpdates() { + // Get default week start based on locale, subtracting one for use with + // android Time. + Calendar cal = Calendar.getInstance(Locale.getDefault()); + mFirstDayOfWeek = cal.getFirstDayOfWeek() - 1; + + mShowWeekNumber = false; + + goTo(mSelectedDay, false, false, false); + mAdapter.setSelectedDay(mSelectedDay); + mTodayUpdater.run(); + } + + /** + * 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); + // Get the week we're going to + // TODO push Util function into Calendar public api. + int position = (day.year - mController.getMinYear()) + * SimpleMonthAdapter.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 { + setSelectionFromTop(position, LIST_TOP_OFFSET); + onScrollStateChanged(this, OnScrollListener.SCROLL_STATE_IDLE); + } + } else if (setSelected) { + setMonthDisplayed(mSelectedDay); + } + return false; + } + + /** + * 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) { + SimpleMonthView child = (SimpleMonthView) 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; + } + } + } +} diff --git a/src/com/android/datetimepicker/date/MonthPickerView.java b/src/com/android/datetimepicker/date/MonthPickerView.java new file mode 100644 index 0000000..81f10ab --- /dev/null +++ b/src/com/android/datetimepicker/date/MonthPickerView.java @@ -0,0 +1,94 @@ +/* + * 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.drawable.StateListDrawable; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.GridView; +import android.widget.SimpleAdapter; + +import com.android.datetimepicker.R; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Displays a selectable list of months in a grid format. + */ +public class MonthPickerView extends GridView implements OnItemClickListener { + + private static final int NUM_COLUMNS = 3; + private static final int NUM_MONTHS = 12; + + private final Calendar mCalendar = Calendar.getInstance(); + private final DatePickerController mController; + + /** + * @param context + */ + public MonthPickerView(Context context, DatePickerController controller) { + super(context); + ViewGroup.LayoutParams frame = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT); + setLayoutParams(frame); + setNumColumns(NUM_COLUMNS); + init(context); + mController = controller; + setOnItemClickListener(this); + setSelector(new StateListDrawable()); + } + + private void init(Context context) { + ArrayList<String> months = new ArrayList<String>(); + mCalendar.set(Calendar.DAY_OF_MONTH, 1); + for (int i = 0; i < NUM_MONTHS; i++) { + mCalendar.set(Calendar.MONTH, i); + months.add(mCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, + Locale.getDefault()).toUpperCase(Locale.getDefault())); + } + setAdapter(new ArrayAdapter<String>(context, R.layout.month_text_view, months)); + } + + public class MonthPickerAdapter extends SimpleAdapter { + + /** + * @param context + * @param data + * @param resource + * @param from + * @param to + */ + public MonthPickerAdapter(Context context, + List<? extends Map<String, ?>> data, + int resource, String[] from, int[] to) { + super(context, data, resource, from, to); + } + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + mController.onMonthPickerSelectionChanged(position); + } +} diff --git a/src/com/android/datetimepicker/date/SelectableTextView.java b/src/com/android/datetimepicker/date/SelectableTextView.java new file mode 100644 index 0000000..0da34fd --- /dev/null +++ b/src/com/android/datetimepicker/date/SelectableTextView.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.datetimepicker.date; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.util.AttributeSet; +import android.widget.TextView; + +import com.android.datetimepicker.R; + +/** + * A text view which, when pressed or selected, displays a blue circle around the text. + */ +public class SelectableTextView extends TextView { + + private static final int SELECTED_CIRCLE_ALPHA = 60; + + Paint mCirclePaint = new Paint(); + + private final int mRadius; + private final int mCircleColor; + + public SelectableTextView(Context context, AttributeSet attrs) { + super(context, attrs); + Resources res = context.getResources(); + mCircleColor = res.getColor(R.color.selected_text); + mRadius = res.getDimensionPixelOffset(R.dimen.month_select_circle_radius); + 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); + } + + @Override + public void onDraw(Canvas canvas) { + if (isPressed() || isSelected()) { + canvas.drawCircle(getWidth() / 2, getHeight() / 2, mRadius, + mCirclePaint); + } + super.onDraw(canvas); + } +} diff --git a/src/com/android/datetimepicker/date/SimpleMonthAdapter.java b/src/com/android/datetimepicker/date/SimpleMonthAdapter.java new file mode 100644 index 0000000..789966c --- /dev/null +++ b/src/com/android/datetimepicker/date/SimpleMonthAdapter.java @@ -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 android.content.Context; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.view.ViewGroup; +import android.widget.AbsListView.LayoutParams; +import android.widget.BaseAdapter; + +import java.util.Calendar; +import java.util.HashMap; + +/** + * An adapter for a list of {@link SimpleMonthView} items. + */ +public class SimpleMonthAdapter extends BaseAdapter implements OnTouchListener { + + private static final String TAG = "SimpleMonthAdapter"; + + private final Context mContext; + private final DatePickerController mController; + + private GestureDetector mGestureDetector; + 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; + 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; + } + + 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 SimpleMonthAdapter(Context context, + DatePickerController controller) { + mContext = context; + mController = controller; + init(); + setSelectedDay(mController.getSelectedDay()); + } + + /** + * Updates the selected day and related parameters. + * + * @param selectedTime The time 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() { + mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener()); + mSelectedDay = new CalendarDay(System.currentTimeMillis()); + } + + @Override + public int getCount() { + return (mController.getMaxYear() - mController.getMinYear()) * MONTHS_IN_YEAR; + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + + @SuppressWarnings("unchecked") + @Override + public View getView(int position, View convertView, ViewGroup parent) { + SimpleMonthView v; + HashMap<String, Integer> drawingParams = null; + if (convertView != null) { + v = (SimpleMonthView) convertView; + // We store the drawing parameters in the view so it can be recycled + drawingParams = (HashMap<String, Integer>) v.getTag(); + } else { + v = new SimpleMonthView(mContext); + // Set up the new view + LayoutParams params = new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + v.setLayoutParams(params); + v.setClickable(true); + v.setOnTouchListener(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(); + Log.d(TAG, "Year: " + year + ", Month: " + month); + + 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(SimpleMonthView.VIEW_PARAMS_SELECTED_DAY, selectedDay); + drawingParams.put(SimpleMonthView.VIEW_PARAMS_YEAR, year); + drawingParams.put(SimpleMonthView.VIEW_PARAMS_MONTH, month); + drawingParams.put(SimpleMonthView.VIEW_PARAMS_WEEK_START, mController.getFirstDayOfWeek()); + v.setMonthParams(drawingParams); + v.invalidate(); + return v; + } + + private boolean isSelectedDayInMonth(int year, int month) { + return mSelectedDay.year == year && mSelectedDay.month == month; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (mGestureDetector.onTouchEvent(event)) { + CalendarDay day = ((SimpleMonthView) v).getDayFromLocation(event.getX(), event.getY()); + if (day != null) { + onDayTapped(day); + } + return true; + } + return false; + } + + /** + * 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.onDayPickerSelectionChanged(day.year, day.month, day.day); + setSelectedDay(day); + } + + /** + * This is here so we can identify single tap events and set the selected + * day correctly + */ + protected class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onSingleTapUp(MotionEvent e) { + return true; + } + } +} diff --git a/src/com/android/datetimepicker/date/SimpleMonthView.java b/src/com/android/datetimepicker/date/SimpleMonthView.java new file mode 100644 index 0000000..2ce6647 --- /dev/null +++ b/src/com/android/datetimepicker/date/SimpleMonthView.java @@ -0,0 +1,418 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.datetimepicker.date; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.text.format.Time; +import android.view.View; + +import com.android.datetimepicker.R; +import com.android.datetimepicker.Utils; +import com.android.datetimepicker.date.SimpleMonthAdapter.CalendarDay; + +import java.security.InvalidParameterException; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Locale; + +/** + * A calendar-like view displaying a specified month and the appropriate selectable day numbers + * within the specified month. + */ +public class SimpleMonthView extends View { + + /** + * 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; + + protected Rect r = new Rect(); + protected Paint mMonthNumPaint; + protected Paint mMonthTitlePaint; + protected Paint mSelectedCirclePaint; + protected Paint mMonthDayLabelPaint; + + // 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 int mNumRows = DEFAULT_NUM_ROWS; + + protected int mBGColor; + protected int mFocusMonthColor; + protected int mOtherMonthColor; + protected int mDaySeparatorColor; + protected int mTodayOutlineColor; + protected int mWeekNumColor; + + protected int mDayTextColor; + protected int mMonthTitleTextColor; + + public SimpleMonthView(Context context) { + super(context); + + Resources res = context.getResources(); + + mDayLabelCalendar = Calendar.getInstance(); + mCalendar = Calendar.getInstance(); + mBGColor = res.getColor(R.color.month_bgcolor); + mFocusMonthColor = res.getColor(R.color.month_mini_day_number); + mOtherMonthColor = res.getColor(R.color.month_other_month_day_number); + mDaySeparatorColor = res.getColor(R.color.month_grid_lines); + mTodayOutlineColor = res.getColor(R.color.mini_month_today_outline_color); + mDayTextColor = res.getColor(R.color.calendar_day_number); + mMonthTitleTextColor = res.getColor(R.color.selected_text); + mWeekNumColor = res.getColor(R.color.month_week_num_color); + + 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.pager_height) - MONTH_HEADER_SIZE) + / MAX_NUM_ROWS; + // Sets up any standard paints that will be used + initView(); + } + + /** + * 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.setColor(mMonthTitleTextColor); + mMonthTitlePaint.setTextAlign(Align.CENTER); + mMonthTitlePaint.setStyle(Style.FILL); + mMonthTitlePaint.setFakeBoldText(true); + + mSelectedCirclePaint = new Paint(); + mSelectedCirclePaint.setFakeBoldText(true); + mSelectedCirclePaint.setAntiAlias(true); + mSelectedCirclePaint.setColor(mMonthTitleTextColor); + 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.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} + * @param tz The time zone this view should reference times in + */ + public void setMonthParams(HashMap<String, Integer> params) { + if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) { + throw new InvalidParameterException("You must specify the 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(); + } + + 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; + } + + private void drawMonthTitle(Canvas canvas) { + int x = (mWidth + 2 * mPadding) / 2; + int y = (MONTH_HEADER_SIZE - MONTH_DAY_LABEL_TEXT_SIZE) / 2; + StringBuffer sbuf = new StringBuffer(); + sbuf.append(mCalendar.getDisplayName(Calendar.MONTH, Calendar.LONG, + Locale.getDefault())); + sbuf.append(" "); + sbuf.append(mYear); + canvas.drawText(sbuf.toString(), 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; + if (mSelectedDay == dayNumber) { + canvas.drawCircle(x, y - (MINI_DAY_NUMBER_TEXT_SIZE / 3), DAY_SELECTED_CIRCLE_SIZE, + mSelectedCirclePaint); + } + + if (mHasToday && mToday == dayNumber) { + mMonthNumPaint.setColor(mMonthTitleTextColor); + } else { + mMonthNumPaint.setColor(mDayTextColor); + } + canvas.drawText(Integer.valueOf(dayNumber).toString(), x, y, mMonthNumPaint); + + j++; + if (j == mNumDays) { + j = 0; + y += mRowHeight; + } + } + } + + 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 a Time referencing that day or null if + * + * @param x The x position of the touch event + * @return A time object for the tapped day or null if the position wasn't + * in a day + */ + public CalendarDay getDayFromLocation(float x, float y) { + int dayStart = mPadding; + if (x < dayStart || x > mWidth - mPadding) { + return null; + } + // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels + int row = (int) (y - mRowHeight) / mRowHeight; + int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)); + + int day = column - findDayOffset() + 1; + day += row * mNumDays; + if (day < 1 || day > mNumCells) { + return null; + } + return new CalendarDay(mYear, mMonth, day); + } + +} diff --git a/src/com/android/datetimepicker/date/YearPickerView.java b/src/com/android/datetimepicker/date/YearPickerView.java new file mode 100644 index 0000000..916437e --- /dev/null +++ b/src/com/android/datetimepicker/date/YearPickerView.java @@ -0,0 +1,68 @@ +/* + * 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.view.Gravity; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.NumberPicker; +import android.widget.NumberPicker.OnValueChangeListener; + +/** + * A number picker allowing a user to choose a specific year. + */ +public class YearPickerView extends FrameLayout implements OnValueChangeListener { + + private final NumberPicker mPicker; + private final DatePickerController mController; + + public YearPickerView(Context context, DatePickerController controller) { + super(context); + mController = controller; + ViewGroup.LayoutParams frame = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT); + setLayoutParams(frame); + mPicker = new NumberPicker(context); + LayoutParams params = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + params.gravity = Gravity.CENTER; + mPicker.setLayoutParams(params); + mPicker.setOnLongPressUpdateInterval(100); + mPicker.setMinValue(controller.getMinYear()); + mPicker.setMaxValue(controller.getMaxYear()); + mPicker.setWrapSelectorWheel(false); + mPicker.setValue(controller.getSelectedDay().year); + mPicker.setOnValueChangedListener(this); + addView(mPicker); + } + + public void setValue(int value) { + mPicker.setValue(value); + } + + public void onChange() { + mPicker.setMinValue(mController.getMinYear()); + mPicker.setMaxValue(mController.getMaxYear()); + requestLayout(); + } + + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + mController.onYearPickerSelectionChanged(newVal); + } +} |