/* * Copyright (C) 2015 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 android.widget; import com.android.internal.R; import com.android.internal.widget.ViewPager; import com.android.internal.widget.ViewPager.OnPageChangeListener; import android.annotation.Nullable; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.icu.util.Calendar; import android.util.AttributeSet; import android.util.MathUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; import java.util.Locale; import libcore.icu.LocaleData; class DayPickerView extends ViewGroup { private static final int DEFAULT_LAYOUT = R.layout.day_picker_content_material; private static final int DEFAULT_START_YEAR = 1900; private static final int DEFAULT_END_YEAR = 2100; private static final int[] ATTRS_TEXT_COLOR = new int[] { R.attr.textColor }; private final Calendar mSelectedDay = Calendar.getInstance(); private final Calendar mMinDate = Calendar.getInstance(); private final Calendar mMaxDate = Calendar.getInstance(); private final AccessibilityManager mAccessibilityManager; private final ViewPager mViewPager; private final ImageButton mPrevButton; private final ImageButton mNextButton; private final DayPickerPagerAdapter mAdapter; /** Temporary calendar used for date calculations. */ private Calendar mTempCalendar; private OnDaySelectedListener mOnDaySelectedListener; public DayPickerView(Context context) { this(context, null); } public DayPickerView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, R.attr.calendarViewStyle); } public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mAccessibilityManager = (AccessibilityManager) context.getSystemService( Context.ACCESSIBILITY_SERVICE); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CalendarView, defStyleAttr, defStyleRes); final int firstDayOfWeek = a.getInt(R.styleable.CalendarView_firstDayOfWeek, LocaleData.get(Locale.getDefault()).firstDayOfWeek); final String minDate = a.getString(R.styleable.CalendarView_minDate); final String maxDate = a.getString(R.styleable.CalendarView_maxDate); final int monthTextAppearanceResId = a.getResourceId( R.styleable.CalendarView_monthTextAppearance, R.style.TextAppearance_Material_Widget_Calendar_Month); final int dayOfWeekTextAppearanceResId = a.getResourceId( R.styleable.CalendarView_weekDayTextAppearance, R.style.TextAppearance_Material_Widget_Calendar_DayOfWeek); final int dayTextAppearanceResId = a.getResourceId( R.styleable.CalendarView_dateTextAppearance, R.style.TextAppearance_Material_Widget_Calendar_Day); final ColorStateList daySelectorColor = a.getColorStateList( R.styleable.CalendarView_daySelectorColor); a.recycle(); // Set up adapter. mAdapter = new DayPickerPagerAdapter(context, R.layout.date_picker_month_item_material, R.id.month_view); mAdapter.setMonthTextAppearance(monthTextAppearanceResId); mAdapter.setDayOfWeekTextAppearance(dayOfWeekTextAppearanceResId); mAdapter.setDayTextAppearance(dayTextAppearanceResId); mAdapter.setDaySelectorColor(daySelectorColor); final LayoutInflater inflater = LayoutInflater.from(context); final ViewGroup content = (ViewGroup) inflater.inflate(DEFAULT_LAYOUT, this, false); // Transfer all children from content to here. while (content.getChildCount() > 0) { final View child = content.getChildAt(0); content.removeViewAt(0); addView(child); } mPrevButton = (ImageButton) findViewById(R.id.prev); mPrevButton.setOnClickListener(mOnClickListener); mNextButton = (ImageButton) findViewById(R.id.next); mNextButton.setOnClickListener(mOnClickListener); mViewPager = (ViewPager) findViewById(R.id.day_picker_view_pager); mViewPager.setAdapter(mAdapter); mViewPager.setOnPageChangeListener(mOnPageChangedListener); // Proxy the month text color into the previous and next buttons. if (monthTextAppearanceResId != 0) { final TypedArray ta = mContext.obtainStyledAttributes(null, ATTRS_TEXT_COLOR, 0, monthTextAppearanceResId); final ColorStateList monthColor = ta.getColorStateList(0); if (monthColor != null) { mPrevButton.setImageTintList(monthColor); mNextButton.setImageTintList(monthColor); } ta.recycle(); } // Set up min and max dates. final Calendar tempDate = Calendar.getInstance(); if (!CalendarView.parseDate(minDate, tempDate)) { tempDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1); } final long minDateMillis = tempDate.getTimeInMillis(); if (!CalendarView.parseDate(maxDate, tempDate)) { tempDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31); } final long maxDateMillis = tempDate.getTimeInMillis(); if (maxDateMillis < minDateMillis) { throw new IllegalArgumentException("maxDate must be >= minDate"); } final long setDateMillis = MathUtils.constrain( System.currentTimeMillis(), minDateMillis, maxDateMillis); setFirstDayOfWeek(firstDayOfWeek); setMinDate(minDateMillis); setMaxDate(maxDateMillis); setDate(setDateMillis, false); // Proxy selection callbacks to our own listener. mAdapter.setOnDaySelectedListener(new DayPickerPagerAdapter.OnDaySelectedListener() { @Override public void onDaySelected(DayPickerPagerAdapter adapter, Calendar day) { if (mOnDaySelectedListener != null) { mOnDaySelectedListener.onDaySelected(DayPickerView.this, day); } } }); } private void updateButtonVisibility(int position) { final boolean hasPrev = position > 0; final boolean hasNext = position < (mAdapter.getCount() - 1); mPrevButton.setVisibility(hasPrev ? View.VISIBLE : View.INVISIBLE); mNextButton.setVisibility(hasNext ? View.VISIBLE : View.INVISIBLE); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final ViewPager viewPager = mViewPager; measureChild(viewPager, widthMeasureSpec, heightMeasureSpec); final int measuredWidthAndState = viewPager.getMeasuredWidthAndState(); final int measuredHeightAndState = viewPager.getMeasuredHeightAndState(); setMeasuredDimension(measuredWidthAndState, measuredHeightAndState); final int pagerWidth = viewPager.getMeasuredWidth(); final int pagerHeight = viewPager.getMeasuredHeight(); final int buttonWidthSpec = MeasureSpec.makeMeasureSpec(pagerWidth, MeasureSpec.AT_MOST); final int buttonHeightSpec = MeasureSpec.makeMeasureSpec(pagerHeight, MeasureSpec.AT_MOST); mPrevButton.measure(buttonWidthSpec, buttonHeightSpec); mNextButton.measure(buttonWidthSpec, buttonHeightSpec); } @Override public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); requestLayout(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final ImageButton leftButton; final ImageButton rightButton; if (isLayoutRtl()) { leftButton = mNextButton; rightButton = mPrevButton; } else { leftButton = mPrevButton; rightButton = mNextButton; } final int width = right - left; final int height = bottom - top; mViewPager.layout(0, 0, width, height); final SimpleMonthView monthView = (SimpleMonthView) mViewPager.getChildAt(0); final int monthHeight = monthView.getMonthHeight(); final int cellWidth = monthView.getCellWidth(); // Vertically center the previous/next buttons within the month // header, horizontally center within the day cell. final int leftDW = leftButton.getMeasuredWidth(); final int leftDH = leftButton.getMeasuredHeight(); final int leftIconTop = monthView.getPaddingTop() + (monthHeight - leftDH) / 2; final int leftIconLeft = monthView.getPaddingLeft() + (cellWidth - leftDW) / 2; leftButton.layout(leftIconLeft, leftIconTop, leftIconLeft + leftDW, leftIconTop + leftDH); final int rightDW = rightButton.getMeasuredWidth(); final int rightDH = rightButton.getMeasuredHeight(); final int rightIconTop = monthView.getPaddingTop() + (monthHeight - rightDH) / 2; final int rightIconRight = width - monthView.getPaddingRight() - (cellWidth - rightDW) / 2; rightButton.layout(rightIconRight - rightDW, rightIconTop, rightIconRight, rightIconTop + rightDH); } public void setDayOfWeekTextAppearance(int resId) { mAdapter.setDayOfWeekTextAppearance(resId); } public int getDayOfWeekTextAppearance() { return mAdapter.getDayOfWeekTextAppearance(); } public void setDayTextAppearance(int resId) { mAdapter.setDayTextAppearance(resId); } public int getDayTextAppearance() { return mAdapter.getDayTextAppearance(); } /** * Sets the currently selected date to the specified timestamp. Jumps * immediately to the new date. To animate to the new date, use * {@link #setDate(long, boolean)}. * * @param timeInMillis the target day in milliseconds */ public void setDate(long timeInMillis) { setDate(timeInMillis, false); } /** * Sets the currently selected date to the specified timestamp. Jumps * immediately to the new date, optionally animating the transition. * * @param timeInMillis the target day in milliseconds * @param animate whether to smooth scroll to the new position */ public void setDate(long timeInMillis, boolean animate) { setDate(timeInMillis, animate, true); } /** * Moves to the month containing the specified day, optionally setting the * day as selected. * * @param timeInMillis the target day in milliseconds * @param animate whether to smooth scroll to the new position * @param setSelected whether to set the specified day as selected */ private void setDate(long timeInMillis, boolean animate, boolean setSelected) { if (setSelected) { mSelectedDay.setTimeInMillis(timeInMillis); } final int position = getPositionFromDay(timeInMillis); if (position != mViewPager.getCurrentItem()) { mViewPager.setCurrentItem(position, animate); } mTempCalendar.setTimeInMillis(timeInMillis); mAdapter.setSelectedDay(mTempCalendar); } public long getDate() { return mSelectedDay.getTimeInMillis(); } public void setFirstDayOfWeek(int firstDayOfWeek) { mAdapter.setFirstDayOfWeek(firstDayOfWeek); } public int getFirstDayOfWeek() { return mAdapter.getFirstDayOfWeek(); } public void setMinDate(long timeInMillis) { mMinDate.setTimeInMillis(timeInMillis); onRangeChanged(); } public long getMinDate() { return mMinDate.getTimeInMillis(); } public void setMaxDate(long timeInMillis) { mMaxDate.setTimeInMillis(timeInMillis); onRangeChanged(); } public long getMaxDate() { return mMaxDate.getTimeInMillis(); } /** * Handles changes to date range. */ public void onRangeChanged() { mAdapter.setRange(mMinDate, mMaxDate); // Changing the min/max date changes the selection position since we // don't really have stable IDs. Jumps immediately to the new position. setDate(mSelectedDay.getTimeInMillis(), false, false); updateButtonVisibility(mViewPager.getCurrentItem()); } /** * Sets the listener to call when the user selects a day. * * @param listener The listener to call. */ public void setOnDaySelectedListener(OnDaySelectedListener listener) { mOnDaySelectedListener = listener; } private int getDiffMonths(Calendar start, Calendar end) { final int diffYears = end.get(Calendar.YEAR) - start.get(Calendar.YEAR); return end.get(Calendar.MONTH) - start.get(Calendar.MONTH) + 12 * diffYears; } private int getPositionFromDay(long timeInMillis) { final int diffMonthMax = getDiffMonths(mMinDate, mMaxDate); final int diffMonth = getDiffMonths(mMinDate, getTempCalendarForTime(timeInMillis)); return MathUtils.constrain(diffMonth, 0, diffMonthMax); } private Calendar getTempCalendarForTime(long timeInMillis) { if (mTempCalendar == null) { mTempCalendar = Calendar.getInstance(); } mTempCalendar.setTimeInMillis(timeInMillis); return mTempCalendar; } /** * Gets the position of the view that is most prominently displayed within the list view. */ public int getMostVisiblePosition() { return mViewPager.getCurrentItem(); } public void setPosition(int position) { mViewPager.setCurrentItem(position, false); } private final OnPageChangeListener mOnPageChangedListener = new OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { final float alpha = Math.abs(0.5f - positionOffset) * 2.0f; mPrevButton.setAlpha(alpha); mNextButton.setAlpha(alpha); } @Override public void onPageScrollStateChanged(int state) {} @Override public void onPageSelected(int position) { updateButtonVisibility(position); } }; private final OnClickListener mOnClickListener = new OnClickListener() { @Override public void onClick(View v) { final int direction; if (v == mPrevButton) { direction = -1; } else if (v == mNextButton) { direction = 1; } else { return; } // Animation is expensive for accessibility services since it sends // lots of scroll and content change events. final boolean animate = !mAccessibilityManager.isEnabled(); // ViewPager clamps input values, so we don't need to worry // about passing invalid indices. final int nextItem = mViewPager.getCurrentItem() + direction; mViewPager.setCurrentItem(nextItem, animate); } }; public interface OnDaySelectedListener { void onDaySelected(DayPickerView view, Calendar day); } }