Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.widget;
     18 
     19 import android.annotation.Nullable;
     20 import android.content.Context;
     21 import android.content.res.ColorStateList;
     22 import android.content.res.TypedArray;
     23 import android.graphics.Rect;
     24 import android.icu.util.Calendar;
     25 import android.util.AttributeSet;
     26 import android.util.MathUtils;
     27 import android.view.LayoutInflater;
     28 import android.view.View;
     29 import android.view.ViewGroup;
     30 import android.view.accessibility.AccessibilityManager;
     31 
     32 import com.android.internal.R;
     33 import com.android.internal.widget.ViewPager;
     34 import com.android.internal.widget.ViewPager.OnPageChangeListener;
     35 
     36 import libcore.icu.LocaleData;
     37 
     38 import java.util.Locale;
     39 
     40 class DayPickerView extends ViewGroup {
     41     private static final int DEFAULT_LAYOUT = R.layout.day_picker_content_material;
     42     private static final int DEFAULT_START_YEAR = 1900;
     43     private static final int DEFAULT_END_YEAR = 2100;
     44 
     45     private static final int[] ATTRS_TEXT_COLOR = new int[] { R.attr.textColor };
     46 
     47     private final Calendar mSelectedDay = Calendar.getInstance();
     48     private final Calendar mMinDate = Calendar.getInstance();
     49     private final Calendar mMaxDate = Calendar.getInstance();
     50 
     51     private final AccessibilityManager mAccessibilityManager;
     52 
     53     private final ViewPager mViewPager;
     54     private final ImageButton mPrevButton;
     55     private final ImageButton mNextButton;
     56 
     57     private final DayPickerPagerAdapter mAdapter;
     58 
     59     /** Temporary calendar used for date calculations. */
     60     private Calendar mTempCalendar;
     61 
     62     private OnDaySelectedListener mOnDaySelectedListener;
     63 
     64     public DayPickerView(Context context) {
     65         this(context, null);
     66     }
     67 
     68     public DayPickerView(Context context, @Nullable AttributeSet attrs) {
     69         this(context, attrs, R.attr.calendarViewStyle);
     70     }
     71 
     72     public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
     73         this(context, attrs, defStyleAttr, 0);
     74     }
     75 
     76     public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
     77             int defStyleRes) {
     78         super(context, attrs, defStyleAttr, defStyleRes);
     79 
     80         mAccessibilityManager = (AccessibilityManager) context.getSystemService(
     81                 Context.ACCESSIBILITY_SERVICE);
     82 
     83         final TypedArray a = context.obtainStyledAttributes(attrs,
     84                 R.styleable.CalendarView, defStyleAttr, defStyleRes);
     85         saveAttributeDataForStyleable(context, R.styleable.CalendarView,
     86                 attrs, a, defStyleAttr, defStyleRes);
     87 
     88         final int firstDayOfWeek = a.getInt(R.styleable.CalendarView_firstDayOfWeek,
     89                 LocaleData.get(Locale.getDefault()).firstDayOfWeek);
     90 
     91         final String minDate = a.getString(R.styleable.CalendarView_minDate);
     92         final String maxDate = a.getString(R.styleable.CalendarView_maxDate);
     93 
     94         final int monthTextAppearanceResId = a.getResourceId(
     95                 R.styleable.CalendarView_monthTextAppearance,
     96                 R.style.TextAppearance_Material_Widget_Calendar_Month);
     97         final int dayOfWeekTextAppearanceResId = a.getResourceId(
     98                 R.styleable.CalendarView_weekDayTextAppearance,
     99                 R.style.TextAppearance_Material_Widget_Calendar_DayOfWeek);
    100         final int dayTextAppearanceResId = a.getResourceId(
    101                 R.styleable.CalendarView_dateTextAppearance,
    102                 R.style.TextAppearance_Material_Widget_Calendar_Day);
    103 
    104         final ColorStateList daySelectorColor = a.getColorStateList(
    105                 R.styleable.CalendarView_daySelectorColor);
    106 
    107         a.recycle();
    108 
    109         // Set up adapter.
    110         mAdapter = new DayPickerPagerAdapter(context,
    111                 R.layout.date_picker_month_item_material, R.id.month_view);
    112         mAdapter.setMonthTextAppearance(monthTextAppearanceResId);
    113         mAdapter.setDayOfWeekTextAppearance(dayOfWeekTextAppearanceResId);
    114         mAdapter.setDayTextAppearance(dayTextAppearanceResId);
    115         mAdapter.setDaySelectorColor(daySelectorColor);
    116 
    117         final LayoutInflater inflater = LayoutInflater.from(context);
    118         final ViewGroup content = (ViewGroup) inflater.inflate(DEFAULT_LAYOUT, this, false);
    119 
    120         // Transfer all children from content to here.
    121         while (content.getChildCount() > 0) {
    122             final View child = content.getChildAt(0);
    123             content.removeViewAt(0);
    124             addView(child);
    125         }
    126 
    127         mPrevButton = findViewById(R.id.prev);
    128         mPrevButton.setOnClickListener(mOnClickListener);
    129 
    130         mNextButton = findViewById(R.id.next);
    131         mNextButton.setOnClickListener(mOnClickListener);
    132 
    133         mViewPager = findViewById(R.id.day_picker_view_pager);
    134         mViewPager.setAdapter(mAdapter);
    135         mViewPager.setOnPageChangeListener(mOnPageChangedListener);
    136 
    137         // Proxy the month text color into the previous and next buttons.
    138         if (monthTextAppearanceResId != 0) {
    139             final TypedArray ta = mContext.obtainStyledAttributes(null,
    140                     ATTRS_TEXT_COLOR, 0, monthTextAppearanceResId);
    141             final ColorStateList monthColor = ta.getColorStateList(0);
    142             if (monthColor != null) {
    143                 mPrevButton.setImageTintList(monthColor);
    144                 mNextButton.setImageTintList(monthColor);
    145             }
    146             ta.recycle();
    147         }
    148 
    149         // Set up min and max dates.
    150         final Calendar tempDate = Calendar.getInstance();
    151         if (!CalendarView.parseDate(minDate, tempDate)) {
    152             tempDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
    153         }
    154         final long minDateMillis = tempDate.getTimeInMillis();
    155 
    156         if (!CalendarView.parseDate(maxDate, tempDate)) {
    157             tempDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
    158         }
    159         final long maxDateMillis = tempDate.getTimeInMillis();
    160 
    161         if (maxDateMillis < minDateMillis) {
    162             throw new IllegalArgumentException("maxDate must be >= minDate");
    163         }
    164 
    165         final long setDateMillis = MathUtils.constrain(
    166                 System.currentTimeMillis(), minDateMillis, maxDateMillis);
    167 
    168         setFirstDayOfWeek(firstDayOfWeek);
    169         setMinDate(minDateMillis);
    170         setMaxDate(maxDateMillis);
    171         setDate(setDateMillis, false);
    172 
    173         // Proxy selection callbacks to our own listener.
    174         mAdapter.setOnDaySelectedListener(new DayPickerPagerAdapter.OnDaySelectedListener() {
    175             @Override
    176             public void onDaySelected(DayPickerPagerAdapter adapter, Calendar day) {
    177                 if (mOnDaySelectedListener != null) {
    178                     mOnDaySelectedListener.onDaySelected(DayPickerView.this, day);
    179                 }
    180             }
    181         });
    182     }
    183 
    184     private void updateButtonVisibility(int position) {
    185         final boolean hasPrev = position > 0;
    186         final boolean hasNext = position < (mAdapter.getCount() - 1);
    187         mPrevButton.setVisibility(hasPrev ? View.VISIBLE : View.INVISIBLE);
    188         mNextButton.setVisibility(hasNext ? View.VISIBLE : View.INVISIBLE);
    189     }
    190 
    191     @Override
    192     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    193         final ViewPager viewPager = mViewPager;
    194         measureChild(viewPager, widthMeasureSpec, heightMeasureSpec);
    195 
    196         final int measuredWidthAndState = viewPager.getMeasuredWidthAndState();
    197         final int measuredHeightAndState = viewPager.getMeasuredHeightAndState();
    198         setMeasuredDimension(measuredWidthAndState, measuredHeightAndState);
    199 
    200         final int pagerWidth = viewPager.getMeasuredWidth();
    201         final int pagerHeight = viewPager.getMeasuredHeight();
    202         final int buttonWidthSpec = MeasureSpec.makeMeasureSpec(pagerWidth, MeasureSpec.AT_MOST);
    203         final int buttonHeightSpec = MeasureSpec.makeMeasureSpec(pagerHeight, MeasureSpec.AT_MOST);
    204         mPrevButton.measure(buttonWidthSpec, buttonHeightSpec);
    205         mNextButton.measure(buttonWidthSpec, buttonHeightSpec);
    206     }
    207 
    208     @Override
    209     public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
    210         super.onRtlPropertiesChanged(layoutDirection);
    211 
    212         requestLayout();
    213     }
    214 
    215     @Override
    216     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    217         final ImageButton leftButton;
    218         final ImageButton rightButton;
    219         if (isLayoutRtl()) {
    220             leftButton = mNextButton;
    221             rightButton = mPrevButton;
    222         } else {
    223             leftButton = mPrevButton;
    224             rightButton = mNextButton;
    225         }
    226 
    227         final int width = right - left;
    228         final int height = bottom - top;
    229         mViewPager.layout(0, 0, width, height);
    230 
    231         final SimpleMonthView monthView = (SimpleMonthView) mViewPager.getChildAt(0);
    232         final int monthHeight = monthView.getMonthHeight();
    233         final int cellWidth = monthView.getCellWidth();
    234 
    235         // Vertically center the previous/next buttons within the month
    236         // header, horizontally center within the day cell.
    237         final int leftDW = leftButton.getMeasuredWidth();
    238         final int leftDH = leftButton.getMeasuredHeight();
    239         final int leftIconTop = monthView.getPaddingTop() + (monthHeight - leftDH) / 2;
    240         final int leftIconLeft = monthView.getPaddingLeft() + (cellWidth - leftDW) / 2;
    241         leftButton.layout(leftIconLeft, leftIconTop, leftIconLeft + leftDW, leftIconTop + leftDH);
    242 
    243         final int rightDW = rightButton.getMeasuredWidth();
    244         final int rightDH = rightButton.getMeasuredHeight();
    245         final int rightIconTop = monthView.getPaddingTop() + (monthHeight - rightDH) / 2;
    246         final int rightIconRight = width - monthView.getPaddingRight() - (cellWidth - rightDW) / 2;
    247         rightButton.layout(rightIconRight - rightDW, rightIconTop,
    248                 rightIconRight, rightIconTop + rightDH);
    249     }
    250 
    251     public void setDayOfWeekTextAppearance(int resId) {
    252         mAdapter.setDayOfWeekTextAppearance(resId);
    253     }
    254 
    255     public int getDayOfWeekTextAppearance() {
    256         return mAdapter.getDayOfWeekTextAppearance();
    257     }
    258 
    259     public void setDayTextAppearance(int resId) {
    260         mAdapter.setDayTextAppearance(resId);
    261     }
    262 
    263     public int getDayTextAppearance() {
    264         return mAdapter.getDayTextAppearance();
    265     }
    266 
    267     /**
    268      * Sets the currently selected date to the specified timestamp. Jumps
    269      * immediately to the new date. To animate to the new date, use
    270      * {@link #setDate(long, boolean)}.
    271      *
    272      * @param timeInMillis the target day in milliseconds
    273      */
    274     public void setDate(long timeInMillis) {
    275         setDate(timeInMillis, false);
    276     }
    277 
    278     /**
    279      * Sets the currently selected date to the specified timestamp. Jumps
    280      * immediately to the new date, optionally animating the transition.
    281      *
    282      * @param timeInMillis the target day in milliseconds
    283      * @param animate whether to smooth scroll to the new position
    284      */
    285     public void setDate(long timeInMillis, boolean animate) {
    286         setDate(timeInMillis, animate, true);
    287     }
    288 
    289     /**
    290      * Moves to the month containing the specified day, optionally setting the
    291      * day as selected.
    292      *
    293      * @param timeInMillis the target day in milliseconds
    294      * @param animate whether to smooth scroll to the new position
    295      * @param setSelected whether to set the specified day as selected
    296      */
    297     private void setDate(long timeInMillis, boolean animate, boolean setSelected) {
    298         boolean dateClamped = false;
    299         // Clamp the target day in milliseconds to the min or max if outside the range.
    300         if (timeInMillis < mMinDate.getTimeInMillis()) {
    301             timeInMillis = mMinDate.getTimeInMillis();
    302             dateClamped = true;
    303         } else if (timeInMillis > mMaxDate.getTimeInMillis()) {
    304             timeInMillis = mMaxDate.getTimeInMillis();
    305             dateClamped = true;
    306         }
    307 
    308         getTempCalendarForTime(timeInMillis);
    309 
    310         if (setSelected || dateClamped) {
    311             mSelectedDay.setTimeInMillis(timeInMillis);
    312         }
    313 
    314         final int position = getPositionFromDay(timeInMillis);
    315         if (position != mViewPager.getCurrentItem()) {
    316             mViewPager.setCurrentItem(position, animate);
    317         }
    318 
    319         mAdapter.setSelectedDay(mTempCalendar);
    320     }
    321 
    322     public long getDate() {
    323         return mSelectedDay.getTimeInMillis();
    324     }
    325 
    326     public boolean getBoundsForDate(long timeInMillis, Rect outBounds) {
    327         final int position = getPositionFromDay(timeInMillis);
    328         if (position != mViewPager.getCurrentItem()) {
    329             return false;
    330         }
    331 
    332         mTempCalendar.setTimeInMillis(timeInMillis);
    333         return mAdapter.getBoundsForDate(mTempCalendar, outBounds);
    334     }
    335 
    336     public void setFirstDayOfWeek(int firstDayOfWeek) {
    337         mAdapter.setFirstDayOfWeek(firstDayOfWeek);
    338     }
    339 
    340     public int getFirstDayOfWeek() {
    341         return mAdapter.getFirstDayOfWeek();
    342     }
    343 
    344     public void setMinDate(long timeInMillis) {
    345         mMinDate.setTimeInMillis(timeInMillis);
    346         onRangeChanged();
    347     }
    348 
    349     public long getMinDate() {
    350         return mMinDate.getTimeInMillis();
    351     }
    352 
    353     public void setMaxDate(long timeInMillis) {
    354         mMaxDate.setTimeInMillis(timeInMillis);
    355         onRangeChanged();
    356     }
    357 
    358     public long getMaxDate() {
    359         return mMaxDate.getTimeInMillis();
    360     }
    361 
    362     /**
    363      * Handles changes to date range.
    364      */
    365     public void onRangeChanged() {
    366         mAdapter.setRange(mMinDate, mMaxDate);
    367 
    368         // Changing the min/max date changes the selection position since we
    369         // don't really have stable IDs. Jumps immediately to the new position.
    370         setDate(mSelectedDay.getTimeInMillis(), false, false);
    371 
    372         updateButtonVisibility(mViewPager.getCurrentItem());
    373     }
    374 
    375     /**
    376      * Sets the listener to call when the user selects a day.
    377      *
    378      * @param listener The listener to call.
    379      */
    380     public void setOnDaySelectedListener(OnDaySelectedListener listener) {
    381         mOnDaySelectedListener = listener;
    382     }
    383 
    384     private int getDiffMonths(Calendar start, Calendar end) {
    385         final int diffYears = end.get(Calendar.YEAR) - start.get(Calendar.YEAR);
    386         return end.get(Calendar.MONTH) - start.get(Calendar.MONTH) + 12 * diffYears;
    387     }
    388 
    389     private int getPositionFromDay(long timeInMillis) {
    390         final int diffMonthMax = getDiffMonths(mMinDate, mMaxDate);
    391         final int diffMonth = getDiffMonths(mMinDate, getTempCalendarForTime(timeInMillis));
    392         return MathUtils.constrain(diffMonth, 0, diffMonthMax);
    393     }
    394 
    395     private Calendar getTempCalendarForTime(long timeInMillis) {
    396         if (mTempCalendar == null) {
    397             mTempCalendar = Calendar.getInstance();
    398         }
    399         mTempCalendar.setTimeInMillis(timeInMillis);
    400         return mTempCalendar;
    401     }
    402 
    403     /**
    404      * Gets the position of the view that is most prominently displayed within the list view.
    405      */
    406     public int getMostVisiblePosition() {
    407         return mViewPager.getCurrentItem();
    408     }
    409 
    410     public void setPosition(int position) {
    411         mViewPager.setCurrentItem(position, false);
    412     }
    413 
    414     private final OnPageChangeListener mOnPageChangedListener = new OnPageChangeListener() {
    415         @Override
    416         public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    417             final float alpha = Math.abs(0.5f - positionOffset) * 2.0f;
    418             mPrevButton.setAlpha(alpha);
    419             mNextButton.setAlpha(alpha);
    420         }
    421 
    422         @Override
    423         public void onPageScrollStateChanged(int state) {}
    424 
    425         @Override
    426         public void onPageSelected(int position) {
    427             updateButtonVisibility(position);
    428         }
    429     };
    430 
    431     private final OnClickListener mOnClickListener = new OnClickListener() {
    432         @Override
    433         public void onClick(View v) {
    434             final int direction;
    435             if (v == mPrevButton) {
    436                 direction = -1;
    437             } else if (v == mNextButton) {
    438                 direction = 1;
    439             } else {
    440                 return;
    441             }
    442 
    443             // Animation is expensive for accessibility services since it sends
    444             // lots of scroll and content change events.
    445             final boolean animate = !mAccessibilityManager.isEnabled();
    446 
    447             // ViewPager clamps input values, so we don't need to worry
    448             // about passing invalid indices.
    449             final int nextItem = mViewPager.getCurrentItem() + direction;
    450             mViewPager.setCurrentItem(nextItem, animate);
    451         }
    452     };
    453 
    454     public interface OnDaySelectedListener {
    455         void onDaySelected(DayPickerView view, Calendar day);
    456     }
    457 }
    458