package com.rey.material.app; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.text.format.DateUtils; import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import com.rey.material.R; import com.rey.material.util.ThemeUtil; import com.rey.material.widget.CircleCheckedTextView; import com.rey.material.widget.TimePicker; import java.text.DateFormat; import java.util.Calendar; /** * Created by Rey on 12/24/2014. */ public class TimePickerDialog extends Dialog{ private TimePickerLayout mTimePickerLayout; private float mCornerRadius; /** * Interface definition for a callback to be invoked when the selected time is changed. */ public interface OnTimeChangedListener{ /** * Called when the selected time is changed. * @param oldHour The hour value of old time. * @param oldMinute The minute value of old time. * @param newHour The hour value of new time. * @param newMinute The minute value of new time. */ public void onTimeChanged(int oldHour, int oldMinute, int newHour, int newMinute); } public TimePickerDialog(Context context) { super(context, R.style.Material_App_Dialog_TimePicker_Light); } public TimePickerDialog(Context context, int style) { super(context, style); } @Override protected void onCreate() { mTimePickerLayout = new TimePickerLayout(getContext()); contentView(mTimePickerLayout); } @Override public Dialog applyStyle(int resId) { super.applyStyle(resId); if(resId == 0) return this; mTimePickerLayout.applyStyle(resId); layoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); return this; } @Override public Dialog layoutParams(int width, int height) { return super.layoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); } @Override public Dialog cornerRadius(float radius){ mCornerRadius = radius; return super.cornerRadius(radius); } /** * Set the selected hour value. * @param hour The selected hour value. * @return The TimePickerDialog for chaining methods. */ public TimePickerDialog hour(int hour){ mTimePickerLayout.setHour(hour); return this; } /** * Set the selected minute value. * @param minute The selected minute value. * @return The TimePickerDialog for chaining methods. */ public TimePickerDialog minute(int minute){ mTimePickerLayout.setMinute(minute); return this; } /** * Set a listener will be called when the selected time is changed. * @param listener The {@link TimePickerDialog.OnTimeChangedListener} will be called. */ public TimePickerDialog onTimeChangedListener(OnTimeChangedListener listener){ mTimePickerLayout.setOnTimeChangedListener(listener); return this; } /** * @return The selected hour value. */ public int getHour(){ return mTimePickerLayout.getHour(); } /** * @return The selected minute value. */ public int getMinute(){ return mTimePickerLayout.getMinute(); } /** * Get the formatted string of selected time. * @param formatter The DateFormat used to format the time. * @return */ public String getFormattedTime(DateFormat formatter){ return mTimePickerLayout.getFormattedTime(formatter); } private class TimePickerLayout extends android.widget.FrameLayout implements View.OnClickListener, TimePicker.OnTimeChangedListener{ private int mHeaderHeight; private int mTextTimeColor; private int mTextTimeSize; private boolean mIsLeadingZero; private boolean mIsAm = true; private int mCheckBoxSize; private int mHeaderRealWidth; private int mHeaderRealHeight; private CircleCheckedTextView mAmView; private CircleCheckedTextView mPmView; private TimePicker mTimePicker; private Paint mPaint; private Path mHeaderBackground; private RectF mRect; private static final String TIME_DIVIDER = ":"; private static final String BASE_TEXT = "0"; private static final String FORMAT = "%02d"; private static final String FORMAT_NO_LEADING_ZERO = "%d"; private boolean mLocationDirty = true; private float mBaseY; private float mHourX; private float mDividerX; private float mMinuteX; private float mMiddayX; private float mHourWidth; private float mMinuteWidth; private float mBaseHeight; private String mHour; private String mMinute; private String mMidday; private OnTimeChangedListener mOnTimeChangedListener; public TimePickerLayout(Context context) { super(context); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setTextAlign(Paint.Align.LEFT); mHeaderBackground = new Path(); mRect = new RectF(); mAmView = new CircleCheckedTextView(context); mPmView = new CircleCheckedTextView(context); mTimePicker = new TimePicker(context); mTimePicker.setPadding(mContentPadding, mContentPadding, mContentPadding, mContentPadding); mTimePicker.setOnTimeChangedListener(this); mAmView.setGravity(Gravity.CENTER); mPmView.setGravity(Gravity.CENTER); if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){ mAmView.setTextAlignment(TEXT_ALIGNMENT_CENTER); mPmView.setTextAlignment(TEXT_ALIGNMENT_CENTER); } mAmView.setCheckedImmediately(mIsAm); mPmView.setCheckedImmediately(!mIsAm); mAmView.setOnClickListener(this); mPmView.setOnClickListener(this); addView(mTimePicker); addView(mAmView); addView(mPmView); setWillNotDraw(false); } public void applyStyle(int resId){ mTimePicker.applyStyle(resId); Context context = getContext(); mCheckBoxSize = ThemeUtil.dpToPx(context, 48); TypedArray a = context.obtainStyledAttributes(resId, R.styleable.TimePickerDialog); mHeaderHeight = a.getDimensionPixelSize(R.styleable.TimePickerDialog_tp_headerHeight, ThemeUtil.dpToPx(context, 120)); mTextTimeColor = a.getColor(R.styleable.TimePickerDialog_tp_textTimeColor, 0xFF000000); mTextTimeSize = a.getDimensionPixelSize(R.styleable.TimePickerDialog_tp_textTimeSize, context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_headline_material)); mIsLeadingZero = a.getBoolean(R.styleable.TimePickerDialog_tp_leadingZero, false); String am = a.getString(R.styleable.TimePickerDialog_tp_am); String pm = a.getString(R.styleable.TimePickerDialog_tp_pm); a.recycle(); if(am == null) am = DateUtils.getAMPMString(Calendar.AM).toUpperCase(); if(pm == null) pm = DateUtils.getAMPMString(Calendar.PM).toUpperCase(); int[][] states = new int[][]{ new int[]{-android.R.attr.state_checked}, new int[]{android.R.attr.state_checked}, }; int[] colors = new int[]{ mTimePicker.getTextColor(), mTimePicker.getTextHighlightColor(), }; mAmView.setBackgroundColor(mTimePicker.getSelectionColor()); mAmView.setAnimDuration(mTimePicker.getAnimDuration()); mAmView.setInterpolator(mTimePicker.getInInterpolator(), mTimePicker.getOutInterpolator()); mAmView.setTypeface(mTimePicker.getTypeface()); mAmView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTimePicker.getTextSize()); mAmView.setTextColor(new ColorStateList(states, colors)); mAmView.setText(am); mPmView.setBackgroundColor(mTimePicker.getSelectionColor()); mPmView.setAnimDuration(mTimePicker.getAnimDuration()); mPmView.setInterpolator(mTimePicker.getInInterpolator(), mTimePicker.getOutInterpolator()); mPmView.setTypeface(mTimePicker.getTypeface()); mPmView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTimePicker.getTextSize()); mPmView.setTextColor(new ColorStateList(states, colors)); mPmView.setText(pm); mPaint.setTypeface(mTimePicker.getTypeface()); mHour = String.format(mIsLeadingZero ? FORMAT : FORMAT_NO_LEADING_ZERO, !mTimePicker.is24Hour() && mTimePicker.getHour() == 0 ? 12 : mTimePicker.getHour()); mMinute = String.format(FORMAT, mTimePicker.getMinute()); if(!mTimePicker.is24Hour()) mMidday = mIsAm ? mAmView.getText().toString() : mPmView.getText().toString(); mLocationDirty = true; invalidate(0, 0, mHeaderRealWidth, mHeaderRealHeight); } public void setHour(int hour){ if(!mTimePicker.is24Hour()){ if(hour > 11 && hour < 24) setAm(false, false); else setAm(true, false); } mTimePicker.setHour(hour); } public int getHour(){ return mTimePicker.is24Hour() || mIsAm ? mTimePicker.getHour() : mTimePicker.getHour() + 12; } public void setMinute(int minute){ mTimePicker.setMinute(minute); } public int getMinute(){ return mTimePicker.getMinute(); } private void setAm(boolean am, boolean animation){ if(mTimePicker.is24Hour()) return; if(mIsAm != am){ int oldHour = getHour(); mIsAm = am; if(animation) { mAmView.setChecked(mIsAm); mPmView.setChecked(!mIsAm); } else{ mAmView.setCheckedImmediately(mIsAm); mPmView.setCheckedImmediately(!mIsAm); } mMidday = mIsAm ? mAmView.getText().toString() : mPmView.getText().toString(); invalidate(0, 0, mHeaderRealWidth, mHeaderRealHeight); if(mOnTimeChangedListener != null) mOnTimeChangedListener.onTimeChanged(oldHour, getMinute(), getHour(), getMinute()); } } public String getFormattedTime(DateFormat formatter){ Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, getHour()); cal.set(Calendar.MINUTE, getMinute()); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); return formatter.format(cal.getTime()); } public void setOnTimeChangedListener(OnTimeChangedListener listener){ mOnTimeChangedListener = listener; } @Override public void onClick(View v) { setAm(v == mAmView, true); } @Override public void onModeChanged(int mode){ invalidate(0, 0, mHeaderRealWidth, mHeaderRealHeight); } @Override public void onHourChanged(int oldValue, int newValue) { int oldHour = mTimePicker.is24Hour() || mIsAm ? oldValue : oldValue + 12; mHour = String.format(mIsLeadingZero ? FORMAT : FORMAT_NO_LEADING_ZERO, !mTimePicker.is24Hour() && newValue == 0 ? 12 : newValue); mLocationDirty = true; invalidate(0, 0, mHeaderRealWidth, mHeaderRealHeight); if(mOnTimeChangedListener != null) mOnTimeChangedListener.onTimeChanged(oldHour, getMinute(), getHour(), getMinute()); } @Override public void onMinuteChanged(int oldValue, int newValue) { mMinute = String.format(FORMAT, newValue); mLocationDirty = true; invalidate(0, 0, mHeaderRealWidth, mHeaderRealHeight); if(mOnTimeChangedListener != null) mOnTimeChangedListener.onTimeChanged(getHour(), oldValue, getHour(), newValue); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); boolean isPortrait = getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; int checkboxSize = mTimePicker.is24Hour() ? 0 : mCheckBoxSize; if(isPortrait){ if(heightMode == MeasureSpec.AT_MOST) heightSize = Math.min(heightSize, checkboxSize + widthSize + mHeaderHeight); if(checkboxSize > 0) { int spec = MeasureSpec.makeMeasureSpec(mCheckBoxSize, MeasureSpec.EXACTLY); mAmView.setVisibility(View.VISIBLE); mPmView.setVisibility(View.VISIBLE); mAmView.measure(spec, spec); mPmView.measure(spec, spec); } else{ mAmView.setVisibility(View.GONE); mPmView.setVisibility(View.GONE); } int spec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); mTimePicker.measure(spec, spec); setMeasuredDimension(widthSize, heightSize); } else{ int halfWidth = widthSize / 2; if(heightMode == MeasureSpec.AT_MOST) heightSize = Math.min(heightSize, Math.max(checkboxSize > 0 ? checkboxSize + mHeaderHeight + mContentPadding : mHeaderHeight, halfWidth)); if(checkboxSize > 0) { int spec = MeasureSpec.makeMeasureSpec(checkboxSize, MeasureSpec.EXACTLY); mAmView.setVisibility(View.VISIBLE); mPmView.setVisibility(View.VISIBLE); mAmView.measure(spec, spec); mPmView.measure(spec, spec); } else{ mAmView.setVisibility(View.GONE); mPmView.setVisibility(View.GONE); } int spec = MeasureSpec.makeMeasureSpec(Math.min(halfWidth, heightSize), MeasureSpec.EXACTLY); mTimePicker.measure(spec, spec); setMeasuredDimension(widthSize, heightSize); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { boolean isPortrait = getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; mLocationDirty = true; int checkboxSize = mTimePicker.is24Hour() ? 0 : mCheckBoxSize; if(isPortrait){ mHeaderRealWidth = w; mHeaderRealHeight = h - checkboxSize - w; mHeaderBackground.reset(); if(mCornerRadius == 0) mHeaderBackground.addRect(0, 0, mHeaderRealWidth, mHeaderRealHeight, Path.Direction.CW); else{ mHeaderBackground.moveTo(0, mHeaderRealHeight); mHeaderBackground.lineTo(0, mCornerRadius); mRect.set(0, 0, mCornerRadius * 2, mCornerRadius * 2); mHeaderBackground.arcTo(mRect, 180f, 90f, false); mHeaderBackground.lineTo(mHeaderRealWidth - mCornerRadius, 0); mRect.set(mHeaderRealWidth - mCornerRadius * 2, 0, mHeaderRealWidth, mCornerRadius * 2); mHeaderBackground.arcTo(mRect, 270f, 90f, false); mHeaderBackground.lineTo(mHeaderRealWidth, mHeaderRealHeight); mHeaderBackground.close(); } } else{ mHeaderRealWidth = w / 2; mHeaderRealHeight = checkboxSize > 0 ? h - checkboxSize - mContentPadding : h; mHeaderBackground.reset(); if(mCornerRadius == 0) mHeaderBackground.addRect(0, 0, mHeaderRealWidth, mHeaderRealHeight, Path.Direction.CW); else{ mHeaderBackground.moveTo(0, mHeaderRealHeight); mHeaderBackground.lineTo(0, mCornerRadius); mRect.set(0, 0, mCornerRadius * 2, mCornerRadius * 2); mHeaderBackground.arcTo(mRect, 180f, 90f, false); mHeaderBackground.lineTo(mHeaderRealWidth, 0); mHeaderBackground.lineTo(mHeaderRealWidth, mHeaderRealHeight); mHeaderBackground.close(); } } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { int childLeft = 0; int childTop = 0; int childRight = right - left; int childBottom = bottom - top; boolean isPortrait = getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; int checkboxSize = mTimePicker.is24Hour() ? 0 : mCheckBoxSize; if(isPortrait){ int paddingHorizontal = mContentPadding + mActionPadding; int paddingVertical = mContentPadding - mActionPadding; if(checkboxSize > 0) { mAmView.layout(childLeft + paddingHorizontal, childBottom - paddingVertical - checkboxSize, childLeft + paddingHorizontal + checkboxSize, childBottom - paddingVertical); mPmView.layout(childRight - paddingHorizontal - checkboxSize, childBottom - paddingVertical - checkboxSize, childRight - paddingHorizontal, childBottom - paddingVertical); } childTop += mHeaderRealHeight; childBottom -= checkboxSize; mTimePicker.layout(childLeft, childTop, childRight, childBottom); } else{ int paddingHorizontal = (childRight / 2 - mTimePicker.getMeasuredWidth()) / 2; int paddingVertical = (childBottom - mTimePicker.getMeasuredHeight()) / 2; mTimePicker.layout(childRight - paddingHorizontal - mTimePicker.getMeasuredWidth(), childTop + paddingVertical, childRight - paddingHorizontal, childTop + paddingVertical + mTimePicker.getMeasuredHeight()); if(checkboxSize > 0){ childRight = childRight / 2; paddingHorizontal = mContentPadding + mActionPadding; paddingVertical = mContentPadding - mActionPadding; mAmView.layout(childLeft + paddingHorizontal, childBottom - paddingVertical - checkboxSize, childLeft + paddingHorizontal + checkboxSize, childBottom - paddingVertical); mPmView.layout(childRight - paddingHorizontal - checkboxSize, childBottom - paddingVertical - checkboxSize, childRight - paddingHorizontal, childBottom - paddingVertical); } } } private void measureTimeText(){ if(!mLocationDirty) return; mPaint.setTextSize(mTextTimeSize); Rect bounds = new Rect(); mPaint.getTextBounds(BASE_TEXT, 0, BASE_TEXT.length(), bounds); mBaseHeight = bounds.height(); mBaseY = (mHeaderRealHeight + mBaseHeight) / 2f; float dividerWidth = mPaint.measureText(TIME_DIVIDER, 0, TIME_DIVIDER.length()); mHourWidth = mPaint.measureText(mHour, 0, mHour.length()); mMinuteWidth = mPaint.measureText(mMinute, 0, mMinute.length()); mDividerX = (mHeaderRealWidth - dividerWidth) / 2f; mHourX = mDividerX - mHourWidth; mMinuteX = mDividerX + dividerWidth; mMiddayX = mMinuteX + mMinuteWidth; mLocationDirty = false; } @Override public void draw(Canvas canvas) { super.draw(canvas); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(mTimePicker.getSelectionColor()); canvas.drawPath(mHeaderBackground, mPaint); measureTimeText(); mPaint.setTextSize(mTextTimeSize); mPaint.setColor(mTimePicker.getMode() == TimePicker.MODE_HOUR ? mTimePicker.getTextHighlightColor() : mTextTimeColor); canvas.drawText(mHour, mHourX, mBaseY, mPaint); mPaint.setColor(mTextTimeColor); canvas.drawText(TIME_DIVIDER, mDividerX, mBaseY, mPaint); mPaint.setColor(mTimePicker.getMode() == TimePicker.MODE_MINUTE ? mTimePicker.getTextHighlightColor() : mTextTimeColor); canvas.drawText(mMinute, mMinuteX, mBaseY, mPaint); if(!mTimePicker.is24Hour()) { mPaint.setTextSize(mTimePicker.getTextSize()); mPaint.setColor(mTextTimeColor); canvas.drawText(mMidday, mMiddayX, mBaseY, mPaint); } } private boolean isTouched(float left, float top, float right, float bottom, float x, float y){ return x >= left && x <= right && y >= top && y <= bottom; } @Override public boolean onTouchEvent(MotionEvent event) { boolean handled = super.onTouchEvent(event); if(handled) return handled; switch (event.getAction()){ case MotionEvent.ACTION_DOWN: if(isTouched(mHourX, mBaseY - mBaseHeight, mHourX + mHourWidth, mBaseY, event.getX(), event.getY())) return mTimePicker.getMode() == TimePicker.MODE_MINUTE; if(isTouched(mMinuteX, mBaseY - mBaseHeight, mMinuteX + mMinuteWidth, mBaseY, event.getX(), event.getY())) return mTimePicker.getMode() == TimePicker.MODE_HOUR; break; case MotionEvent.ACTION_UP: if(isTouched(mHourX, mBaseY - mBaseHeight, mHourX + mHourWidth, mBaseY, event.getX(), event.getY())) mTimePicker.setMode(TimePicker.MODE_HOUR, true); if(isTouched(mMinuteX, mBaseY - mBaseHeight, mMinuteX + mMinuteWidth, mBaseY, event.getX(), event.getY())) mTimePicker.setMode(TimePicker.MODE_MINUTE, true); break; } return false; } } public static class Builder extends Dialog.Builder implements OnTimeChangedListener { protected int mHour; protected int mMinute; public Builder(){ super(R.style.Material_App_Dialog_TimePicker_Light); Calendar cal = Calendar.getInstance(); mHour = cal.get(Calendar.HOUR_OF_DAY); mMinute = cal.get(Calendar.MINUTE); } public Builder(int hourOfDay, int minute){ this(R.style.Material_App_Dialog_TimePicker_Light, hourOfDay, minute); } public Builder(int styleId, int hourOfDay, int minute){ super(styleId); hour(hourOfDay); minute(minute); } public Builder hour(int hour){ mHour = Math.min(Math.max(hour, 0), 24); return this; } public Builder minute(int minute){ mMinute = minute; return this; } public int getHour(){ return mHour; } public int getMinute(){ return mMinute; } @Override public Dialog.Builder contentView(int layoutId) { return this; } @Override protected Dialog onBuild(Context context, int styleId) { TimePickerDialog dialog = new TimePickerDialog(context, styleId); dialog.hour(mHour) .minute(mMinute) .onTimeChangedListener(this); return dialog; } @Override public void onTimeChanged(int oldHour, int oldMinute, int newHour, int newMinute) { hour(newHour).minute(newMinute); } protected Builder(Parcel in){ super(in); } @Override protected void onWriteToParcel(Parcel dest, int flags) { dest.writeInt(mHour); dest.writeInt(mMinute); } @Override protected void onReadFromParcel(Parcel in) { mHour = in.readInt(); mMinute = in.readInt(); } public static final Parcelable.Creator<Builder> CREATOR = new Parcelable.Creator<Builder>() { public Builder createFromParcel(Parcel in) { return new Builder(in); } public Builder[] newArray(int size) { return new Builder[size]; } }; } }