package com.novachevskyi.datepicker.base.views;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ListView;
import com.novachevskyi.datepicker.CalendarDatePickerDialog;
import com.novachevskyi.datepicker.base.CalendarDatePickerController;
import com.novachevskyi.datepicker.base.adapters.MonthAdapter;
import com.novachevskyi.datepicker.base.adapters.SimpleMonthAdapter;
import com.novachevskyi.datepicker.utils.Utils;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
public abstract class DayPickerView extends ListView implements OnScrollListener,
CalendarDatePickerDialog.OnDateChangedListener {
private static final String TAG = "MonthFragment";
protected static final int GOTO_SCROLL_DURATION = 250;
protected static final int SCROLL_CHANGE_DELAY = 40;
public static final int LIST_TOP_OFFSET = -1;
private static final SimpleDateFormat YEAR_FORMAT =
new SimpleDateFormat("yyyy", Locale.getDefault());
protected float mFriction = 1.0f;
protected Context mContext;
protected Handler mHandler;
protected MonthAdapter.CalendarDay mSelectedDay = new MonthAdapter.CalendarDay();
protected MonthAdapter mAdapter;
protected MonthAdapter.CalendarDay mTempDay = new MonthAdapter.CalendarDay();
protected int mCurrentMonthDisplayed;
protected long mPreviousScrollPosition;
protected int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE;
protected int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE;
private CalendarDatePickerController mController;
private boolean mPerformingScroll;
public DayPickerView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public DayPickerView(Context context, CalendarDatePickerController controller) {
super(context);
init(context);
setController(controller);
}
public void setController(CalendarDatePickerController controller) {
mController = controller;
mController.registerOnDateChangedListener(this);
refreshAdapter();
onDateChanged();
}
public void init(Context context) {
mHandler = new Handler();
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
setDrawSelectorOnTop(false);
mContext = context;
setUpListView();
}
public void onChange() {
refreshAdapter();
}
protected void refreshAdapter() {
if (mAdapter == null) {
mAdapter = createMonthAdapter(getContext(), mController);
} else {
mAdapter.setSelectedDay(mSelectedDay);
}
setAdapter(mAdapter);
}
public abstract MonthAdapter createMonthAdapter(Context context,
CalendarDatePickerController controller);
@SuppressLint("NewApi")
protected void setUpListView() {
setCacheColorHint(0);
setDivider(null);
setItemsCanFocus(true);
setFastScrollEnabled(false);
setVerticalScrollBarEnabled(false);
setOnScrollListener(this);
setFadingEdgeLength(0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
setFriction(ViewConfiguration.getScrollFriction() * mFriction);
}
}
public boolean goTo(MonthAdapter.CalendarDay day, boolean animate, boolean setSelected,
boolean forceScroll) {
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;
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);
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);
}
if (position != selectedPosition || forceScroll) {
setMonthDisplayed(mTempDay);
mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING;
if (animate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
smoothScrollToPositionFromTop(
position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION);
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);
}
@Override
public void onScroll(
AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
MonthView child = (MonthView) view.getChildAt(0);
if (child == null) {
return;
}
mPreviousScrollPosition =
(long) (view.getFirstVisiblePosition() * child.getHeight() - child.getBottom());
mPreviousScrollState = mCurrentScrollState;
}
protected void setMonthDisplayed(MonthAdapter.CalendarDay date) {
mCurrentMonthDisplayed = date.month;
invalidateViews();
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
mScrollStateChangedRunnable.doScrollStateChange(scrollState);
}
protected ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable();
protected class ScrollStateRunnable implements Runnable {
private int mNewState;
public void doScrollStateChange(int scrollState) {
mHandler.removeCallbacks(this);
mNewState = scrollState;
mHandler.postDelayed(this, SCROLL_CHANGE_DELAY);
}
@Override
public void run() {
mCurrentScrollState = mNewState;
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG,
"new scroll state: " + mNewState + " old state: " + mPreviousScrollState);
}
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) {
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 (bottom > midpoint) {
smoothScrollBy(top, GOTO_SCROLL_DURATION);
} else {
smoothScrollBy(bottom, GOTO_SCROLL_DURATION);
}
}
} else {
mPreviousScrollState = mNewState;
}
}
}
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);
}
private MonthAdapter.CalendarDay findAccessibilityFocus() {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child instanceof MonthView) {
final MonthAdapter.CalendarDay focus = ((MonthView) child).getAccessibilityFocus();
if (focus != null) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) {
((MonthView) child).clearAccessibilityFocus();
}
return focus;
}
}
}
return null;
}
private boolean restoreAccessibilityFocus(MonthAdapter.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 MonthView) {
if (((MonthView) child).restoreAccessibilityFocus(day)) {
return true;
}
}
}
return false;
}
@Override
protected void layoutChildren() {
final MonthAdapter.CalendarDay focusedDay = findAccessibilityFocus();
super.layoutChildren();
if (mPerformingScroll) {
mPerformingScroll = false;
} else {
restoreAccessibilityFocus(focusedDay);
}
}
@Override
public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
event.setItemCount(-1);
}
private String getMonthAndYearString(MonthAdapter.CalendarDay day) {
Calendar cal = Calendar.getInstance();
cal.set(day.year, day.month, day.day);
return cal.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault())
+ " "
+ YEAR_FORMAT.format(cal.getTime());
}
@Override
public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
}
@SuppressLint("NewApi")
@Override
public boolean performAccessibilityAction(int action, Bundle arguments) {
if (action != AccessibilityNodeInfo.ACTION_SCROLL_FORWARD &&
action != AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
return super.performAccessibilityAction(action, arguments);
}
int firstVisiblePosition = getFirstVisiblePosition();
int month = firstVisiblePosition % 12;
int year = firstVisiblePosition / 12 + mController.getMinYear();
MonthAdapter.CalendarDay day = new MonthAdapter.CalendarDay(year, month, 1);
if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
day.month++;
if (day.month == 12) {
day.month = 0;
day.year++;
}
} else {
View firstVisibleView = getChildAt(0);
if (firstVisibleView != null && firstVisibleView.getTop() >= -1) {
day.month--;
if (day.month == -1) {
day.month = 11;
day.year--;
}
}
}
Utils.tryAccessibilityAnnounce(this,
getMonthAndYearString(day));
goTo(day, true, false, true);
mPerformingScroll = true;
return true;
}
}