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