13e9818e0267619fecebd55095ab26c53eff92e93James Kung/*
23e9818e0267619fecebd55095ab26c53eff92e93James Kung * Copyright (C) 2013 The Android Open Source Project
33e9818e0267619fecebd55095ab26c53eff92e93James Kung *
43e9818e0267619fecebd55095ab26c53eff92e93James Kung * Licensed under the Apache License, Version 2.0 (the "License");
53e9818e0267619fecebd55095ab26c53eff92e93James Kung * you may not use this file except in compliance with the License.
63e9818e0267619fecebd55095ab26c53eff92e93James Kung * You may obtain a copy of the License at
73e9818e0267619fecebd55095ab26c53eff92e93James Kung *
83e9818e0267619fecebd55095ab26c53eff92e93James Kung *      http://www.apache.org/licenses/LICENSE-2.0
93e9818e0267619fecebd55095ab26c53eff92e93James Kung *
103e9818e0267619fecebd55095ab26c53eff92e93James Kung * Unless required by applicable law or agreed to in writing, software
113e9818e0267619fecebd55095ab26c53eff92e93James Kung * distributed under the License is distributed on an "AS IS" BASIS,
123e9818e0267619fecebd55095ab26c53eff92e93James Kung * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
133e9818e0267619fecebd55095ab26c53eff92e93James Kung * See the License for the specific language governing permissions and
143e9818e0267619fecebd55095ab26c53eff92e93James Kung * limitations under the License.
153e9818e0267619fecebd55095ab26c53eff92e93James Kung */
163e9818e0267619fecebd55095ab26c53eff92e93James Kung
173e9818e0267619fecebd55095ab26c53eff92e93James Kungpackage com.android.datetimepicker.date;
183e9818e0267619fecebd55095ab26c53eff92e93James Kung
19cb3f2522609186db6239ad154af275957118295cSam Blitzsteinimport android.annotation.SuppressLint;
203e9818e0267619fecebd55095ab26c53eff92e93James Kungimport android.content.Context;
21a35f6b580aefd7bf1c7e92306e13dacd44316714Alan Viveretteimport android.os.Build;
22cb3f2522609186db6239ad154af275957118295cSam Blitzsteinimport android.os.Bundle;
233e9818e0267619fecebd55095ab26c53eff92e93James Kungimport android.os.Handler;
24385d36f15358e7d035b2e89e395588eb7952ebc2James Kungimport android.util.AttributeSet;
253e9818e0267619fecebd55095ab26c53eff92e93James Kungimport android.util.Log;
263e9818e0267619fecebd55095ab26c53eff92e93James Kungimport android.view.View;
273e9818e0267619fecebd55095ab26c53eff92e93James Kungimport android.view.ViewConfiguration;
28cb3f2522609186db6239ad154af275957118295cSam Blitzsteinimport android.view.accessibility.AccessibilityEvent;
29cb3f2522609186db6239ad154af275957118295cSam Blitzsteinimport android.view.accessibility.AccessibilityNodeInfo;
303e9818e0267619fecebd55095ab26c53eff92e93James Kungimport android.widget.AbsListView;
313e9818e0267619fecebd55095ab26c53eff92e93James Kungimport android.widget.AbsListView.OnScrollListener;
323e9818e0267619fecebd55095ab26c53eff92e93James Kungimport android.widget.ListView;
333e9818e0267619fecebd55095ab26c53eff92e93James Kung
34cb3f2522609186db6239ad154af275957118295cSam Blitzsteinimport com.android.datetimepicker.Utils;
352e00aa34c051111529290cf23c6ba940c2c0c142James Kungimport com.android.datetimepicker.date.DatePickerDialog.OnDateChangedListener;
36e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kungimport com.android.datetimepicker.date.MonthAdapter.CalendarDay;
373e9818e0267619fecebd55095ab26c53eff92e93James Kung
38cb3f2522609186db6239ad154af275957118295cSam Blitzsteinimport java.text.SimpleDateFormat;
39cb3f2522609186db6239ad154af275957118295cSam Blitzsteinimport java.util.Calendar;
40cb3f2522609186db6239ad154af275957118295cSam Blitzsteinimport java.util.Locale;
41cb3f2522609186db6239ad154af275957118295cSam Blitzstein
423e9818e0267619fecebd55095ab26c53eff92e93James Kung/**
433e9818e0267619fecebd55095ab26c53eff92e93James Kung * This displays a list of months in a calendar format with selectable days.
443e9818e0267619fecebd55095ab26c53eff92e93James Kung */
45e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kungpublic abstract class DayPickerView extends ListView implements OnScrollListener,
46e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung    OnDateChangedListener {
473e9818e0267619fecebd55095ab26c53eff92e93James Kung
483e9818e0267619fecebd55095ab26c53eff92e93James Kung    private static final String TAG = "MonthFragment";
493e9818e0267619fecebd55095ab26c53eff92e93James Kung
503e9818e0267619fecebd55095ab26c53eff92e93James Kung    // Affects when the month selection will change while scrolling up
513e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected static final int SCROLL_HYST_WEEKS = 2;
523e9818e0267619fecebd55095ab26c53eff92e93James Kung    // How long the GoTo fling animation should last
533e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected static final int GOTO_SCROLL_DURATION = 250;
543e9818e0267619fecebd55095ab26c53eff92e93James Kung    // How long to wait after receiving an onScrollStateChanged notification
553e9818e0267619fecebd55095ab26c53eff92e93James Kung    // before acting on it
563e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected static final int SCROLL_CHANGE_DELAY = 40;
573e9818e0267619fecebd55095ab26c53eff92e93James Kung    // The number of days to display in each week
583e9818e0267619fecebd55095ab26c53eff92e93James Kung    public static final int DAYS_PER_WEEK = 7;
593e9818e0267619fecebd55095ab26c53eff92e93James Kung    public static int LIST_TOP_OFFSET = -1; // so that the top line will be
603e9818e0267619fecebd55095ab26c53eff92e93James Kung                                            // under the separator
613e9818e0267619fecebd55095ab26c53eff92e93James Kung    // You can override these numbers to get a different appearance
623e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected int mNumWeeks = 6;
633e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected boolean mShowWeekNumber = false;
643e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected int mDaysPerWeek = 7;
65cb3f2522609186db6239ad154af275957118295cSam Blitzstein    private static SimpleDateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy", Locale.getDefault());
663e9818e0267619fecebd55095ab26c53eff92e93James Kung
673e9818e0267619fecebd55095ab26c53eff92e93James Kung    // These affect the scroll speed and feel
683e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected float mFriction = 1.0f;
693e9818e0267619fecebd55095ab26c53eff92e93James Kung
703e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected Context mContext;
713e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected Handler mHandler;
723e9818e0267619fecebd55095ab26c53eff92e93James Kung
733e9818e0267619fecebd55095ab26c53eff92e93James Kung    // highlighted time
743e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected CalendarDay mSelectedDay = new CalendarDay();
75e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung    protected MonthAdapter mAdapter;
763e9818e0267619fecebd55095ab26c53eff92e93James Kung
773e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected CalendarDay mTempDay = new CalendarDay();
783e9818e0267619fecebd55095ab26c53eff92e93James Kung
793e9818e0267619fecebd55095ab26c53eff92e93James Kung    // When the week starts; numbered like Time.<WEEKDAY> (e.g. SUNDAY=0).
803e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected int mFirstDayOfWeek;
813e9818e0267619fecebd55095ab26c53eff92e93James Kung    // The last name announced by accessibility
823e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected CharSequence mPrevMonthName;
833e9818e0267619fecebd55095ab26c53eff92e93James Kung    // which month should be displayed/highlighted [0-11]
843e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected int mCurrentMonthDisplayed;
853e9818e0267619fecebd55095ab26c53eff92e93James Kung    // used for tracking during a scroll
863e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected long mPreviousScrollPosition;
873e9818e0267619fecebd55095ab26c53eff92e93James Kung    // used for tracking what state listview is in
883e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE;
893e9818e0267619fecebd55095ab26c53eff92e93James Kung    // used for tracking what state listview is in
903e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE;
913e9818e0267619fecebd55095ab26c53eff92e93James Kung
92385d36f15358e7d035b2e89e395588eb7952ebc2James Kung    private DatePickerController mController;
93cb3f2522609186db6239ad154af275957118295cSam Blitzstein    private boolean mPerformingScroll;
943e9818e0267619fecebd55095ab26c53eff92e93James Kung
95385d36f15358e7d035b2e89e395588eb7952ebc2James Kung    public DayPickerView(Context context, AttributeSet attrs) {
96385d36f15358e7d035b2e89e395588eb7952ebc2James Kung        super(context, attrs);
97385d36f15358e7d035b2e89e395588eb7952ebc2James Kung        init(context);
98385d36f15358e7d035b2e89e395588eb7952ebc2James Kung    }
99385d36f15358e7d035b2e89e395588eb7952ebc2James Kung
1003e9818e0267619fecebd55095ab26c53eff92e93James Kung    public DayPickerView(Context context, DatePickerController controller) {
1013e9818e0267619fecebd55095ab26c53eff92e93James Kung        super(context);
102385d36f15358e7d035b2e89e395588eb7952ebc2James Kung        init(context);
103385d36f15358e7d035b2e89e395588eb7952ebc2James Kung        setController(controller);
104385d36f15358e7d035b2e89e395588eb7952ebc2James Kung    }
105385d36f15358e7d035b2e89e395588eb7952ebc2James Kung
106385d36f15358e7d035b2e89e395588eb7952ebc2James Kung    public void setController(DatePickerController controller) {
1073e9818e0267619fecebd55095ab26c53eff92e93James Kung        mController = controller;
1082e00aa34c051111529290cf23c6ba940c2c0c142James Kung        mController.registerOnDateChangedListener(this);
109e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung        refreshAdapter();
1102e00aa34c051111529290cf23c6ba940c2c0c142James Kung        onDateChanged();
1113e9818e0267619fecebd55095ab26c53eff92e93James Kung    }
1123e9818e0267619fecebd55095ab26c53eff92e93James Kung
1133e9818e0267619fecebd55095ab26c53eff92e93James Kung    public void init(Context context) {
114385d36f15358e7d035b2e89e395588eb7952ebc2James Kung        mHandler = new Handler();
115385d36f15358e7d035b2e89e395588eb7952ebc2James Kung        setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
116385d36f15358e7d035b2e89e395588eb7952ebc2James Kung        setDrawSelectorOnTop(false);
117385d36f15358e7d035b2e89e395588eb7952ebc2James Kung
1183e9818e0267619fecebd55095ab26c53eff92e93James Kung        mContext = context;
1193e9818e0267619fecebd55095ab26c53eff92e93James Kung        setUpListView();
1203e9818e0267619fecebd55095ab26c53eff92e93James Kung    }
1213e9818e0267619fecebd55095ab26c53eff92e93James Kung
1223e9818e0267619fecebd55095ab26c53eff92e93James Kung    public void onChange() {
123e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung        refreshAdapter();
1243e9818e0267619fecebd55095ab26c53eff92e93James Kung    }
1253e9818e0267619fecebd55095ab26c53eff92e93James Kung
1263e9818e0267619fecebd55095ab26c53eff92e93James Kung    /**
1273e9818e0267619fecebd55095ab26c53eff92e93James Kung     * Creates a new adapter if necessary and sets up its parameters. Override
1283e9818e0267619fecebd55095ab26c53eff92e93James Kung     * this method to provide a custom adapter.
1293e9818e0267619fecebd55095ab26c53eff92e93James Kung     */
130e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung    protected void refreshAdapter() {
1313e9818e0267619fecebd55095ab26c53eff92e93James Kung        if (mAdapter == null) {
132e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung            mAdapter = createMonthAdapter(getContext(), mController);
1333e9818e0267619fecebd55095ab26c53eff92e93James Kung        } else {
1343e9818e0267619fecebd55095ab26c53eff92e93James Kung            mAdapter.setSelectedDay(mSelectedDay);
1353e9818e0267619fecebd55095ab26c53eff92e93James Kung        }
1363e9818e0267619fecebd55095ab26c53eff92e93James Kung        // refresh the view with the new parameters
137e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung        setAdapter(mAdapter);
1383e9818e0267619fecebd55095ab26c53eff92e93James Kung    }
1393e9818e0267619fecebd55095ab26c53eff92e93James Kung
140e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung    public abstract MonthAdapter createMonthAdapter(Context context,
141e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung            DatePickerController controller);
142e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung
1433e9818e0267619fecebd55095ab26c53eff92e93James Kung    /*
1443e9818e0267619fecebd55095ab26c53eff92e93James Kung     * Sets all the required fields for the list view. Override this method to
1453e9818e0267619fecebd55095ab26c53eff92e93James Kung     * set a different list view behavior.
1463e9818e0267619fecebd55095ab26c53eff92e93James Kung     */
1473e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected void setUpListView() {
1483e9818e0267619fecebd55095ab26c53eff92e93James Kung        // Transparent background on scroll
1493e9818e0267619fecebd55095ab26c53eff92e93James Kung        setCacheColorHint(0);
1503e9818e0267619fecebd55095ab26c53eff92e93James Kung        // No dividers
1513e9818e0267619fecebd55095ab26c53eff92e93James Kung        setDivider(null);
1523e9818e0267619fecebd55095ab26c53eff92e93James Kung        // Items are clickable
1533e9818e0267619fecebd55095ab26c53eff92e93James Kung        setItemsCanFocus(true);
1543e9818e0267619fecebd55095ab26c53eff92e93James Kung        // The thumb gets in the way, so disable it
1553e9818e0267619fecebd55095ab26c53eff92e93James Kung        setFastScrollEnabled(false);
1563e9818e0267619fecebd55095ab26c53eff92e93James Kung        setVerticalScrollBarEnabled(false);
1573e9818e0267619fecebd55095ab26c53eff92e93James Kung        setOnScrollListener(this);
1583e9818e0267619fecebd55095ab26c53eff92e93James Kung        setFadingEdgeLength(0);
1593e9818e0267619fecebd55095ab26c53eff92e93James Kung        // Make the scrolling behavior nicer
1603e9818e0267619fecebd55095ab26c53eff92e93James Kung        setFriction(ViewConfiguration.getScrollFriction() * mFriction);
1613e9818e0267619fecebd55095ab26c53eff92e93James Kung    }
1623e9818e0267619fecebd55095ab26c53eff92e93James Kung
1633e9818e0267619fecebd55095ab26c53eff92e93James Kung    /**
1643e9818e0267619fecebd55095ab26c53eff92e93James Kung     * This moves to the specified time in the view. If the time is not already
1653e9818e0267619fecebd55095ab26c53eff92e93James Kung     * in range it will move the list so that the first of the month containing
1663e9818e0267619fecebd55095ab26c53eff92e93James Kung     * the time is at the top of the view. If the new time is already in view
1673e9818e0267619fecebd55095ab26c53eff92e93James Kung     * the list will not be scrolled unless forceScroll is true. This time may
1683e9818e0267619fecebd55095ab26c53eff92e93James Kung     * optionally be highlighted as selected as well.
1693e9818e0267619fecebd55095ab26c53eff92e93James Kung     *
1703e9818e0267619fecebd55095ab26c53eff92e93James Kung     * @param time The time to move to
1713e9818e0267619fecebd55095ab26c53eff92e93James Kung     * @param animate Whether to scroll to the given time or just redraw at the
1723e9818e0267619fecebd55095ab26c53eff92e93James Kung     *            new location
1733e9818e0267619fecebd55095ab26c53eff92e93James Kung     * @param setSelected Whether to set the given time as selected
1743e9818e0267619fecebd55095ab26c53eff92e93James Kung     * @param forceScroll Whether to recenter even if the time is already
1753e9818e0267619fecebd55095ab26c53eff92e93James Kung     *            visible
1763e9818e0267619fecebd55095ab26c53eff92e93James Kung     * @return Whether or not the view animated to the new location
1773e9818e0267619fecebd55095ab26c53eff92e93James Kung     */
1783e9818e0267619fecebd55095ab26c53eff92e93James Kung    public boolean goTo(CalendarDay day, boolean animate, boolean setSelected, boolean forceScroll) {
1793e9818e0267619fecebd55095ab26c53eff92e93James Kung
1803e9818e0267619fecebd55095ab26c53eff92e93James Kung        // Set the selected day
1813e9818e0267619fecebd55095ab26c53eff92e93James Kung        if (setSelected) {
1823e9818e0267619fecebd55095ab26c53eff92e93James Kung            mSelectedDay.set(day);
1833e9818e0267619fecebd55095ab26c53eff92e93James Kung        }
1843e9818e0267619fecebd55095ab26c53eff92e93James Kung
1853e9818e0267619fecebd55095ab26c53eff92e93James Kung        mTempDay.set(day);
1862e00aa34c051111529290cf23c6ba940c2c0c142James Kung        final int position = (day.year - mController.getMinYear())
187e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung                * MonthAdapter.MONTHS_IN_YEAR + day.month;
1883e9818e0267619fecebd55095ab26c53eff92e93James Kung
1893e9818e0267619fecebd55095ab26c53eff92e93James Kung        View child;
1903e9818e0267619fecebd55095ab26c53eff92e93James Kung        int i = 0;
1913e9818e0267619fecebd55095ab26c53eff92e93James Kung        int top = 0;
1923e9818e0267619fecebd55095ab26c53eff92e93James Kung        // Find a child that's completely in the view
1933e9818e0267619fecebd55095ab26c53eff92e93James Kung        do {
1943e9818e0267619fecebd55095ab26c53eff92e93James Kung            child = getChildAt(i++);
1953e9818e0267619fecebd55095ab26c53eff92e93James Kung            if (child == null) {
1963e9818e0267619fecebd55095ab26c53eff92e93James Kung                break;
1973e9818e0267619fecebd55095ab26c53eff92e93James Kung            }
1983e9818e0267619fecebd55095ab26c53eff92e93James Kung            top = child.getTop();
1993e9818e0267619fecebd55095ab26c53eff92e93James Kung            if (Log.isLoggable(TAG, Log.DEBUG)) {
2003e9818e0267619fecebd55095ab26c53eff92e93James Kung                Log.d(TAG, "child at " + (i - 1) + " has top " + top);
2013e9818e0267619fecebd55095ab26c53eff92e93James Kung            }
2023e9818e0267619fecebd55095ab26c53eff92e93James Kung        } while (top < 0);
2033e9818e0267619fecebd55095ab26c53eff92e93James Kung
2043e9818e0267619fecebd55095ab26c53eff92e93James Kung        // Compute the first and last position visible
2053e9818e0267619fecebd55095ab26c53eff92e93James Kung        int selectedPosition;
2063e9818e0267619fecebd55095ab26c53eff92e93James Kung        if (child != null) {
2073e9818e0267619fecebd55095ab26c53eff92e93James Kung            selectedPosition = getPositionForView(child);
2083e9818e0267619fecebd55095ab26c53eff92e93James Kung        } else {
2093e9818e0267619fecebd55095ab26c53eff92e93James Kung            selectedPosition = 0;
2103e9818e0267619fecebd55095ab26c53eff92e93James Kung        }
2113e9818e0267619fecebd55095ab26c53eff92e93James Kung
2123e9818e0267619fecebd55095ab26c53eff92e93James Kung        if (setSelected) {
2133e9818e0267619fecebd55095ab26c53eff92e93James Kung            mAdapter.setSelectedDay(mSelectedDay);
2143e9818e0267619fecebd55095ab26c53eff92e93James Kung        }
2153e9818e0267619fecebd55095ab26c53eff92e93James Kung
2163e9818e0267619fecebd55095ab26c53eff92e93James Kung        if (Log.isLoggable(TAG, Log.DEBUG)) {
2173e9818e0267619fecebd55095ab26c53eff92e93James Kung            Log.d(TAG, "GoTo position " + position);
2183e9818e0267619fecebd55095ab26c53eff92e93James Kung        }
2193e9818e0267619fecebd55095ab26c53eff92e93James Kung        // Check if the selected day is now outside of our visible range
2203e9818e0267619fecebd55095ab26c53eff92e93James Kung        // and if so scroll to the month that contains it
2213e9818e0267619fecebd55095ab26c53eff92e93James Kung        if (position != selectedPosition || forceScroll) {
2223e9818e0267619fecebd55095ab26c53eff92e93James Kung            setMonthDisplayed(mTempDay);
2233e9818e0267619fecebd55095ab26c53eff92e93James Kung            mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING;
2243e9818e0267619fecebd55095ab26c53eff92e93James Kung            if (animate) {
2253e9818e0267619fecebd55095ab26c53eff92e93James Kung                smoothScrollToPositionFromTop(
2263e9818e0267619fecebd55095ab26c53eff92e93James Kung                        position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION);
2273e9818e0267619fecebd55095ab26c53eff92e93James Kung                return true;
2283e9818e0267619fecebd55095ab26c53eff92e93James Kung            } else {
2292e00aa34c051111529290cf23c6ba940c2c0c142James Kung                postSetSelection(position);
2303e9818e0267619fecebd55095ab26c53eff92e93James Kung            }
2313e9818e0267619fecebd55095ab26c53eff92e93James Kung        } else if (setSelected) {
2323e9818e0267619fecebd55095ab26c53eff92e93James Kung            setMonthDisplayed(mSelectedDay);
2333e9818e0267619fecebd55095ab26c53eff92e93James Kung        }
2343e9818e0267619fecebd55095ab26c53eff92e93James Kung        return false;
2353e9818e0267619fecebd55095ab26c53eff92e93James Kung    }
2363e9818e0267619fecebd55095ab26c53eff92e93James Kung
2372e00aa34c051111529290cf23c6ba940c2c0c142James Kung    public void postSetSelection(final int position) {
2382e00aa34c051111529290cf23c6ba940c2c0c142James Kung        clearFocus();
2392e00aa34c051111529290cf23c6ba940c2c0c142James Kung        post(new Runnable() {
2402e00aa34c051111529290cf23c6ba940c2c0c142James Kung
2412e00aa34c051111529290cf23c6ba940c2c0c142James Kung            @Override
2422e00aa34c051111529290cf23c6ba940c2c0c142James Kung            public void run() {
2432e00aa34c051111529290cf23c6ba940c2c0c142James Kung                setSelection(position);
2442e00aa34c051111529290cf23c6ba940c2c0c142James Kung            }
2452e00aa34c051111529290cf23c6ba940c2c0c142James Kung        });
2462e00aa34c051111529290cf23c6ba940c2c0c142James Kung        onScrollStateChanged(this, OnScrollListener.SCROLL_STATE_IDLE);
2472e00aa34c051111529290cf23c6ba940c2c0c142James Kung    }
2482e00aa34c051111529290cf23c6ba940c2c0c142James Kung
2493e9818e0267619fecebd55095ab26c53eff92e93James Kung    /**
2503e9818e0267619fecebd55095ab26c53eff92e93James Kung     * Updates the title and selected month if the view has moved to a new
2513e9818e0267619fecebd55095ab26c53eff92e93James Kung     * month.
2523e9818e0267619fecebd55095ab26c53eff92e93James Kung     */
2533e9818e0267619fecebd55095ab26c53eff92e93James Kung    @Override
2543e9818e0267619fecebd55095ab26c53eff92e93James Kung    public void onScroll(
2553e9818e0267619fecebd55095ab26c53eff92e93James Kung            AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
256e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung        MonthView child = (MonthView) view.getChildAt(0);
2573e9818e0267619fecebd55095ab26c53eff92e93James Kung        if (child == null) {
2583e9818e0267619fecebd55095ab26c53eff92e93James Kung            return;
2593e9818e0267619fecebd55095ab26c53eff92e93James Kung        }
2603e9818e0267619fecebd55095ab26c53eff92e93James Kung
2613e9818e0267619fecebd55095ab26c53eff92e93James Kung        // Figure out where we are
2623e9818e0267619fecebd55095ab26c53eff92e93James Kung        long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom();
2633e9818e0267619fecebd55095ab26c53eff92e93James Kung        mPreviousScrollPosition = currScroll;
2643e9818e0267619fecebd55095ab26c53eff92e93James Kung        mPreviousScrollState = mCurrentScrollState;
2653e9818e0267619fecebd55095ab26c53eff92e93James Kung    }
2663e9818e0267619fecebd55095ab26c53eff92e93James Kung
2673e9818e0267619fecebd55095ab26c53eff92e93James Kung    /**
2683e9818e0267619fecebd55095ab26c53eff92e93James Kung     * Sets the month displayed at the top of this view based on time. Override
2693e9818e0267619fecebd55095ab26c53eff92e93James Kung     * to add custom events when the title is changed.
2703e9818e0267619fecebd55095ab26c53eff92e93James Kung     */
2713e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected void setMonthDisplayed(CalendarDay date) {
2723e9818e0267619fecebd55095ab26c53eff92e93James Kung        mCurrentMonthDisplayed = date.month;
2733e9818e0267619fecebd55095ab26c53eff92e93James Kung        invalidateViews();
2743e9818e0267619fecebd55095ab26c53eff92e93James Kung    }
2753e9818e0267619fecebd55095ab26c53eff92e93James Kung
2763e9818e0267619fecebd55095ab26c53eff92e93James Kung    @Override
2773e9818e0267619fecebd55095ab26c53eff92e93James Kung    public void onScrollStateChanged(AbsListView view, int scrollState) {
2783e9818e0267619fecebd55095ab26c53eff92e93James Kung        // use a post to prevent re-entering onScrollStateChanged before it
2793e9818e0267619fecebd55095ab26c53eff92e93James Kung        // exits
2803e9818e0267619fecebd55095ab26c53eff92e93James Kung        mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
2813e9818e0267619fecebd55095ab26c53eff92e93James Kung    }
2823e9818e0267619fecebd55095ab26c53eff92e93James Kung
2833e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable();
2843e9818e0267619fecebd55095ab26c53eff92e93James Kung
2853e9818e0267619fecebd55095ab26c53eff92e93James Kung    protected class ScrollStateRunnable implements Runnable {
2863e9818e0267619fecebd55095ab26c53eff92e93James Kung        private int mNewState;
2873e9818e0267619fecebd55095ab26c53eff92e93James Kung
2883e9818e0267619fecebd55095ab26c53eff92e93James Kung        /**
2893e9818e0267619fecebd55095ab26c53eff92e93James Kung         * Sets up the runnable with a short delay in case the scroll state
2903e9818e0267619fecebd55095ab26c53eff92e93James Kung         * immediately changes again.
2913e9818e0267619fecebd55095ab26c53eff92e93James Kung         *
2923e9818e0267619fecebd55095ab26c53eff92e93James Kung         * @param view The list view that changed state
2933e9818e0267619fecebd55095ab26c53eff92e93James Kung         * @param scrollState The new state it changed to
2943e9818e0267619fecebd55095ab26c53eff92e93James Kung         */
2953e9818e0267619fecebd55095ab26c53eff92e93James Kung        public void doScrollStateChange(AbsListView view, int scrollState) {
2963e9818e0267619fecebd55095ab26c53eff92e93James Kung            mHandler.removeCallbacks(this);
2973e9818e0267619fecebd55095ab26c53eff92e93James Kung            mNewState = scrollState;
2983e9818e0267619fecebd55095ab26c53eff92e93James Kung            mHandler.postDelayed(this, SCROLL_CHANGE_DELAY);
2993e9818e0267619fecebd55095ab26c53eff92e93James Kung        }
3003e9818e0267619fecebd55095ab26c53eff92e93James Kung
3013e9818e0267619fecebd55095ab26c53eff92e93James Kung        @Override
3023e9818e0267619fecebd55095ab26c53eff92e93James Kung        public void run() {
3033e9818e0267619fecebd55095ab26c53eff92e93James Kung            mCurrentScrollState = mNewState;
3043e9818e0267619fecebd55095ab26c53eff92e93James Kung            if (Log.isLoggable(TAG, Log.DEBUG)) {
3053e9818e0267619fecebd55095ab26c53eff92e93James Kung                Log.d(TAG,
3063e9818e0267619fecebd55095ab26c53eff92e93James Kung                        "new scroll state: " + mNewState + " old state: " + mPreviousScrollState);
3073e9818e0267619fecebd55095ab26c53eff92e93James Kung            }
3083e9818e0267619fecebd55095ab26c53eff92e93James Kung            // Fix the position after a scroll or a fling ends
3093e9818e0267619fecebd55095ab26c53eff92e93James Kung            if (mNewState == OnScrollListener.SCROLL_STATE_IDLE
3103e9818e0267619fecebd55095ab26c53eff92e93James Kung                    && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE
3113e9818e0267619fecebd55095ab26c53eff92e93James Kung                    && mPreviousScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
3123e9818e0267619fecebd55095ab26c53eff92e93James Kung                mPreviousScrollState = mNewState;
3133e9818e0267619fecebd55095ab26c53eff92e93James Kung                int i = 0;
3143e9818e0267619fecebd55095ab26c53eff92e93James Kung                View child = getChildAt(i);
3153e9818e0267619fecebd55095ab26c53eff92e93James Kung                while (child != null && child.getBottom() <= 0) {
3163e9818e0267619fecebd55095ab26c53eff92e93James Kung                    child = getChildAt(++i);
3173e9818e0267619fecebd55095ab26c53eff92e93James Kung                }
3183e9818e0267619fecebd55095ab26c53eff92e93James Kung                if (child == null) {
3193e9818e0267619fecebd55095ab26c53eff92e93James Kung                    // The view is no longer visible, just return
3203e9818e0267619fecebd55095ab26c53eff92e93James Kung                    return;
3213e9818e0267619fecebd55095ab26c53eff92e93James Kung                }
3223e9818e0267619fecebd55095ab26c53eff92e93James Kung                int firstPosition = getFirstVisiblePosition();
3233e9818e0267619fecebd55095ab26c53eff92e93James Kung                int lastPosition = getLastVisiblePosition();
3243e9818e0267619fecebd55095ab26c53eff92e93James Kung                boolean scroll = firstPosition != 0 && lastPosition != getCount() - 1;
3253e9818e0267619fecebd55095ab26c53eff92e93James Kung                final int top = child.getTop();
3263e9818e0267619fecebd55095ab26c53eff92e93James Kung                final int bottom = child.getBottom();
3273e9818e0267619fecebd55095ab26c53eff92e93James Kung                final int midpoint = getHeight() / 2;
3283e9818e0267619fecebd55095ab26c53eff92e93James Kung                if (scroll && top < LIST_TOP_OFFSET) {
3293e9818e0267619fecebd55095ab26c53eff92e93James Kung                    if (bottom > midpoint) {
3303e9818e0267619fecebd55095ab26c53eff92e93James Kung                        smoothScrollBy(top, GOTO_SCROLL_DURATION);
3313e9818e0267619fecebd55095ab26c53eff92e93James Kung                    } else {
3323e9818e0267619fecebd55095ab26c53eff92e93James Kung                        smoothScrollBy(bottom, GOTO_SCROLL_DURATION);
3333e9818e0267619fecebd55095ab26c53eff92e93James Kung                    }
3343e9818e0267619fecebd55095ab26c53eff92e93James Kung                }
3353e9818e0267619fecebd55095ab26c53eff92e93James Kung            } else {
3363e9818e0267619fecebd55095ab26c53eff92e93James Kung                mPreviousScrollState = mNewState;
3373e9818e0267619fecebd55095ab26c53eff92e93James Kung            }
3383e9818e0267619fecebd55095ab26c53eff92e93James Kung        }
3393e9818e0267619fecebd55095ab26c53eff92e93James Kung    }
3402e00aa34c051111529290cf23c6ba940c2c0c142James Kung
3412e00aa34c051111529290cf23c6ba940c2c0c142James Kung    /**
3422e00aa34c051111529290cf23c6ba940c2c0c142James Kung     * Gets the position of the view that is most prominently displayed within the list view.
3432e00aa34c051111529290cf23c6ba940c2c0c142James Kung     */
3442e00aa34c051111529290cf23c6ba940c2c0c142James Kung    public int getMostVisiblePosition() {
3452e00aa34c051111529290cf23c6ba940c2c0c142James Kung        final int firstPosition = getFirstVisiblePosition();
3462e00aa34c051111529290cf23c6ba940c2c0c142James Kung        final int height = getHeight();
3472e00aa34c051111529290cf23c6ba940c2c0c142James Kung
3482e00aa34c051111529290cf23c6ba940c2c0c142James Kung        int maxDisplayedHeight = 0;
3492e00aa34c051111529290cf23c6ba940c2c0c142James Kung        int mostVisibleIndex = 0;
3502e00aa34c051111529290cf23c6ba940c2c0c142James Kung        int i=0;
3512e00aa34c051111529290cf23c6ba940c2c0c142James Kung        int bottom = 0;
3522e00aa34c051111529290cf23c6ba940c2c0c142James Kung        while (bottom < height) {
3532e00aa34c051111529290cf23c6ba940c2c0c142James Kung            View child = getChildAt(i);
3542e00aa34c051111529290cf23c6ba940c2c0c142James Kung            if (child == null) {
3552e00aa34c051111529290cf23c6ba940c2c0c142James Kung                break;
3562e00aa34c051111529290cf23c6ba940c2c0c142James Kung            }
3572e00aa34c051111529290cf23c6ba940c2c0c142James Kung            bottom = child.getBottom();
3582e00aa34c051111529290cf23c6ba940c2c0c142James Kung            int displayedHeight = Math.min(bottom, height) - Math.max(0, child.getTop());
3592e00aa34c051111529290cf23c6ba940c2c0c142James Kung            if (displayedHeight > maxDisplayedHeight) {
3602e00aa34c051111529290cf23c6ba940c2c0c142James Kung                mostVisibleIndex = i;
3612e00aa34c051111529290cf23c6ba940c2c0c142James Kung                maxDisplayedHeight = displayedHeight;
3622e00aa34c051111529290cf23c6ba940c2c0c142James Kung            }
3632e00aa34c051111529290cf23c6ba940c2c0c142James Kung            i++;
3642e00aa34c051111529290cf23c6ba940c2c0c142James Kung        }
3652e00aa34c051111529290cf23c6ba940c2c0c142James Kung        return firstPosition + mostVisibleIndex;
3662e00aa34c051111529290cf23c6ba940c2c0c142James Kung    }
3672e00aa34c051111529290cf23c6ba940c2c0c142James Kung
3682e00aa34c051111529290cf23c6ba940c2c0c142James Kung    @Override
3692e00aa34c051111529290cf23c6ba940c2c0c142James Kung    public void onDateChanged() {
3702e00aa34c051111529290cf23c6ba940c2c0c142James Kung        goTo(mController.getSelectedDay(), false, true, true);
3712e00aa34c051111529290cf23c6ba940c2c0c142James Kung    }
372090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette
373090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette    /**
374090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette     * Attempts to return the date that has accessibility focus.
375090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette     *
376090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette     * @return The date that has accessibility focus, or {@code null} if no date
377090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette     *         has focus.
378090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette     */
379090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette    private CalendarDay findAccessibilityFocus() {
380090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette        final int childCount = getChildCount();
381090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette        for (int i = 0; i < childCount; i++) {
382090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette            final View child = getChildAt(i);
383e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung            if (child instanceof MonthView) {
384e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung                final CalendarDay focus = ((MonthView) child).getAccessibilityFocus();
385090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette                if (focus != null) {
386a35f6b580aefd7bf1c7e92306e13dacd44316714Alan Viverette                    if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) {
387a35f6b580aefd7bf1c7e92306e13dacd44316714Alan Viverette                        // Clear focus to avoid ListView bug in Jelly Bean MR1.
388e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung                        ((MonthView) child).clearAccessibilityFocus();
389a35f6b580aefd7bf1c7e92306e13dacd44316714Alan Viverette                    }
390090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette                    return focus;
391090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette                }
392090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette            }
393090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette        }
394090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette
395090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette        return null;
396090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette    }
397090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette
398090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette    /**
399090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette     * Attempts to restore accessibility focus to a given date. No-op if
400090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette     * {@code day} is {@code null}.
401090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette     *
402090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette     * @param day The date that should receive accessibility focus
403090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette     * @return {@code true} if focus was restored
404090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette     */
405090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette    private boolean restoreAccessibilityFocus(CalendarDay day) {
406090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette        if (day == null) {
407090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette            return false;
408090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette        }
409090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette
410090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette        final int childCount = getChildCount();
411090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette        for (int i = 0; i < childCount; i++) {
412090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette            final View child = getChildAt(i);
413e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung            if (child instanceof MonthView) {
414e668d6b1b77ac4b127f961150e0d0a8a088143d9James Kung                if (((MonthView) child).restoreAccessibilityFocus(day)) {
415090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette                    return true;
416090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette                }
417090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette            }
418090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette        }
419090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette
420090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette        return false;
421090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette    }
422090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette
423090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette    @Override
424090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette    protected void layoutChildren() {
425090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette        final CalendarDay focusedDay = findAccessibilityFocus();
426090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette        super.layoutChildren();
427cb3f2522609186db6239ad154af275957118295cSam Blitzstein        if (mPerformingScroll) {
428cb3f2522609186db6239ad154af275957118295cSam Blitzstein            mPerformingScroll = false;
429cb3f2522609186db6239ad154af275957118295cSam Blitzstein        } else {
430cb3f2522609186db6239ad154af275957118295cSam Blitzstein            restoreAccessibilityFocus(focusedDay);
431cb3f2522609186db6239ad154af275957118295cSam Blitzstein        }
432cb3f2522609186db6239ad154af275957118295cSam Blitzstein    }
433cb3f2522609186db6239ad154af275957118295cSam Blitzstein
434cb3f2522609186db6239ad154af275957118295cSam Blitzstein    @Override
435cb3f2522609186db6239ad154af275957118295cSam Blitzstein    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
436cb3f2522609186db6239ad154af275957118295cSam Blitzstein        super.onInitializeAccessibilityEvent(event);
437cb3f2522609186db6239ad154af275957118295cSam Blitzstein        event.setItemCount(-1);
438cb3f2522609186db6239ad154af275957118295cSam Blitzstein   }
439cb3f2522609186db6239ad154af275957118295cSam Blitzstein
440aa0dd58622335c458af9afc0e0656e4664293087Scott Kennedy    private static String getMonthAndYearString(CalendarDay day) {
441cb3f2522609186db6239ad154af275957118295cSam Blitzstein        Calendar cal = Calendar.getInstance();
442cb3f2522609186db6239ad154af275957118295cSam Blitzstein        cal.set(day.year, day.month, day.day);
443cb3f2522609186db6239ad154af275957118295cSam Blitzstein
444cb3f2522609186db6239ad154af275957118295cSam Blitzstein        StringBuffer sbuf = new StringBuffer();
445cb3f2522609186db6239ad154af275957118295cSam Blitzstein        sbuf.append(cal.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault()));
446cb3f2522609186db6239ad154af275957118295cSam Blitzstein        sbuf.append(" ");
447cb3f2522609186db6239ad154af275957118295cSam Blitzstein        sbuf.append(YEAR_FORMAT.format(cal.getTime()));
448cb3f2522609186db6239ad154af275957118295cSam Blitzstein        return sbuf.toString();
449cb3f2522609186db6239ad154af275957118295cSam Blitzstein    }
450cb3f2522609186db6239ad154af275957118295cSam Blitzstein
451cb3f2522609186db6239ad154af275957118295cSam Blitzstein    /**
452cb3f2522609186db6239ad154af275957118295cSam Blitzstein     * Necessary for accessibility, to ensure we support "scrolling" forward and backward
453cb3f2522609186db6239ad154af275957118295cSam Blitzstein     * in the month list.
454cb3f2522609186db6239ad154af275957118295cSam Blitzstein     */
455cb3f2522609186db6239ad154af275957118295cSam Blitzstein    @Override
456cb3f2522609186db6239ad154af275957118295cSam Blitzstein    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
457cb3f2522609186db6239ad154af275957118295cSam Blitzstein      super.onInitializeAccessibilityNodeInfo(info);
458cb3f2522609186db6239ad154af275957118295cSam Blitzstein      info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
459cb3f2522609186db6239ad154af275957118295cSam Blitzstein      info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
460cb3f2522609186db6239ad154af275957118295cSam Blitzstein    }
461cb3f2522609186db6239ad154af275957118295cSam Blitzstein
462cb3f2522609186db6239ad154af275957118295cSam Blitzstein    /**
463cb3f2522609186db6239ad154af275957118295cSam Blitzstein     * When scroll forward/backward events are received, announce the newly scrolled-to month.
464cb3f2522609186db6239ad154af275957118295cSam Blitzstein     */
465cb3f2522609186db6239ad154af275957118295cSam Blitzstein    @SuppressLint("NewApi")
466cb3f2522609186db6239ad154af275957118295cSam Blitzstein    @Override
467cb3f2522609186db6239ad154af275957118295cSam Blitzstein    public boolean performAccessibilityAction(int action, Bundle arguments) {
468cb3f2522609186db6239ad154af275957118295cSam Blitzstein        if (action != AccessibilityNodeInfo.ACTION_SCROLL_FORWARD &&
469cb3f2522609186db6239ad154af275957118295cSam Blitzstein                action != AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
470cb3f2522609186db6239ad154af275957118295cSam Blitzstein            return super.performAccessibilityAction(action, arguments);
471cb3f2522609186db6239ad154af275957118295cSam Blitzstein        }
472cb3f2522609186db6239ad154af275957118295cSam Blitzstein
473cb3f2522609186db6239ad154af275957118295cSam Blitzstein        // Figure out what month is showing.
474cb3f2522609186db6239ad154af275957118295cSam Blitzstein        int firstVisiblePosition = getFirstVisiblePosition();
475cb3f2522609186db6239ad154af275957118295cSam Blitzstein        int month = firstVisiblePosition % 12;
476cb3f2522609186db6239ad154af275957118295cSam Blitzstein        int year = firstVisiblePosition / 12 + mController.getMinYear();
477cb3f2522609186db6239ad154af275957118295cSam Blitzstein        CalendarDay day = new CalendarDay(year, month, 1);
478cb3f2522609186db6239ad154af275957118295cSam Blitzstein
479cb3f2522609186db6239ad154af275957118295cSam Blitzstein        // Scroll either forward or backward one month.
480cb3f2522609186db6239ad154af275957118295cSam Blitzstein        if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
481cb3f2522609186db6239ad154af275957118295cSam Blitzstein            day.month++;
482cb3f2522609186db6239ad154af275957118295cSam Blitzstein            if (day.month == 12) {
483cb3f2522609186db6239ad154af275957118295cSam Blitzstein                day.month = 0;
484cb3f2522609186db6239ad154af275957118295cSam Blitzstein                day.year++;
485cb3f2522609186db6239ad154af275957118295cSam Blitzstein            }
486cb3f2522609186db6239ad154af275957118295cSam Blitzstein        } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
487cb3f2522609186db6239ad154af275957118295cSam Blitzstein            View firstVisibleView = getChildAt(0);
488cb3f2522609186db6239ad154af275957118295cSam Blitzstein            // If the view is fully visible, jump one month back. Otherwise, we'll just jump
489cb3f2522609186db6239ad154af275957118295cSam Blitzstein            // to the first day of first visible month.
490cb3f2522609186db6239ad154af275957118295cSam Blitzstein            if (firstVisibleView != null && firstVisibleView.getTop() >= -1) {
491cb3f2522609186db6239ad154af275957118295cSam Blitzstein                // There's an off-by-one somewhere, so the top of the first visible item will
492cb3f2522609186db6239ad154af275957118295cSam Blitzstein                // actually be -1 when it's at the exact top.
493cb3f2522609186db6239ad154af275957118295cSam Blitzstein                day.month--;
494cb3f2522609186db6239ad154af275957118295cSam Blitzstein                if (day.month == -1) {
495cb3f2522609186db6239ad154af275957118295cSam Blitzstein                    day.month = 11;
496cb3f2522609186db6239ad154af275957118295cSam Blitzstein                    day.year--;
497cb3f2522609186db6239ad154af275957118295cSam Blitzstein                }
498cb3f2522609186db6239ad154af275957118295cSam Blitzstein            }
499cb3f2522609186db6239ad154af275957118295cSam Blitzstein        }
500cb3f2522609186db6239ad154af275957118295cSam Blitzstein
501cb3f2522609186db6239ad154af275957118295cSam Blitzstein        // Go to that month.
502cb3f2522609186db6239ad154af275957118295cSam Blitzstein        Utils.tryAccessibilityAnnounce(this, getMonthAndYearString(day));
503cb3f2522609186db6239ad154af275957118295cSam Blitzstein        goTo(day, true, false, true);
504cb3f2522609186db6239ad154af275957118295cSam Blitzstein        mPerformingScroll = true;
505cb3f2522609186db6239ad154af275957118295cSam Blitzstein        return true;
506090a46d6ee500d3674401fd3de48cd0f12ce7186Alan Viverette    }
5073e9818e0267619fecebd55095ab26c53eff92e93James Kung}
508