package org.gingercal; import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Map; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; /** * Month grid drawn by hand (Android 2.3 has no calendar widget). Six weeks of days, each with * up to four coloured marks for its events. Tap selects a day, long-press asks to create an * event on it, a sideways swipe changes month. */ public class MonthView extends View { public interface Listener { void daySelected(long day); void dayLongPressed(long day); void monthSwiped(int delta); } static final int BG = 0xFF101418, GRID = 0xFF2A3036, WEEKDAY = 0xFF90A4AE, NUM = 0xFFECEFF1, OTHER = 0xFF5C666E, TODAY = 0xFF3D8EF0, SELECTED = 0xFF2E3B44; final float density; final Paint fill = new Paint(1 /* ANTI_ALIAS_FLAG */), text = new Paint(1), line = new Paint(); final GestureDetector gestures; final String[] weekdays = new String[7]; final long[] days = new long[42]; Listener listener; int month = -1; // Calendar.MONTH of the displayed month long selected = -1, today; Map> marks = new HashMap>(); public MonthView(Context c) { super(c); density = c.getResources().getDisplayMetrics().density; line.setColor(GRID); String[] shortNames = L.symbols().getShortWeekdays(); // index 1 = Sunday int first = L.firstDayOfWeek(); for (int i = 0; i < 7; i++) { String n = shortNames[(first - 1 + i) % 7 + 1]; weekdays[i] = n.length() > 3 ? n.substring(0, 3) : n; } gestures = new GestureDetector(c, new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { return true; } @Override public boolean onSingleTapUp(MotionEvent e) { long d = dayAt(e.getX(), e.getY()); if (d >= 0 && listener != null) listener.daySelected(d); return true; } @Override public void onLongPress(MotionEvent e) { long d = dayAt(e.getX(), e.getY()); if (d >= 0 && listener != null) { performHapticFeedback(0 /* HapticFeedbackConstants.LONG_PRESS */); listener.dayLongPressed(d); } } @Override public boolean onFling(MotionEvent a, MotionEvent b, float vx, float vy) { if (listener != null && Math.abs(vx) > Math.abs(vy) * 1.5f && Math.abs(vx) > 250 * density) { listener.monthSwiped(vx < 0 ? 1 : -1); return true; } return false; } }); } /** Shows the month containing `anyDay`, weeks starting on the locale's first weekday. */ public void setMonth(long anyDay) { Calendar c = Calendar.getInstance(); c.setTimeInMillis(ICal.startOfDay(anyDay)); c.set(Calendar.DAY_OF_MONTH, 1); month = c.get(Calendar.MONTH); int back = (c.get(Calendar.DAY_OF_WEEK) - L.firstDayOfWeek() + 7) % 7; c.add(Calendar.DAY_OF_MONTH, -back); for (int i = 0; i < 42; i++) { days[i] = c.getTimeInMillis(); c.add(Calendar.DAY_OF_MONTH, 1); } today = ICal.startOfDay(System.currentTimeMillis()); invalidate(); } public long gridStart() { return days[0]; } public long gridEnd() { return ICal.addDays(days[41], 1); } public boolean inMonth(long day) { Calendar c = Calendar.getInstance(); c.setTimeInMillis(day); return c.get(Calendar.MONTH) == month; } public void setSelected(long day) { selected = day; invalidate(); } public void setMarks(Map> m) { marks = m; invalidate(); } // -------------------------------------------------------------- layout float headerH() { return 16 * density; } @Override protected void onMeasure(int wSpec, int hSpec) { int w = MeasureSpec.getSize(wSpec); int h; if (MeasureSpec.getMode(hSpec) == MeasureSpec.EXACTLY) { h = MeasureSpec.getSize(hSpec); } else { float cell = Math.max(22 * density, Math.min(w / 7f * 0.66f, 44 * density)); h = (int) (headerH() + 6 * cell); if (MeasureSpec.getMode(hSpec) == MeasureSpec.AT_MOST) h = Math.min(h, MeasureSpec.getSize(hSpec)); } setMeasuredDimension(w, h); } long dayAt(float x, float y) { float cw = getWidth() / 7f, ch = (getHeight() - headerH()) / 6f; if (y < headerH() || cw <= 0 || ch <= 0) return -1; int col = Math.min(6, Math.max(0, (int) (x / cw))); int row = Math.min(5, Math.max(0, (int) ((y - headerH()) / ch))); return days[row * 7 + col]; } @Override public boolean onTouchEvent(MotionEvent e) { return gestures.onTouchEvent(e); } // ------------------------------------------------------------- drawing @Override protected void onDraw(Canvas canvas) { if (month < 0) return; float w = getWidth(), cw = w / 7f, top = headerH(), ch = (getHeight() - top) / 6f; fill.setStyle(Paint.Style.FILL); fill.setColor(BG); canvas.drawRect(0, 0, w, getHeight(), fill); text.setTextAlign(Paint.Align.CENTER); text.setFakeBoldText(false); text.setTextSize(11 * density); text.setColor(WEEKDAY); for (int i = 0; i < 7; i++) canvas.drawText(weekdays[i], cw * i + cw / 2, top - 4 * density, text); float numSize = Math.min(14 * density, ch * 0.45f); float mark = Math.max(2, 3 * density); for (int i = 0; i < 42; i++) { float x0 = cw * (i % 7), y0 = top + ch * (i / 7); long d = days[i]; if (d == selected) { fill.setColor(SELECTED); canvas.drawRect(x0, y0, x0 + cw, y0 + ch, fill); } boolean isToday = d == today, here = inMonth(d); Calendar c = Calendar.getInstance(); c.setTimeInMillis(d); String num = String.valueOf(c.get(Calendar.DAY_OF_MONTH)); text.setTextSize(numSize); text.setFakeBoldText(isToday); text.setColor(isToday ? TODAY : here ? NUM : OTHER); float ty = y0 + ch * 0.12f - text.ascent(); canvas.drawText(num, x0 + cw / 2, ty, text); if (isToday) { fill.setStyle(Paint.Style.STROKE); fill.setStrokeWidth(Math.max(1, 1.5f * density)); fill.setColor(TODAY); canvas.drawRect(x0 + 1, y0 + 1, x0 + cw - 1, y0 + ch - 1, fill); fill.setStyle(Paint.Style.FILL); } List m = marks.get(Long.valueOf(d)); if (m != null && !m.isEmpty()) { int n = Math.min(4, m.size()); float gap = 2 * density, mw = Math.min(6 * density, (cw - gap * 5) / 4); float total = n * mw + (n - 1) * gap, mx = x0 + (cw - total) / 2; float my = Math.min(y0 + ch - mark - 3 * density, ty + text.descent() + 2 * density); for (int k = 0; k < n; k++) { fill.setColor(here ? m.get(k).intValue() : (m.get(k).intValue() & 0x00FFFFFF) | 0x77000000); canvas.drawRect(mx, my, mx + mw, my + mark, fill); mx += mw + gap; } } } for (int r = 0; r <= 6; r++) canvas.drawLine(0, top + ch * r, w, top + ch * r, line); } }