Skip to content

Instantly share code, notes, and snippets.

@vijaysharm
Last active January 21, 2016 23:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vijaysharm/cb88d520d0c9ca6414de to your computer and use it in GitHub Desktop.
Save vijaysharm/cb88d520d0c9ca6414de to your computer and use it in GitHub Desktop.
A Calendar View pager
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical">
<TextView
android:id="@+id/day_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Sun"
android:textSize="12sp"
android:layout_marginBottom="4dp"
/>
<FrameLayout
android:id="@+id/day_value_container"
android:layout_gravity="center"
android:layout_width="28dp"
android:layout_height="28dp">
<TextView
android:id="@+id/day_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="18sp"
tools:text="1"
/>
</FrameLayout>
</LinearLayout>
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
<com.younility.dispatch.WeekPager
android:id="@+id/week_pager"
android:layout_width="match_parent"
android:layout_height="55dp"/>
</android.support.design.widget.AppBarLayout>
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.support.annotation.ColorRes;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.touchfleet.dispatch.R;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import static android.support.v4.view.ViewPager.LayoutParams.MATCH_PARENT;
import static android.support.v4.view.ViewPager.LayoutParams.WRAP_CONTENT;
import static java.util.Calendar.DAY_OF_MONTH;
import static java.util.Calendar.HOUR_OF_DAY;
import static java.util.Calendar.MILLISECOND;
import static java.util.Calendar.MINUTE;
import static java.util.Calendar.MONTH;
import static java.util.Calendar.SECOND;
import static java.util.Calendar.WEEK_OF_YEAR;
import static java.util.Calendar.YEAR;
public class WeekPager extends ViewPager implements View.OnClickListener {
private final WeekAdapter adapter;
private final List<WeekDescriptor> weeks;
private final TypedValue mTypedValue = new TypedValue();
private Locale locale;
private Calendar minCal;
private Calendar maxCal;
private Calendar today;
private Calendar selected;
private Callback callback;
public WeekPager(Context context, AttributeSet attrs) {
super(context, attrs);
this.adapter = new WeekAdapter();
this.weeks = new ArrayList<>();
this.locale = Locale.getDefault();
this.today = Calendar.getInstance(this.locale);
this.minCal = Calendar.getInstance(this.locale);
this.maxCal = Calendar.getInstance(this.locale);
this.selected = Calendar.getInstance(this.locale);
context.getTheme().resolveAttribute(R.attr.selectableItemBackground, mTypedValue, true);
if (isInEditMode()) {
Calendar min = Calendar.getInstance(locale);
min.add(Calendar.MONTH, -1);
Calendar max = Calendar.getInstance(locale);
max.add(Calendar.YEAR, 1);
init(min.getTime(), max.getTime(), locale);
}
}
public FluentInitializer init(Date minDate, Date maxDate) {
return init(minDate, maxDate, Locale.getDefault());
}
public FluentInitializer init(Date minDate, Date maxDate, Locale locale) {
if (minDate == null || maxDate == null) {
throw new IllegalArgumentException(
"minDate and maxDate must be non-null. " + dbg(minDate, maxDate));
}
if (minDate.after(maxDate)) {
throw new IllegalArgumentException(
"minDate must be before maxDate. " + dbg(minDate, maxDate));
}
if (locale == null) {
throw new IllegalArgumentException("Locale is null.");
}
this.weeks.clear();
this.locale = locale;
this.today = Calendar.getInstance(this.locale);
this.minCal = Calendar.getInstance(this.locale);
this.maxCal = Calendar.getInstance(this.locale);
this.minCal.setTime(minDate);
this.maxCal.setTime(maxDate);
setMidnight(minCal);
setMidnight(maxCal);
// maxDate is exclusive: bump back to the previous day so if maxDate is the first of a month,
// we don't accidentally include that month in the view.
maxCal.add(MINUTE, -1);
return new FluentInitializer();
}
public void setSelected(Date date) {
selected.setTime(date);
validateAndUpdate();
int page = findPage(selected);
if (page != -1) {
setCurrentItem(page, true);
}
}
private int findPage(Calendar date) {
int week = date.get(WEEK_OF_YEAR);
int year = date.get(YEAR);
for (int index = 0; index < weeks.size(); index++) {
WeekDescriptor descriptor = weeks.get(index);
if (week == descriptor.week && year == descriptor.year)
return index;
}
return -1;
}
@Override
public void onClick(View v) {
Date date = (Date) v.getTag();
selected.setTime(date);
validateAndUpdate();
if (callback != null) {
callback.didSelect(date);
}
}
private List<WeekDayDescriptor> getWeekDayDescriptors(Calendar start) {
ArrayList<WeekDayDescriptor> descriptors = new ArrayList<>(7);
Calendar cal = Calendar.getInstance(locale);
cal.setTime(start.getTime());
cal.set(Calendar.DAY_OF_WEEK, 1);
for (int c = 0; c < 7; c++) {
boolean isToday = sameDate(cal, today);
int day = cal.get(Calendar.DAY_OF_MONTH);
String displayName = cal.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, locale);
descriptors.add(new WeekDayDescriptor(
day, displayName, isToday, cal.getTime()
));
cal.add(Calendar.DAY_OF_WEEK, 1);
}
return descriptors;
}
private void validateAndUpdate() {
if (getAdapter() == null) {
setAdapter(adapter);
}
adapter.notifyDataSetChanged();
}
private static boolean sameDate(Calendar cal, Calendar selectedDate) {
return cal.get(MONTH) == selectedDate.get(MONTH)
&& cal.get(YEAR) == selectedDate.get(YEAR)
&& cal.get(DAY_OF_MONTH) == selectedDate.get(DAY_OF_MONTH);
}
private static void setMidnight(Calendar cal) {
cal.set(HOUR_OF_DAY, 0);
cal.set(MINUTE, 0);
cal.set(SECOND, 0);
cal.set(MILLISECOND, 0);
}
private static Drawable borderlessDrawable(Context context, @ColorRes int primaryColor) {
int color = context.getResources().getColor(primaryColor);
int colors[] = {color, color};
GradientDrawable shape = new GradientDrawable();
shape.setShape(GradientDrawable.OVAL);
shape.setColors(colors);
shape.setSize(24, 24);
return shape;
}
/** Returns a string summarizing what the client sent us for init() params. */
private static String dbg(Date minDate, Date maxDate) {
return "minDate: " + minDate + "\nmaxDate: " + maxDate;
}
public interface Callback {
void didSelect(Date date);
}
public class FluentInitializer {
public FluentInitializer withSelectedDate(Date selectedDates) {
selected.setTime(selectedDates);
return this;
}
public FluentInitializer withListener(Callback listener) {
callback = listener;
return this;
}
public void build() {
Calendar weekCounter = Calendar.getInstance(locale);
weekCounter.setTime(minCal.getTime());
final int maxWeek = maxCal.get(WEEK_OF_YEAR);
final int maxYear = maxCal.get(YEAR);
while ((weekCounter.get(WEEK_OF_YEAR) <= maxWeek // Up to, including the week.
|| weekCounter.get(YEAR) < maxYear) // Up to the year.
&& weekCounter.get(YEAR) < maxYear + 1) { // But not > next yr.
int year = weekCounter.get(YEAR);
int week = weekCounter.get(WEEK_OF_YEAR);
Date date = weekCounter.getTime();
final List<WeekDayDescriptor> days = getWeekDayDescriptors(weekCounter);
weeks.add(new WeekDescriptor(
year, week, date, days
));
weekCounter.add(Calendar.WEEK_OF_YEAR, 1);
}
final int page = findPage(selected);
if (page != -1) setCurrentItem(page);
validateAndUpdate();
}
}
private class WeekAdapter extends PagerAdapter {
@Override
public Object instantiateItem(ViewGroup container, int position) {
Context context = getContext();
LayoutInflater inflater = LayoutInflater.from(context);
LinearLayout group = new LinearLayout(context);
group.setOrientation(LinearLayout.HORIZONTAL);
group.setLayoutParams(new LinearLayout.LayoutParams(
MATCH_PARENT, WRAP_CONTENT
));
final WeekDescriptor week = weeks.get(position);
for (WeekDayDescriptor day : week.days) {
Calendar date = Calendar.getInstance(locale);
date.setTime(day.date);
View view = inflater.inflate(R.layout.item_day, group, false);
TextView title = (TextView) view.findViewById(R.id.day_title);
title.setText(day.label);
TextView value = (TextView) view.findViewById(R.id.day_value);
String display = String.format("%d", day.day);
value.setText(display);
View valueContainer = view.findViewById(R.id.day_value_container);
boolean isToday = sameDate(today, date);
boolean isSelected = sameDate(selected, date);
if (isToday && isSelected) {
value.setTextColor(getContext().getResources().getColor(R.color.primary));
valueContainer.setBackground(borderlessDrawable(getContext(), R.color.textPrimary));
} else if (isToday) {
value.setTextColor(getContext().getResources().getColor(R.color.accent));
valueContainer.setBackgroundResource(android.R.color.transparent);
} else if (isSelected) {
value.setTextColor(getContext().getResources().getColor(R.color.primary));
valueContainer.setBackground(borderlessDrawable(getContext(), R.color.accent));
} else {
value.setTextColor(getContext().getResources().getColor(R.color.textPrimary));
valueContainer.setBackgroundResource(android.R.color.transparent);
}
view.setOnClickListener(WeekPager.this);
view.setTag(day.date);
view.setBackgroundResource(mTypedValue.resourceId);
view.setLayoutParams(new LinearLayout.LayoutParams(
0, LayoutParams.WRAP_CONTENT, 1f
));
group.addView(view);
}
container.addView(group);
return group;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
@Override
public int getItemPosition(Object object) {
return PagerAdapter.POSITION_NONE;
}
@Override
public int getCount() {
return weeks.size();
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
}
private static final class WeekDayDescriptor {
public final int day;
public final String label;
public final boolean isToday;
public final Date date;
public WeekDayDescriptor(int day, String label, boolean isToday, Date date) {
this.day = day;
this.label = label;
this.isToday = isToday;
this.date = date;
}
}
private static final class WeekDescriptor {
public final int year;
public final int week;
public final Date date;
public final List<WeekDayDescriptor> days;
public WeekDescriptor(int year, int week, Date date, List<WeekDayDescriptor> days) {
this.year = year;
this.week = week;
this.date = date;
this.days = days;
}
@Override
public String toString() {
return "WeekDescriptor {" +
"year=" + year +
", week=" + week +
", date=" + date +
'}';
}
}
}
@vijaysharm
Copy link
Author

This is a reproduction of the iOS8 calendar date picker widget. It uses a similar API to the Square Android TimeSquare API.

        Locale locale = Locale.getDefault();
        Calendar min = Calendar.getInstance(locale);
        min.add(Calendar.MONTH, -3);

        Calendar max = Calendar.getInstance(locale);
        max.add(Calendar.YEAR, 1);
        updateTitle(activeDate);
        weekPager
            .init(min.getTime(), max.getTime(), locale)
            .withSelectedDate(activeDate)
            .withListener(this)
            .build();

@vijaysharm
Copy link
Author

I had to hard-code the height of the Pager because when the ViewPager's layout_height is set to wrap_content, it ends up taking up the whole screen.

@vijaysharm
Copy link
Author

I find setting the title of the activity useful. I use a method similar to this

    private void updateTitle(Date date) {
        Locale locale = Locale.getDefault();
        Calendar cal = Calendar.getInstance(locale);
        int year = cal.get(Calendar.YEAR);

        cal.setTime(date);
        int titleYear = cal.get(Calendar.YEAR);

        if (titleYear != year) {
            final String shortMonth = cal.getDisplayName(Calendar.MONTH, Calendar.SHORT, locale);
            toolbar.setTitle(shortMonth + " " + titleYear);
        } else {
            final String longMonth = cal.getDisplayName(Calendar.MONTH, Calendar.LONG, locale);
            toolbar.setTitle(longMonth);
        }
    }

@vijaysharm
Copy link
Author

TODO: I still need to add support for selecting the first day of the month when you swipe pages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment