com.redinput.datetimepickercompat.date.DayPickerView.java Source code

Java tutorial

Introduction

Here is the source code for com.redinput.datetimepickercompat.date.DayPickerView.java

Source

/*
 * Copyright 2013 RedInput
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

package com.redinput.datetimepickercompat.date;

import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.util.Log;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ListView;

import com.redinput.datetimepickercompat.Utils;
import com.redinput.datetimepickercompat.date.DatePickerDialog.OnDateChangedListener;
import com.redinput.datetimepickercompat.date.SimpleMonthAdapter.CalendarDay;

/**
 * This displays a list of months in a calendar format with selectable days.
 */
public class DayPickerView extends ListView implements OnScrollListener, OnDateChangedListener {

    private static final String TAG = "MonthFragment";

    // Affects when the month selection will change while scrolling up
    protected static final int SCROLL_HYST_WEEKS = 2;
    // How long the GoTo fling animation should last
    protected static final int GOTO_SCROLL_DURATION = 250;
    // How long to wait after receiving an onScrollStateChanged notification
    // before acting on it
    protected static final int SCROLL_CHANGE_DELAY = 40;
    // The number of days to display in each week
    public static final int DAYS_PER_WEEK = 7;
    public static int LIST_TOP_OFFSET = -1; // so that the top line will be
    // under the separator
    // You can override these numbers to get a different appearance
    protected int mNumWeeks = 6;
    protected boolean mShowWeekNumber = false;
    protected int mDaysPerWeek = 7;
    private static SimpleDateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy", Locale.getDefault());

    // These affect the scroll speed and feel
    protected float mFriction = 1.0f;

    protected Context mContext;
    protected Handler mHandler;

    // highlighted time
    protected CalendarDay mSelectedDay = new CalendarDay();
    protected SimpleMonthAdapter mAdapter;

    protected CalendarDay mTempDay = new CalendarDay();

    private static float mScale = 0;
    // When the week starts; numbered like Time.<WEEKDAY> (e.g. SUNDAY=0).
    protected int mFirstDayOfWeek;
    // The last name announced by accessibility
    protected CharSequence mPrevMonthName;
    // which month should be displayed/highlighted [0-11]
    protected int mCurrentMonthDisplayed;
    // used for tracking during a scroll
    protected long mPreviousScrollPosition;
    // used for tracking what state listview is in
    protected int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE;
    // used for tracking what state listview is in
    protected int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE;

    private final DatePickerController mController;
    private boolean mPerformingScroll;

    public DayPickerView(Context context, DatePickerController controller) {
        super(context);
        mHandler = new Handler();
        mController = controller;
        mController.registerOnDateChangedListener(this);
        setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        setDrawSelectorOnTop(false);
        init(context);
        onDateChanged();
        installAccessibilityDelegate();
    }

    public void init(Context context) {
        mContext = context;
        setUpListView();
        setUpAdapter();
        setAdapter(mAdapter);
    }

    public void onChange() {
        setUpAdapter();
        setAdapter(mAdapter);
    }

    /**
     * Creates a new adapter if necessary and sets up its parameters. Override
     * this method to provide a custom adapter.
     */
    protected void setUpAdapter() {
        if (mAdapter == null) {
            mAdapter = new SimpleMonthAdapter(getContext(), mController);
        } else {
            mAdapter.setSelectedDay(mSelectedDay);
            mAdapter.notifyDataSetChanged();
        }
        // refresh the view with the new parameters
        mAdapter.notifyDataSetChanged();
    }

    /*
     * Sets all the required fields for the list view. Override this method to
     * set a different list view behavior.
     */
    @SuppressLint("NewApi")
    protected void setUpListView() {
        // Transparent background on scroll
        setCacheColorHint(0);
        // No dividers
        setDivider(null);
        // Items are clickable
        setItemsCanFocus(true);
        // The thumb gets in the way, so disable it
        setFastScrollEnabled(false);
        setVerticalScrollBarEnabled(false);
        setOnScrollListener(this);
        setFadingEdgeLength(0);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // Make the scrolling behavior nicer
            setFriction(ViewConfiguration.getScrollFriction() * mFriction);
        }
    }

    /**
     * This moves to the specified time in the view. If the time is not already
     * in range it will move the list so that the first of the month containing
     * the time is at the top of the view. If the new time is already in view
     * the list will not be scrolled unless forceScroll is true. This time may
     * optionally be highlighted as selected as well.
     * 
     * @param time
     *            The time to move to
     * @param animate
     *            Whether to scroll to the given time or just redraw at the
     *            new location
     * @param setSelected
     *            Whether to set the given time as selected
     * @param forceScroll
     *            Whether to recenter even if the time is already
     *            visible
     * @return Whether or not the view animated to the new location
     */
    @SuppressLint("NewApi")
    public boolean goTo(CalendarDay day, boolean animate, boolean setSelected, boolean forceScroll) {

        // Set the selected day
        if (setSelected) {
            mSelectedDay.set(day);
        }

        mTempDay.set(day);
        final int position = (day.year - mController.getMinYear()) * SimpleMonthAdapter.MONTHS_IN_YEAR + day.month;

        View child;
        int i = 0;
        int top = 0;
        // Find a child that's completely in the view
        do {
            child = getChildAt(i++);
            if (child == null) {
                break;
            }
            top = child.getTop();
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "child at " + (i - 1) + " has top " + top);
            }
        } while (top < 0);

        // Compute the first and last position visible
        int selectedPosition;
        if (child != null) {
            selectedPosition = getPositionForView(child);
        } else {
            selectedPosition = 0;
        }

        if (setSelected) {
            mAdapter.setSelectedDay(mSelectedDay);
        }

        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "GoTo position " + position);
        }
        // Check if the selected day is now outside of our visible range
        // and if so scroll to the month that contains it
        if (position != selectedPosition || forceScroll) {
            setMonthDisplayed(mTempDay);
            mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING;
            if (animate) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                    smoothScrollToPositionFromTop(position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION);
                } else {
                    postSetSelection(position);
                }
                return true;
            } else {
                postSetSelection(position);
            }
        } else if (setSelected) {
            setMonthDisplayed(mSelectedDay);
        }
        return false;
    }

    public void postSetSelection(final int position) {
        clearFocus();
        post(new Runnable() {

            @Override
            public void run() {
                setSelection(position);
            }
        });
        onScrollStateChanged(this, OnScrollListener.SCROLL_STATE_IDLE);
    }

    /**
     * Updates the title and selected month if the view has moved to a new
     * month.
     */
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        SimpleMonthView child = (SimpleMonthView) view.getChildAt(0);
        if (child == null) {
            return;
        }

        // Figure out where we are
        long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom();
        mPreviousScrollPosition = currScroll;
        mPreviousScrollState = mCurrentScrollState;
    }

    /**
     * Sets the month displayed at the top of this view based on time. Override
     * to add custom events when the title is changed.
     */
    protected void setMonthDisplayed(CalendarDay date) {
        mCurrentMonthDisplayed = date.month;
        invalidateViews();
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        // use a post to prevent re-entering onScrollStateChanged before it
        // exits
        mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
    }

    protected ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable();

    protected class ScrollStateRunnable implements Runnable {
        private int mNewState;

        /**
         * Sets up the runnable with a short delay in case the scroll state
         * immediately changes again.
         * 
         * @param view
         *            The list view that changed state
         * @param scrollState
         *            The new state it changed to
         */
        public void doScrollStateChange(AbsListView view, int scrollState) {
            mHandler.removeCallbacks(this);
            mNewState = scrollState;
            mHandler.postDelayed(this, SCROLL_CHANGE_DELAY);
        }

        @SuppressLint("NewApi")
        @Override
        public void run() {
            mCurrentScrollState = mNewState;
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "new scroll state: " + mNewState + " old state: " + mPreviousScrollState);
            }
            // Fix the position after a scroll or a fling ends
            if (mNewState == OnScrollListener.SCROLL_STATE_IDLE
                    && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE
                    && mPreviousScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
                mPreviousScrollState = mNewState;
                int i = 0;
                View child = getChildAt(i);
                while (child != null && child.getBottom() <= 0) {
                    child = getChildAt(++i);
                }
                if (child == null) {
                    // The view is no longer visible, just return
                    return;
                }
                int firstPosition = getFirstVisiblePosition();
                int lastPosition = getLastVisiblePosition();
                boolean scroll = firstPosition != 0 && lastPosition != getCount() - 1;
                final int top = child.getTop();
                final int bottom = child.getBottom();
                final int midpoint = getHeight() / 2;
                if (scroll && top < LIST_TOP_OFFSET) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
                        if (bottom > midpoint) {
                            smoothScrollBy(top, GOTO_SCROLL_DURATION);
                        } else {
                            smoothScrollBy(bottom, GOTO_SCROLL_DURATION);
                        }
                    } else {
                        if (bottom > midpoint) {
                            scrollBy(top, GOTO_SCROLL_DURATION);
                        } else {
                            scrollBy(bottom, GOTO_SCROLL_DURATION);
                        }
                    }
                }
            } else {
                mPreviousScrollState = mNewState;
            }
        }
    }

    /**
     * Gets the position of the view that is most prominently displayed within the list view.
     */
    public int getMostVisiblePosition() {
        final int firstPosition = getFirstVisiblePosition();
        final int height = getHeight();

        int maxDisplayedHeight = 0;
        int mostVisibleIndex = 0;
        int i = 0;
        int bottom = 0;
        while (bottom < height) {
            View child = getChildAt(i);
            if (child == null) {
                break;
            }
            bottom = child.getBottom();
            int displayedHeight = Math.min(bottom, height) - Math.max(0, child.getTop());
            if (displayedHeight > maxDisplayedHeight) {
                mostVisibleIndex = i;
                maxDisplayedHeight = displayedHeight;
            }
            i++;
        }
        return firstPosition + mostVisibleIndex;
    }

    @Override
    public void onDateChanged() {
        goTo(mController.getSelectedDay(), false, true, true);
    }

    /**
     * Attempts to return the date that has accessibility focus.
     * 
     * @return The date that has accessibility focus, or {@code null} if no date
     *         has focus.
     */
    private CalendarDay findAccessibilityFocus() {
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child instanceof SimpleMonthView) {
                final CalendarDay focus = ((SimpleMonthView) child).getAccessibilityFocus();
                if (focus != null) {
                    // Clear focus to avoid ListView bug in Jelly Bean MR1.
                    ((SimpleMonthView) child).clearAccessibilityFocus();
                    return focus;
                }
            }
        }

        return null;
    }

    /**
     * Attempts to restore accessibility focus to a given date. No-op if {@code day} is {@code null}
     * .
     * 
     * @param day
     *            The date that should receive accessibility focus
     * @return {@code true} if focus was restored
     */
    private boolean restoreAccessibilityFocus(CalendarDay day) {
        if (day == null) {
            return false;
        }

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child instanceof SimpleMonthView) {
                if (((SimpleMonthView) child).restoreAccessibilityFocus(day)) {
                    return true;
                }
            }
        }

        return false;
    }

    @Override
    protected void layoutChildren() {
        final CalendarDay focusedDay = findAccessibilityFocus();
        super.layoutChildren();
        if (mPerformingScroll) {
            mPerformingScroll = false;
        } else {
            restoreAccessibilityFocus(focusedDay);
        }
    }

    private void installAccessibilityDelegate() {
        // The accessibility delegate enables customizing accessibility behavior
        // via composition as opposed as inheritance. The main benefit is that
        // one can write a backwards compatible application by setting the delegate
        // only if the API level is high enough i.e. the delegate is part of the APIs.
        // The easiest way to achieve that is by using the support library which
        // takes the burden of checking API version and knowing which API version
        // introduced the delegate off the developer.
        ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegateCompat() {

            @Override
            public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
                super.onInitializeAccessibilityEvent(host, event);
                // Note that View.onInitializeAccessibilityNodeInfo was introduced in
                // ICS and we would like to tweak a bit the text that is reported to
                // accessibility services via the AccessibilityNodeInfo.
                event.setItemCount(-1);
            }

            @Override
            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
                super.onInitializeAccessibilityNodeInfo(host, info);
                // Note that View.onInitializeAccessibilityNodeInfo was introduced in
                // ICS and we would like to tweak a bit the text that is reported to
                // accessibility services via the AccessibilityNodeInfo.
                info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
                info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
            }

            @Override
            public boolean performAccessibilityAction(View host, int action, Bundle args) {
                if (action != AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD
                        && action != AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD) {
                    return super.performAccessibilityAction(host, action, args);
                }

                // Figure out what month is showing.
                int firstVisiblePosition = getFirstVisiblePosition();
                int month = firstVisiblePosition % 12;
                int year = firstVisiblePosition / 12 + mController.getMinYear();
                CalendarDay day = new CalendarDay(year, month, 1);

                // Scroll either forward or backward one month.
                if (action == AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD) {
                    day.month++;
                    if (day.month == 12) {
                        day.month = 0;
                        day.year++;
                    }
                } else if (action == AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD) {
                    View firstVisibleView = getChildAt(0);
                    // If the view is fully visible, jump one month back. Otherwise, we'll just jump
                    // to the first day of first visible month.
                    if (firstVisibleView != null && firstVisibleView.getTop() >= -1) {
                        // There's an off-by-one somewhere, so the top of the first visible item
                        // will
                        // actually be -1 when it's at the exact top.
                        day.month--;
                        if (day.month == -1) {
                            day.month = 11;
                            day.year--;
                        }
                    }
                }

                // Go to that month.
                Utils.tryAccessibilityAnnounce(host, getMonthAndYearString(day));
                goTo(day, true, false, true);
                mPerformingScroll = true;
                return true;
            }
        });
    }

    private String getMonthAndYearString(CalendarDay day) {
        Calendar cal = Calendar.getInstance();
        cal.set(day.year, day.month, day.day);

        StringBuffer sbuf = new StringBuffer();
        sbuf.append(new DateFormatSymbols(Locale.getDefault()).getWeekdays()[cal.get(Calendar.MONTH)]);
        sbuf.append(" ");
        sbuf.append(YEAR_FORMAT.format(cal.getTime()));
        return sbuf.toString();
    }
}