summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMahi Kolla <mahikolla@google.com>2021-07-12 18:57:04 +0000
committerMahi Kolla <mahikolla@google.com>2021-07-12 18:57:04 +0000
commit7adab399715fcad7ea185eb9e349c9a073d3a0f3 (patch)
treec92d5afb9679f75bdaca14aa009f20c1a8a06b6b
parent68ba903864a2480a99c80c4280da26dfef7cad23 (diff)
downloadCalendar-7adab399715fcad7ea185eb9e349c9a073d3a0f3.tar.gz
AOSP/Calendar - Add Kotlin copy of MonthWeekEventsView.java
Test: none, no functional change in this commit when merged with corresponding Kotlin conversion. Change-Id: I4d956b5f0ae5f983ac30d09e01b55ff3a75bd606
-rw-r--r--src/com/android/calendar/month/MonthWeekEventsView.kt1110
1 files changed, 1110 insertions, 0 deletions
diff --git a/src/com/android/calendar/month/MonthWeekEventsView.kt b/src/com/android/calendar/month/MonthWeekEventsView.kt
new file mode 100644
index 00000000..071daee3
--- /dev/null
+++ b/src/com/android/calendar/month/MonthWeekEventsView.kt
@@ -0,0 +1,1110 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.android.calendar.month;
+
+import com.android.calendar.Event;
+import com.android.calendar.R;
+import com.android.calendar.Utils;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.app.Service;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Paint.Style;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.provider.CalendarContract.Attendees;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+
+public class MonthWeekEventsView extends SimpleWeekView {
+
+ private static final String TAG = "MonthView";
+
+ private static final boolean DEBUG_LAYOUT = false;
+
+ public static final String VIEW_PARAMS_ORIENTATION = "orientation";
+ public static final String VIEW_PARAMS_ANIMATE_TODAY = "animate_today";
+
+ /* NOTE: these are not constants, and may be multiplied by a scale factor */
+ private static int TEXT_SIZE_MONTH_NUMBER = 32;
+ private static int TEXT_SIZE_EVENT = 12;
+ private static int TEXT_SIZE_EVENT_TITLE = 14;
+ private static int TEXT_SIZE_MORE_EVENTS = 12;
+ private static int TEXT_SIZE_MONTH_NAME = 14;
+ private static int TEXT_SIZE_WEEK_NUM = 12;
+
+ private static int DNA_MARGIN = 4;
+ private static int DNA_ALL_DAY_HEIGHT = 4;
+ private static int DNA_MIN_SEGMENT_HEIGHT = 4;
+ private static int DNA_WIDTH = 8;
+ private static int DNA_ALL_DAY_WIDTH = 32;
+ private static int DNA_SIDE_PADDING = 6;
+ private static int CONFLICT_COLOR = Color.BLACK;
+ private static int EVENT_TEXT_COLOR = Color.WHITE;
+
+ private static int DEFAULT_EDGE_SPACING = 0;
+ private static int SIDE_PADDING_MONTH_NUMBER = 4;
+ private static int TOP_PADDING_MONTH_NUMBER = 4;
+ private static int TOP_PADDING_WEEK_NUMBER = 4;
+ private static int SIDE_PADDING_WEEK_NUMBER = 20;
+ private static int DAY_SEPARATOR_OUTER_WIDTH = 0;
+ private static int DAY_SEPARATOR_INNER_WIDTH = 1;
+ private static int DAY_SEPARATOR_VERTICAL_LENGTH = 53;
+ private static int DAY_SEPARATOR_VERTICAL_LENGTH_PORTRAIT = 64;
+ private static int MIN_WEEK_WIDTH = 50;
+
+ private static int EVENT_X_OFFSET_LANDSCAPE = 38;
+ private static int EVENT_Y_OFFSET_LANDSCAPE = 8;
+ private static int EVENT_Y_OFFSET_PORTRAIT = 7;
+ private static int EVENT_SQUARE_WIDTH = 10;
+ private static int EVENT_SQUARE_BORDER = 2;
+ private static int EVENT_LINE_PADDING = 2;
+ private static int EVENT_RIGHT_PADDING = 4;
+ private static int EVENT_BOTTOM_PADDING = 3;
+
+ private static int TODAY_HIGHLIGHT_WIDTH = 2;
+
+ private static int SPACING_WEEK_NUMBER = 24;
+ private static boolean mInitialized = false;
+ private static boolean mShowDetailsInMonth;
+
+ protected Time mToday = new Time();
+ protected boolean mHasToday = false;
+ protected int mTodayIndex = -1;
+ protected int mOrientation = Configuration.ORIENTATION_LANDSCAPE;
+ protected List<ArrayList<Event>> mEvents = null;
+ protected ArrayList<Event> mUnsortedEvents = null;
+ HashMap<Integer, Utils.DNAStrand> mDna = null;
+ // This is for drawing the outlines around event chips and supports up to 10
+ // events being drawn on each day. The code will expand this if necessary.
+ protected FloatRef mEventOutlines = new FloatRef(10 * 4 * 4 * 7);
+
+
+
+ protected static StringBuilder mStringBuilder = new StringBuilder(50);
+ // TODO recreate formatter when locale changes
+ protected static Formatter mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
+
+ protected Paint mMonthNamePaint;
+ protected TextPaint mEventPaint;
+ protected TextPaint mSolidBackgroundEventPaint;
+ protected TextPaint mFramedEventPaint;
+ protected TextPaint mDeclinedEventPaint;
+ protected TextPaint mEventExtrasPaint;
+ protected TextPaint mEventDeclinedExtrasPaint;
+ protected Paint mWeekNumPaint;
+ protected Paint mDNAAllDayPaint;
+ protected Paint mDNATimePaint;
+ protected Paint mEventSquarePaint;
+
+
+ protected Drawable mTodayDrawable;
+
+ protected int mMonthNumHeight;
+ protected int mMonthNumAscentHeight;
+ protected int mEventHeight;
+ protected int mEventAscentHeight;
+ protected int mExtrasHeight;
+ protected int mExtrasAscentHeight;
+ protected int mExtrasDescent;
+ protected int mWeekNumAscentHeight;
+
+ protected int mMonthBGColor;
+ protected int mMonthBGOtherColor;
+ protected int mMonthBGTodayColor;
+ protected int mMonthNumColor;
+ protected int mMonthNumOtherColor;
+ protected int mMonthNumTodayColor;
+ protected int mMonthNameColor;
+ protected int mMonthNameOtherColor;
+ protected int mMonthEventColor;
+ protected int mMonthDeclinedEventColor;
+ protected int mMonthDeclinedExtrasColor;
+ protected int mMonthEventExtraColor;
+ protected int mMonthEventOtherColor;
+ protected int mMonthEventExtraOtherColor;
+ protected int mMonthWeekNumColor;
+ protected int mMonthBusyBitsBgColor;
+ protected int mMonthBusyBitsBusyTimeColor;
+ protected int mMonthBusyBitsConflictTimeColor;
+ private int mClickedDayIndex = -1;
+ private int mClickedDayColor;
+ private static final int mClickedAlpha = 128;
+
+ protected int mEventChipOutlineColor = 0xFFFFFFFF;
+ protected int mDaySeparatorInnerColor;
+ protected int mTodayAnimateColor;
+
+ private boolean mAnimateToday;
+ private int mAnimateTodayAlpha = 0;
+ private ObjectAnimator mTodayAnimator = null;
+
+ private final TodayAnimatorListener mAnimatorListener = new TodayAnimatorListener();
+
+ class TodayAnimatorListener extends AnimatorListenerAdapter {
+ private volatile Animator mAnimator = null;
+ private volatile boolean mFadingIn = false;
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ synchronized (this) {
+ if (mAnimator != animation) {
+ animation.removeAllListeners();
+ animation.cancel();
+ return;
+ }
+ if (mFadingIn) {
+ if (mTodayAnimator != null) {
+ mTodayAnimator.removeAllListeners();
+ mTodayAnimator.cancel();
+ }
+ mTodayAnimator = ObjectAnimator.ofInt(MonthWeekEventsView.this,
+ "animateTodayAlpha", 255, 0);
+ mAnimator = mTodayAnimator;
+ mFadingIn = false;
+ mTodayAnimator.addListener(this);
+ mTodayAnimator.setDuration(600);
+ mTodayAnimator.start();
+ } else {
+ mAnimateToday = false;
+ mAnimateTodayAlpha = 0;
+ mAnimator.removeAllListeners();
+ mAnimator = null;
+ mTodayAnimator = null;
+ invalidate();
+ }
+ }
+ }
+
+ public void setAnimator(Animator animation) {
+ mAnimator = animation;
+ }
+
+ public void setFadingIn(boolean fadingIn) {
+ mFadingIn = fadingIn;
+ }
+
+ }
+
+ private int[] mDayXs;
+
+ /**
+ * This provides a reference to a float array which allows for easy size
+ * checking and reallocation. Used for drawing lines.
+ */
+ private class FloatRef {
+ float[] array;
+
+ public FloatRef(int size) {
+ array = new float[size];
+ }
+
+ public void ensureSize(int newSize) {
+ if (newSize >= array.length) {
+ // Add enough space for 7 more boxes to be drawn
+ array = Arrays.copyOf(array, newSize + 16 * 7);
+ }
+ }
+ }
+
+ /**
+ * Shows up as an error if we don't include this.
+ */
+ public MonthWeekEventsView(Context context) {
+ super(context);
+ }
+
+ // Sets the list of events for this week. Takes a sorted list of arrays
+ // divided up by day for generating the large month version and the full
+ // arraylist sorted by start time to generate the dna version.
+ public void setEvents(List<ArrayList<Event>> sortedEvents, ArrayList<Event> unsortedEvents) {
+ setEvents(sortedEvents);
+ // The MIN_WEEK_WIDTH is a hack to prevent the view from trying to
+ // generate dna bits before its width has been fixed.
+ createDna(unsortedEvents);
+ }
+
+ /**
+ * Sets up the dna bits for the view. This will return early if the view
+ * isn't in a state that will create a valid set of dna yet (such as the
+ * views width not being set correctly yet).
+ */
+ public void createDna(ArrayList<Event> unsortedEvents) {
+ if (unsortedEvents == null || mWidth <= MIN_WEEK_WIDTH || getContext() == null) {
+ // Stash the list of events for use when this view is ready, or
+ // just clear it if a null set has been passed to this view
+ mUnsortedEvents = unsortedEvents;
+ mDna = null;
+ return;
+ } else {
+ // clear the cached set of events since we're ready to build it now
+ mUnsortedEvents = null;
+ }
+ // Create the drawing coordinates for dna
+ if (!mShowDetailsInMonth) {
+ int numDays = mEvents.size();
+ int effectiveWidth = mWidth - mPadding * 2;
+ if (mShowWeekNum) {
+ effectiveWidth -= SPACING_WEEK_NUMBER;
+ }
+ DNA_ALL_DAY_WIDTH = effectiveWidth / numDays - 2 * DNA_SIDE_PADDING;
+ mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH);
+ mDayXs = new int[numDays];
+ for (int day = 0; day < numDays; day++) {
+ mDayXs[day] = computeDayLeftPosition(day) + DNA_WIDTH / 2 + DNA_SIDE_PADDING;
+
+ }
+
+ int top = DAY_SEPARATOR_INNER_WIDTH + DNA_MARGIN + DNA_ALL_DAY_HEIGHT + 1;
+ int bottom = mHeight - DNA_MARGIN;
+ mDna = Utils.createDNAStrands(mFirstJulianDay, unsortedEvents, top, bottom,
+ DNA_MIN_SEGMENT_HEIGHT, mDayXs, getContext());
+ }
+ }
+
+ public void setEvents(List<ArrayList<Event>> sortedEvents) {
+ mEvents = sortedEvents;
+ if (sortedEvents == null) {
+ return;
+ }
+ if (sortedEvents.size() != mNumDays) {
+ if (Log.isLoggable(TAG, Log.ERROR)) {
+ Log.wtf(TAG, "Events size must be same as days displayed: size="
+ + sortedEvents.size() + " days=" + mNumDays);
+ }
+ mEvents = null;
+ return;
+ }
+ }
+
+ protected void loadColors(Context context) {
+ Resources res = context.getResources();
+ mMonthWeekNumColor = res.getColor(R.color.month_week_num_color);
+ mMonthNumColor = res.getColor(R.color.month_day_number);
+ mMonthNumOtherColor = res.getColor(R.color.month_day_number_other);
+ mMonthNumTodayColor = res.getColor(R.color.month_today_number);
+ mMonthNameColor = mMonthNumColor;
+ mMonthNameOtherColor = mMonthNumOtherColor;
+ mMonthEventColor = res.getColor(R.color.month_event_color);
+ mMonthDeclinedEventColor = res.getColor(R.color.agenda_item_declined_color);
+ mMonthDeclinedExtrasColor = res.getColor(R.color.agenda_item_where_declined_text_color);
+ mMonthEventExtraColor = res.getColor(R.color.month_event_extra_color);
+ mMonthEventOtherColor = res.getColor(R.color.month_event_other_color);
+ mMonthEventExtraOtherColor = res.getColor(R.color.month_event_extra_other_color);
+ mMonthBGTodayColor = res.getColor(R.color.month_today_bgcolor);
+ mMonthBGOtherColor = res.getColor(R.color.month_other_bgcolor);
+ mMonthBGColor = res.getColor(R.color.month_bgcolor);
+ mDaySeparatorInnerColor = res.getColor(R.color.month_grid_lines);
+ mTodayAnimateColor = res.getColor(R.color.today_highlight_color);
+ mClickedDayColor = res.getColor(R.color.day_clicked_background_color);
+ mTodayDrawable = res.getDrawable(R.drawable.today_blue_week_holo_light);
+ }
+
+ /**
+ * Sets up the text and style properties for painting. Override this if you
+ * want to use a different paint.
+ */
+ @Override
+ protected void initView() {
+ super.initView();
+
+ if (!mInitialized) {
+ Resources resources = getContext().getResources();
+ mShowDetailsInMonth = Utils.getConfigBool(getContext(), R.bool.show_details_in_month);
+ TEXT_SIZE_EVENT_TITLE = resources.getInteger(R.integer.text_size_event_title);
+ TEXT_SIZE_MONTH_NUMBER = resources.getInteger(R.integer.text_size_month_number);
+ SIDE_PADDING_MONTH_NUMBER = resources.getInteger(R.integer.month_day_number_margin);
+ CONFLICT_COLOR = resources.getColor(R.color.month_dna_conflict_time_color);
+ EVENT_TEXT_COLOR = resources.getColor(R.color.calendar_event_text_color);
+ if (mScale != 1) {
+ TOP_PADDING_MONTH_NUMBER *= mScale;
+ TOP_PADDING_WEEK_NUMBER *= mScale;
+ SIDE_PADDING_MONTH_NUMBER *= mScale;
+ SIDE_PADDING_WEEK_NUMBER *= mScale;
+ SPACING_WEEK_NUMBER *= mScale;
+ TEXT_SIZE_MONTH_NUMBER *= mScale;
+ TEXT_SIZE_EVENT *= mScale;
+ TEXT_SIZE_EVENT_TITLE *= mScale;
+ TEXT_SIZE_MORE_EVENTS *= mScale;
+ TEXT_SIZE_MONTH_NAME *= mScale;
+ TEXT_SIZE_WEEK_NUM *= mScale;
+ DAY_SEPARATOR_OUTER_WIDTH *= mScale;
+ DAY_SEPARATOR_INNER_WIDTH *= mScale;
+ DAY_SEPARATOR_VERTICAL_LENGTH *= mScale;
+ DAY_SEPARATOR_VERTICAL_LENGTH_PORTRAIT *= mScale;
+ EVENT_X_OFFSET_LANDSCAPE *= mScale;
+ EVENT_Y_OFFSET_LANDSCAPE *= mScale;
+ EVENT_Y_OFFSET_PORTRAIT *= mScale;
+ EVENT_SQUARE_WIDTH *= mScale;
+ EVENT_SQUARE_BORDER *= mScale;
+ EVENT_LINE_PADDING *= mScale;
+ EVENT_BOTTOM_PADDING *= mScale;
+ EVENT_RIGHT_PADDING *= mScale;
+ DNA_MARGIN *= mScale;
+ DNA_WIDTH *= mScale;
+ DNA_ALL_DAY_HEIGHT *= mScale;
+ DNA_MIN_SEGMENT_HEIGHT *= mScale;
+ DNA_SIDE_PADDING *= mScale;
+ DEFAULT_EDGE_SPACING *= mScale;
+ DNA_ALL_DAY_WIDTH *= mScale;
+ TODAY_HIGHLIGHT_WIDTH *= mScale;
+ }
+ if (!mShowDetailsInMonth) {
+ TOP_PADDING_MONTH_NUMBER += DNA_ALL_DAY_HEIGHT + DNA_MARGIN;
+ }
+ mInitialized = true;
+ }
+ mPadding = DEFAULT_EDGE_SPACING;
+ loadColors(getContext());
+ // TODO modify paint properties depending on isMini
+
+ mMonthNumPaint = new Paint();
+ mMonthNumPaint.setFakeBoldText(false);
+ mMonthNumPaint.setAntiAlias(true);
+ mMonthNumPaint.setTextSize(TEXT_SIZE_MONTH_NUMBER);
+ mMonthNumPaint.setColor(mMonthNumColor);
+ mMonthNumPaint.setStyle(Style.FILL);
+ mMonthNumPaint.setTextAlign(Align.RIGHT);
+ mMonthNumPaint.setTypeface(Typeface.DEFAULT);
+
+ mMonthNumAscentHeight = (int) (-mMonthNumPaint.ascent() + 0.5f);
+ mMonthNumHeight = (int) (mMonthNumPaint.descent() - mMonthNumPaint.ascent() + 0.5f);
+
+ mEventPaint = new TextPaint();
+ mEventPaint.setFakeBoldText(true);
+ mEventPaint.setAntiAlias(true);
+ mEventPaint.setTextSize(TEXT_SIZE_EVENT_TITLE);
+ mEventPaint.setColor(mMonthEventColor);
+
+ mSolidBackgroundEventPaint = new TextPaint(mEventPaint);
+ mSolidBackgroundEventPaint.setColor(EVENT_TEXT_COLOR);
+ mFramedEventPaint = new TextPaint(mSolidBackgroundEventPaint);
+
+ mDeclinedEventPaint = new TextPaint();
+ mDeclinedEventPaint.setFakeBoldText(true);
+ mDeclinedEventPaint.setAntiAlias(true);
+ mDeclinedEventPaint.setTextSize(TEXT_SIZE_EVENT_TITLE);
+ mDeclinedEventPaint.setColor(mMonthDeclinedEventColor);
+
+ mEventAscentHeight = (int) (-mEventPaint.ascent() + 0.5f);
+ mEventHeight = (int) (mEventPaint.descent() - mEventPaint.ascent() + 0.5f);
+
+ mEventExtrasPaint = new TextPaint();
+ mEventExtrasPaint.setFakeBoldText(false);
+ mEventExtrasPaint.setAntiAlias(true);
+ mEventExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER);
+ mEventExtrasPaint.setTextSize(TEXT_SIZE_EVENT);
+ mEventExtrasPaint.setColor(mMonthEventExtraColor);
+ mEventExtrasPaint.setStyle(Style.FILL);
+ mEventExtrasPaint.setTextAlign(Align.LEFT);
+ mExtrasHeight = (int)(mEventExtrasPaint.descent() - mEventExtrasPaint.ascent() + 0.5f);
+ mExtrasAscentHeight = (int)(-mEventExtrasPaint.ascent() + 0.5f);
+ mExtrasDescent = (int)(mEventExtrasPaint.descent() + 0.5f);
+
+ mEventDeclinedExtrasPaint = new TextPaint();
+ mEventDeclinedExtrasPaint.setFakeBoldText(false);
+ mEventDeclinedExtrasPaint.setAntiAlias(true);
+ mEventDeclinedExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER);
+ mEventDeclinedExtrasPaint.setTextSize(TEXT_SIZE_EVENT);
+ mEventDeclinedExtrasPaint.setColor(mMonthDeclinedExtrasColor);
+ mEventDeclinedExtrasPaint.setStyle(Style.FILL);
+ mEventDeclinedExtrasPaint.setTextAlign(Align.LEFT);
+
+ mWeekNumPaint = new Paint();
+ mWeekNumPaint.setFakeBoldText(false);
+ mWeekNumPaint.setAntiAlias(true);
+ mWeekNumPaint.setTextSize(TEXT_SIZE_WEEK_NUM);
+ mWeekNumPaint.setColor(mWeekNumColor);
+ mWeekNumPaint.setStyle(Style.FILL);
+ mWeekNumPaint.setTextAlign(Align.RIGHT);
+
+ mWeekNumAscentHeight = (int) (-mWeekNumPaint.ascent() + 0.5f);
+
+ mDNAAllDayPaint = new Paint();
+ mDNATimePaint = new Paint();
+ mDNATimePaint.setColor(mMonthBusyBitsBusyTimeColor);
+ mDNATimePaint.setStyle(Style.FILL_AND_STROKE);
+ mDNATimePaint.setStrokeWidth(DNA_WIDTH);
+ mDNATimePaint.setAntiAlias(false);
+ mDNAAllDayPaint.setColor(mMonthBusyBitsConflictTimeColor);
+ mDNAAllDayPaint.setStyle(Style.FILL_AND_STROKE);
+ mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH);
+ mDNAAllDayPaint.setAntiAlias(false);
+
+ mEventSquarePaint = new Paint();
+ mEventSquarePaint.setStrokeWidth(EVENT_SQUARE_BORDER);
+ mEventSquarePaint.setAntiAlias(false);
+
+ if (DEBUG_LAYOUT) {
+ Log.d("EXTRA", "mScale=" + mScale);
+ Log.d("EXTRA", "mMonthNumPaint ascent=" + mMonthNumPaint.ascent()
+ + " descent=" + mMonthNumPaint.descent() + " int height=" + mMonthNumHeight);
+ Log.d("EXTRA", "mEventPaint ascent=" + mEventPaint.ascent()
+ + " descent=" + mEventPaint.descent() + " int height=" + mEventHeight
+ + " int ascent=" + mEventAscentHeight);
+ Log.d("EXTRA", "mEventExtrasPaint ascent=" + mEventExtrasPaint.ascent()
+ + " descent=" + mEventExtrasPaint.descent() + " int height=" + mExtrasHeight);
+ Log.d("EXTRA", "mWeekNumPaint ascent=" + mWeekNumPaint.ascent()
+ + " descent=" + mWeekNumPaint.descent());
+ }
+ }
+
+ @Override
+ public void setWeekParams(HashMap<String, Integer> params, String tz) {
+ super.setWeekParams(params, tz);
+
+ if (params.containsKey(VIEW_PARAMS_ORIENTATION)) {
+ mOrientation = params.get(VIEW_PARAMS_ORIENTATION);
+ }
+
+ updateToday(tz);
+ mNumCells = mNumDays + 1;
+
+ if (params.containsKey(VIEW_PARAMS_ANIMATE_TODAY) && mHasToday) {
+ synchronized (mAnimatorListener) {
+ if (mTodayAnimator != null) {
+ mTodayAnimator.removeAllListeners();
+ mTodayAnimator.cancel();
+ }
+ mTodayAnimator = ObjectAnimator.ofInt(this, "animateTodayAlpha",
+ Math.max(mAnimateTodayAlpha, 80), 255);
+ mTodayAnimator.setDuration(150);
+ mAnimatorListener.setAnimator(mTodayAnimator);
+ mAnimatorListener.setFadingIn(true);
+ mTodayAnimator.addListener(mAnimatorListener);
+ mAnimateToday = true;
+ mTodayAnimator.start();
+ }
+ }
+ }
+
+ /**
+ * @param tz
+ */
+ public boolean updateToday(String tz) {
+ mToday.timezone = tz;
+ mToday.setToNow();
+ mToday.normalize(true);
+ int julianToday = Time.getJulianDay(mToday.toMillis(false), mToday.gmtoff);
+ if (julianToday >= mFirstJulianDay && julianToday < mFirstJulianDay + mNumDays) {
+ mHasToday = true;
+ mTodayIndex = julianToday - mFirstJulianDay;
+ } else {
+ mHasToday = false;
+ mTodayIndex = -1;
+ }
+ return mHasToday;
+ }
+
+ public void setAnimateTodayAlpha(int alpha) {
+ mAnimateTodayAlpha = alpha;
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ drawBackground(canvas);
+ drawWeekNums(canvas);
+ drawDaySeparators(canvas);
+ if (mHasToday && mAnimateToday) {
+ drawToday(canvas);
+ }
+ if (mShowDetailsInMonth) {
+ drawEvents(canvas);
+ } else {
+ if (mDna == null && mUnsortedEvents != null) {
+ createDna(mUnsortedEvents);
+ }
+ drawDNA(canvas);
+ }
+ drawClick(canvas);
+ }
+
+ protected void drawToday(Canvas canvas) {
+ r.top = DAY_SEPARATOR_INNER_WIDTH + (TODAY_HIGHLIGHT_WIDTH / 2);
+ r.bottom = mHeight - (int) Math.ceil(TODAY_HIGHLIGHT_WIDTH / 2.0f);
+ p.setStyle(Style.STROKE);
+ p.setStrokeWidth(TODAY_HIGHLIGHT_WIDTH);
+ r.left = computeDayLeftPosition(mTodayIndex) + (TODAY_HIGHLIGHT_WIDTH / 2);
+ r.right = computeDayLeftPosition(mTodayIndex + 1)
+ - (int) Math.ceil(TODAY_HIGHLIGHT_WIDTH / 2.0f);
+ p.setColor(mTodayAnimateColor | (mAnimateTodayAlpha << 24));
+ canvas.drawRect(r, p);
+ p.setStyle(Style.FILL);
+ }
+
+ // TODO move into SimpleWeekView
+ // Computes the x position for the left side of the given day
+ private int computeDayLeftPosition(int day) {
+ int effectiveWidth = mWidth;
+ int x = 0;
+ int xOffset = 0;
+ if (mShowWeekNum) {
+ xOffset = SPACING_WEEK_NUMBER + mPadding;
+ effectiveWidth -= xOffset;
+ }
+ x = day * effectiveWidth / mNumDays + xOffset;
+ return x;
+ }
+
+ @Override
+ protected void drawDaySeparators(Canvas canvas) {
+ float lines[] = new float[8 * 4];
+ int count = 6 * 4;
+ int wkNumOffset = 0;
+ int i = 0;
+ if (mShowWeekNum) {
+ // This adds the first line separating the week number
+ int xOffset = SPACING_WEEK_NUMBER + mPadding;
+ count += 4;
+ lines[i++] = xOffset;
+ lines[i++] = 0;
+ lines[i++] = xOffset;
+ lines[i++] = mHeight;
+ wkNumOffset++;
+ }
+ count += 4;
+ lines[i++] = 0;
+ lines[i++] = 0;
+ lines[i++] = mWidth;
+ lines[i++] = 0;
+ int y0 = 0;
+ int y1 = mHeight;
+
+ while (i < count) {
+ int x = computeDayLeftPosition(i / 4 - wkNumOffset);
+ lines[i++] = x;
+ lines[i++] = y0;
+ lines[i++] = x;
+ lines[i++] = y1;
+ }
+ p.setColor(mDaySeparatorInnerColor);
+ p.setStrokeWidth(DAY_SEPARATOR_INNER_WIDTH);
+ canvas.drawLines(lines, 0, count, p);
+ }
+
+ @Override
+ protected void drawBackground(Canvas canvas) {
+ int i = 0;
+ int offset = 0;
+ r.top = DAY_SEPARATOR_INNER_WIDTH;
+ r.bottom = mHeight;
+ if (mShowWeekNum) {
+ i++;
+ offset++;
+ }
+ if (!mOddMonth[i]) {
+ while (++i < mOddMonth.length && !mOddMonth[i])
+ ;
+ r.right = computeDayLeftPosition(i - offset);
+ r.left = 0;
+ p.setColor(mMonthBGOtherColor);
+ canvas.drawRect(r, p);
+ // compute left edge for i, set up r, draw
+ } else if (!mOddMonth[(i = mOddMonth.length - 1)]) {
+ while (--i >= offset && !mOddMonth[i])
+ ;
+ i++;
+ // compute left edge for i, set up r, draw
+ r.right = mWidth;
+ r.left = computeDayLeftPosition(i - offset);
+ p.setColor(mMonthBGOtherColor);
+ canvas.drawRect(r, p);
+ }
+ if (mHasToday) {
+ p.setColor(mMonthBGTodayColor);
+ r.left = computeDayLeftPosition(mTodayIndex);
+ r.right = computeDayLeftPosition(mTodayIndex + 1);
+ canvas.drawRect(r, p);
+ }
+ }
+
+ // Draw the "clicked" color on the tapped day
+ private void drawClick(Canvas canvas) {
+ if (mClickedDayIndex != -1) {
+ int alpha = p.getAlpha();
+ p.setColor(mClickedDayColor);
+ p.setAlpha(mClickedAlpha);
+ r.left = computeDayLeftPosition(mClickedDayIndex);
+ r.right = computeDayLeftPosition(mClickedDayIndex + 1);
+ r.top = DAY_SEPARATOR_INNER_WIDTH;
+ r.bottom = mHeight;
+ canvas.drawRect(r, p);
+ p.setAlpha(alpha);
+ }
+ }
+
+ @Override
+ protected void drawWeekNums(Canvas canvas) {
+ int y;
+
+ int i = 0;
+ int offset = -1;
+ int todayIndex = mTodayIndex;
+ int x = 0;
+ int numCount = mNumDays;
+ if (mShowWeekNum) {
+ x = SIDE_PADDING_WEEK_NUMBER + mPadding;
+ y = mWeekNumAscentHeight + TOP_PADDING_WEEK_NUMBER;
+ canvas.drawText(mDayNumbers[0], x, y, mWeekNumPaint);
+ numCount++;
+ i++;
+ todayIndex++;
+ offset++;
+
+ }
+
+ y = mMonthNumAscentHeight + TOP_PADDING_MONTH_NUMBER;
+
+ boolean isFocusMonth = mFocusDay[i];
+ boolean isBold = false;
+ mMonthNumPaint.setColor(isFocusMonth ? mMonthNumColor : mMonthNumOtherColor);
+ for (; i < numCount; i++) {
+ if (mHasToday && todayIndex == i) {
+ mMonthNumPaint.setColor(mMonthNumTodayColor);
+ mMonthNumPaint.setFakeBoldText(isBold = true);
+ if (i + 1 < numCount) {
+ // Make sure the color will be set back on the next
+ // iteration
+ isFocusMonth = !mFocusDay[i + 1];
+ }
+ } else if (mFocusDay[i] != isFocusMonth) {
+ isFocusMonth = mFocusDay[i];
+ mMonthNumPaint.setColor(isFocusMonth ? mMonthNumColor : mMonthNumOtherColor);
+ }
+ x = computeDayLeftPosition(i - offset) - (SIDE_PADDING_MONTH_NUMBER);
+ canvas.drawText(mDayNumbers[i], x, y, mMonthNumPaint);
+ if (isBold) {
+ mMonthNumPaint.setFakeBoldText(isBold = false);
+ }
+ }
+ }
+
+ protected void drawEvents(Canvas canvas) {
+ if (mEvents == null) {
+ return;
+ }
+
+ int day = -1;
+ for (ArrayList<Event> eventDay : mEvents) {
+ day++;
+ if (eventDay == null || eventDay.size() == 0) {
+ continue;
+ }
+ int ySquare;
+ int xSquare = computeDayLeftPosition(day) + SIDE_PADDING_MONTH_NUMBER + 1;
+ int rightEdge = computeDayLeftPosition(day + 1);
+
+ if (mOrientation == Configuration.ORIENTATION_PORTRAIT) {
+ ySquare = EVENT_Y_OFFSET_PORTRAIT + mMonthNumHeight + TOP_PADDING_MONTH_NUMBER;
+ rightEdge -= SIDE_PADDING_MONTH_NUMBER + 1;
+ } else {
+ ySquare = EVENT_Y_OFFSET_LANDSCAPE;
+ rightEdge -= EVENT_X_OFFSET_LANDSCAPE;
+ }
+
+ // Determine if everything will fit when time ranges are shown.
+ boolean showTimes = true;
+ Iterator<Event> iter = eventDay.iterator();
+ int yTest = ySquare;
+ while (iter.hasNext()) {
+ Event event = iter.next();
+ int newY = drawEvent(canvas, event, xSquare, yTest, rightEdge, iter.hasNext(),
+ showTimes, /*doDraw*/ false);
+ if (newY == yTest) {
+ showTimes = false;
+ break;
+ }
+ yTest = newY;
+ }
+
+ int eventCount = 0;
+ iter = eventDay.iterator();
+ while (iter.hasNext()) {
+ Event event = iter.next();
+ int newY = drawEvent(canvas, event, xSquare, ySquare, rightEdge, iter.hasNext(),
+ showTimes, /*doDraw*/ true);
+ if (newY == ySquare) {
+ break;
+ }
+ eventCount++;
+ ySquare = newY;
+ }
+
+ int remaining = eventDay.size() - eventCount;
+ if (remaining > 0) {
+ drawMoreEvents(canvas, remaining, xSquare);
+ }
+ }
+ }
+
+ protected int addChipOutline(FloatRef lines, int count, int x, int y) {
+ lines.ensureSize(count + 16);
+ // top of box
+ lines.array[count++] = x;
+ lines.array[count++] = y;
+ lines.array[count++] = x + EVENT_SQUARE_WIDTH;
+ lines.array[count++] = y;
+ // right side of box
+ lines.array[count++] = x + EVENT_SQUARE_WIDTH;
+ lines.array[count++] = y;
+ lines.array[count++] = x + EVENT_SQUARE_WIDTH;
+ lines.array[count++] = y + EVENT_SQUARE_WIDTH;
+ // left side of box
+ lines.array[count++] = x;
+ lines.array[count++] = y;
+ lines.array[count++] = x;
+ lines.array[count++] = y + EVENT_SQUARE_WIDTH + 1;
+ // bottom of box
+ lines.array[count++] = x;
+ lines.array[count++] = y + EVENT_SQUARE_WIDTH;
+ lines.array[count++] = x + EVENT_SQUARE_WIDTH + 1;
+ lines.array[count++] = y + EVENT_SQUARE_WIDTH;
+
+ return count;
+ }
+
+ /**
+ * Attempts to draw the given event. Returns the y for the next event or the
+ * original y if the event will not fit. An event is considered to not fit
+ * if the event and its extras won't fit or if there are more events and the
+ * more events line would not fit after drawing this event.
+ *
+ * @param canvas the canvas to draw on
+ * @param event the event to draw
+ * @param x the top left corner for this event's color chip
+ * @param y the top left corner for this event's color chip
+ * @param rightEdge the rightmost point we're allowed to draw on (exclusive)
+ * @param moreEvents indicates whether additional events will follow this one
+ * @param showTimes if set, a second line with a time range will be displayed for non-all-day
+ * events
+ * @param doDraw if set, do the actual drawing; otherwise this just computes the height
+ * and returns
+ * @return the y for the next event or the original y if it won't fit
+ */
+ protected int drawEvent(Canvas canvas, Event event, int x, int y, int rightEdge,
+ boolean moreEvents, boolean showTimes, boolean doDraw) {
+ /*
+ * Vertical layout:
+ * (top of box)
+ * a. EVENT_Y_OFFSET_LANDSCAPE or portrait equivalent
+ * b. Event title: mEventHeight for a normal event, + 2xBORDER_SPACE for all-day event
+ * c. [optional] Time range (mExtrasHeight)
+ * d. EVENT_LINE_PADDING
+ *
+ * Repeat (b,c,d) as needed and space allows. If we have more events than fit, we need
+ * to leave room for something like "+2" at the bottom:
+ *
+ * e. "+ more" line (mExtrasHeight)
+ *
+ * f. EVENT_BOTTOM_PADDING (overlaps EVENT_LINE_PADDING)
+ * (bottom of box)
+ */
+ final int BORDER_SPACE = EVENT_SQUARE_BORDER + 1; // want a 1-pixel gap inside border
+ final int STROKE_WIDTH_ADJ = EVENT_SQUARE_BORDER / 2; // adjust bounds for stroke width
+ boolean allDay = event.allDay;
+ int eventRequiredSpace = mEventHeight;
+ if (allDay) {
+ // Add a few pixels for the box we draw around all-day events.
+ eventRequiredSpace += BORDER_SPACE * 2;
+ } else if (showTimes) {
+ // Need room for the "1pm - 2pm" line.
+ eventRequiredSpace += mExtrasHeight;
+ }
+ int reservedSpace = EVENT_BOTTOM_PADDING; // leave a bit of room at the bottom
+ if (moreEvents) {
+ // More events follow. Leave a bit of space between events.
+ eventRequiredSpace += EVENT_LINE_PADDING;
+
+ // Make sure we have room for the "+ more" line. (The "+ more" line is expected
+ // to be <= the height of an event line, so we won't show "+1" when we could be
+ // showing the event.)
+ reservedSpace += mExtrasHeight;
+ }
+
+ if (y + eventRequiredSpace + reservedSpace > mHeight) {
+ // Not enough space, return original y
+ return y;
+ } else if (!doDraw) {
+ return y + eventRequiredSpace;
+ }
+
+ boolean isDeclined = event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED;
+ int color = event.color;
+ if (isDeclined) {
+ color = Utils.getDeclinedColorFromColor(color);
+ }
+
+ int textX, textY, textRightEdge;
+
+ if (allDay) {
+ // We shift the render offset "inward", because drawRect with a stroke width greater
+ // than 1 draws outside the specified bounds. (We don't adjust the left edge, since
+ // we want to match the existing appearance of the "event square".)
+ r.left = x;
+ r.right = rightEdge - STROKE_WIDTH_ADJ;
+ r.top = y + STROKE_WIDTH_ADJ;
+ r.bottom = y + mEventHeight + BORDER_SPACE * 2 - STROKE_WIDTH_ADJ;
+ textX = x + BORDER_SPACE;
+ textY = y + mEventAscentHeight + BORDER_SPACE;
+ textRightEdge = rightEdge - BORDER_SPACE;
+ } else {
+ r.left = x;
+ r.right = x + EVENT_SQUARE_WIDTH;
+ r.bottom = y + mEventAscentHeight;
+ r.top = r.bottom - EVENT_SQUARE_WIDTH;
+ textX = x + EVENT_SQUARE_WIDTH + EVENT_RIGHT_PADDING;
+ textY = y + mEventAscentHeight;
+ textRightEdge = rightEdge;
+ }
+
+ Style boxStyle = Style.STROKE;
+ boolean solidBackground = false;
+ if (event.selfAttendeeStatus != Attendees.ATTENDEE_STATUS_INVITED) {
+ boxStyle = Style.FILL_AND_STROKE;
+ if (allDay) {
+ solidBackground = true;
+ }
+ }
+ mEventSquarePaint.setStyle(boxStyle);
+ mEventSquarePaint.setColor(color);
+ canvas.drawRect(r, mEventSquarePaint);
+
+ float avail = textRightEdge - textX;
+ CharSequence text = TextUtils.ellipsize(
+ event.title, mEventPaint, avail, TextUtils.TruncateAt.END);
+ Paint textPaint;
+ if (solidBackground) {
+ // Text color needs to contrast with solid background.
+ textPaint = mSolidBackgroundEventPaint;
+ } else if (isDeclined) {
+ // Use "declined event" color.
+ textPaint = mDeclinedEventPaint;
+ } else if (allDay) {
+ // Text inside frame is same color as frame.
+ mFramedEventPaint.setColor(color);
+ textPaint = mFramedEventPaint;
+ } else {
+ // Use generic event text color.
+ textPaint = mEventPaint;
+ }
+ canvas.drawText(text.toString(), textX, textY, textPaint);
+ y += mEventHeight;
+ if (allDay) {
+ y += BORDER_SPACE * 2;
+ }
+
+ if (showTimes && !allDay) {
+ // show start/end time, e.g. "1pm - 2pm"
+ textY = y + mExtrasAscentHeight;
+ mStringBuilder.setLength(0);
+ text = DateUtils.formatDateRange(getContext(), mFormatter, event.startMillis,
+ event.endMillis, DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL,
+ Utils.getTimeZone(getContext(), null)).toString();
+ text = TextUtils.ellipsize(text, mEventExtrasPaint, avail, TextUtils.TruncateAt.END);
+ canvas.drawText(text.toString(), textX, textY, isDeclined ? mEventDeclinedExtrasPaint
+ : mEventExtrasPaint);
+ y += mExtrasHeight;
+ }
+
+ y += EVENT_LINE_PADDING;
+
+ return y;
+ }
+
+ protected void drawMoreEvents(Canvas canvas, int remainingEvents, int x) {
+ int y = mHeight - (mExtrasDescent + EVENT_BOTTOM_PADDING);
+ String text = getContext().getResources().getQuantityString(
+ R.plurals.month_more_events, remainingEvents);
+ mEventExtrasPaint.setAntiAlias(true);
+ mEventExtrasPaint.setFakeBoldText(true);
+ canvas.drawText(String.format(text, remainingEvents), x, y, mEventExtrasPaint);
+ mEventExtrasPaint.setFakeBoldText(false);
+ }
+
+ /**
+ * Draws a line showing busy times in each day of week The method draws
+ * non-conflicting times in the event color and times with conflicting
+ * events in the dna conflict color defined in colors.
+ *
+ * @param canvas
+ */
+ protected void drawDNA(Canvas canvas) {
+ // Draw event and conflict times
+ if (mDna != null) {
+ for (Utils.DNAStrand strand : mDna.values()) {
+ if (strand.color == CONFLICT_COLOR || strand.points == null
+ || strand.points.length == 0) {
+ continue;
+ }
+ mDNATimePaint.setColor(strand.color);
+ canvas.drawLines(strand.points, mDNATimePaint);
+ }
+ // Draw black last to make sure it's on top
+ Utils.DNAStrand strand = mDna.get(CONFLICT_COLOR);
+ if (strand != null && strand.points != null && strand.points.length != 0) {
+ mDNATimePaint.setColor(strand.color);
+ canvas.drawLines(strand.points, mDNATimePaint);
+ }
+ if (mDayXs == null) {
+ return;
+ }
+ int numDays = mDayXs.length;
+ int xOffset = (DNA_ALL_DAY_WIDTH - DNA_WIDTH) / 2;
+ if (strand != null && strand.allDays != null && strand.allDays.length == numDays) {
+ for (int i = 0; i < numDays; i++) {
+ // this adds at most 7 draws. We could sort it by color and
+ // build an array instead but this is easier.
+ if (strand.allDays[i] != 0) {
+ mDNAAllDayPaint.setColor(strand.allDays[i]);
+ canvas.drawLine(mDayXs[i] + xOffset, DNA_MARGIN, mDayXs[i] + xOffset,
+ DNA_MARGIN + DNA_ALL_DAY_HEIGHT, mDNAAllDayPaint);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void updateSelectionPositions() {
+ if (mHasSelectedDay) {
+ int selectedPosition = mSelectedDay - mWeekStart;
+ if (selectedPosition < 0) {
+ selectedPosition += 7;
+ }
+ int effectiveWidth = mWidth - mPadding * 2;
+ effectiveWidth -= SPACING_WEEK_NUMBER;
+ mSelectedLeft = selectedPosition * effectiveWidth / mNumDays + mPadding;
+ mSelectedRight = (selectedPosition + 1) * effectiveWidth / mNumDays + mPadding;
+ mSelectedLeft += SPACING_WEEK_NUMBER;
+ mSelectedRight += SPACING_WEEK_NUMBER;
+ }
+ }
+
+ public int getDayIndexFromLocation(float x) {
+ int dayStart = mShowWeekNum ? SPACING_WEEK_NUMBER + mPadding : mPadding;
+ if (x < dayStart || x > mWidth - mPadding) {
+ return -1;
+ }
+ // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
+ return ((int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)));
+ }
+
+ @Override
+ public Time getDayFromLocation(float x) {
+ int dayPosition = getDayIndexFromLocation(x);
+ if (dayPosition == -1) {
+ return null;
+ }
+ int day = mFirstJulianDay + dayPosition;
+
+ Time time = new Time(mTimeZone);
+ if (mWeek == 0) {
+ // This week is weird...
+ if (day < Time.EPOCH_JULIAN_DAY) {
+ day++;
+ } else if (day == Time.EPOCH_JULIAN_DAY) {
+ time.set(1, 0, 1970);
+ time.normalize(true);
+ return time;
+ }
+ }
+
+ time.setJulianDay(day);
+ return time;
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ Context context = getContext();
+ // only send accessibility events if accessibility and exploration are
+ // on.
+ AccessibilityManager am = (AccessibilityManager) context
+ .getSystemService(Service.ACCESSIBILITY_SERVICE);
+ if (!am.isEnabled() || !am.isTouchExplorationEnabled()) {
+ return super.onHoverEvent(event);
+ }
+ if (event.getAction() != MotionEvent.ACTION_HOVER_EXIT) {
+ Time hover = getDayFromLocation(event.getX());
+ if (hover != null
+ && (mLastHoverTime == null || Time.compare(hover, mLastHoverTime) != 0)) {
+ Long millis = hover.toMillis(true);
+ String date = Utils.formatDateRange(context, millis, millis,
+ DateUtils.FORMAT_SHOW_DATE);
+ AccessibilityEvent accessEvent = AccessibilityEvent
+ .obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
+ accessEvent.getText().add(date);
+ if (mShowDetailsInMonth && mEvents != null) {
+ int dayStart = SPACING_WEEK_NUMBER + mPadding;
+ int dayPosition = (int) ((event.getX() - dayStart) * mNumDays / (mWidth
+ - dayStart - mPadding));
+ ArrayList<Event> events = mEvents.get(dayPosition);
+ List<CharSequence> text = accessEvent.getText();
+ for (Event e : events) {
+ text.add(e.getTitleAndLocation() + ". ");
+ int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
+ if (!e.allDay) {
+ flags |= DateUtils.FORMAT_SHOW_TIME;
+ if (DateFormat.is24HourFormat(context)) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ }
+ } else {
+ flags |= DateUtils.FORMAT_UTC;
+ }
+ text.add(Utils.formatDateRange(context, e.startMillis, e.endMillis,
+ flags) + ". ");
+ }
+ }
+ sendAccessibilityEventUnchecked(accessEvent);
+ mLastHoverTime = hover;
+ }
+ }
+ return true;
+ }
+
+ public void setClickedDay(float xLocation) {
+ mClickedDayIndex = getDayIndexFromLocation(xLocation);
+ invalidate();
+ }
+ public void clearClickedDay() {
+ mClickedDayIndex = -1;
+ invalidate();
+ }
+}