Java tutorial
/** Copyright (C) 2017-2018 Forrest Guice This file is part of SuntimesWidget. SuntimesWidget is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SuntimesWidget is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with SuntimesWidget. If not, see <http://www.gnu.org/licenses/>. */ package com.forrestguice.suntimeswidget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Paint; import android.graphics.Typeface; import android.os.Bundle; import android.support.v4.content.ContextCompat; import android.text.SpannableString; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.ViewFlipper; import com.forrestguice.suntimeswidget.calculator.SuntimesEquinoxSolsticeDataset; import com.forrestguice.suntimeswidget.settings.AppSettings; import com.forrestguice.suntimeswidget.settings.WidgetSettings; import java.util.ArrayList; import java.util.Calendar; @SuppressWarnings("Convert2Diamond") public class EquinoxView extends LinearLayout { public static final String KEY_UI_USERSWAPPEDCARD = "userSwappedEquinoxCard"; public static final String KEY_UI_CARDISNEXTYEAR = "equinoxCardIsNextYear"; public static final String KEY_UI_MINIMIZED = "equinoxIsMinimized"; private SuntimesUtils utils = new SuntimesUtils(); private boolean userSwappedCard = false; private boolean isRtl = false; private boolean minimized = false; private boolean centered = false; private WidgetSettings.TrackingMode trackingMode = WidgetSettings.TrackingMode.SOONEST; private TextView empty; private ViewFlipper flipper; // flip between thisYear, nextYear private Animation anim_card_outNext, anim_card_inNext, anim_card_outPrev, anim_card_inPrev; private ImageButton btn_flipperNext_thisYear, btn_flipperPrev_thisYear; private ImageButton btn_flipperNext_nextYear, btn_flipperPrev_nextYear; private TextView titleThisYear, titleNextYear; private EquinoxNote note_equinox_vernal, note_solstice_summer, note_equinox_autumnal, note_solstice_winter; // this year private EquinoxNote note_equinox_vernal2, note_solstice_summer2, note_equinox_autumnal2, note_solstice_winter2; // and next year private ArrayList<EquinoxNote> notes; public EquinoxView(Context context) { super(context); init(context, null); } public EquinoxView(Context context, AttributeSet attrs) { super(context, attrs); applyAttributes(context, attrs); init(context, attrs); } private void applyAttributes(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EquinoxView, 0, 0); try { setMinimized(a.getBoolean(R.styleable.EquinoxView_minimized, false)); } finally { a.recycle(); } } private void init(Context context, AttributeSet attrs) { initLocale(context); initColors(context); LayoutInflater.from(context).inflate(R.layout.layout_view_equinox, this, true); if (attrs != null) { LinearLayout.LayoutParams lp = generateLayoutParams(attrs); centered = ((lp.gravity == Gravity.CENTER) || (lp.gravity == Gravity.CENTER_HORIZONTAL)); } empty = (TextView) findViewById(R.id.txt_empty); flipper = (ViewFlipper) findViewById(R.id.info_equinoxsolstice_flipper); flipper.setOnTouchListener(cardTouchListener); notes = new ArrayList<EquinoxNote>(); RelativeLayout thisYear = (RelativeLayout) findViewById(R.id.info_equinoxsolstice_thisyear); if (thisYear != null) { btn_flipperNext_thisYear = (ImageButton) thisYear.findViewById(R.id.info_time_nextbtn); btn_flipperNext_thisYear.setOnClickListener(onNextCardClick); btn_flipperNext_thisYear.setOnTouchListener(createButtonListener(btn_flipperNext_thisYear)); btn_flipperPrev_thisYear = (ImageButton) thisYear.findViewById(R.id.info_time_prevbtn); btn_flipperPrev_thisYear.setVisibility(View.GONE); titleThisYear = (TextView) thisYear.findViewById(R.id.text_title); TextView txt_equinox_vernal_label = (TextView) thisYear .findViewById(R.id.text_date_equinox_vernal_label); TextView txt_equinox_vernal = (TextView) thisYear.findViewById(R.id.text_date_equinox_vernal); TextView txt_equinox_vernal_note = (TextView) thisYear.findViewById(R.id.text_date_equinox_vernal_note); note_equinox_vernal = addNote(txt_equinox_vernal_label, txt_equinox_vernal, txt_equinox_vernal_note, 0); TextView txt_solstice_summer_label = (TextView) thisYear .findViewById(R.id.text_date_solstice_summer_label); TextView txt_solstice_summer = (TextView) thisYear.findViewById(R.id.text_date_solstice_summer); TextView txt_solstice_summer_note = (TextView) thisYear .findViewById(R.id.text_date_solstice_summer_note); note_solstice_summer = addNote(txt_solstice_summer_label, txt_solstice_summer, txt_solstice_summer_note, 0); TextView txt_equinox_autumnal_label = (TextView) thisYear .findViewById(R.id.text_date_equinox_autumnal_label); TextView txt_equinox_autumnal = (TextView) thisYear.findViewById(R.id.text_date_equinox_autumnal); TextView txt_equinox_autumnal_note = (TextView) thisYear .findViewById(R.id.text_date_equinox_autumnal_note); note_equinox_autumnal = addNote(txt_equinox_autumnal_label, txt_equinox_autumnal, txt_equinox_autumnal_note, 0); TextView txt_solstice_winter_label = (TextView) thisYear .findViewById(R.id.text_date_solstice_winter_label); TextView txt_solstice_winter = (TextView) thisYear.findViewById(R.id.text_date_solstice_winter); TextView txt_solstice_winter_note = (TextView) thisYear .findViewById(R.id.text_date_solstice_winter_note); note_solstice_winter = addNote(txt_solstice_winter_label, txt_solstice_winter, txt_solstice_winter_note, 0); if (centered) { FrameLayout.LayoutParams lpThisYear = (FrameLayout.LayoutParams) thisYear.getLayoutParams(); lpThisYear.gravity = Gravity.CENTER_HORIZONTAL; thisYear.setLayoutParams(lpThisYear); } } RelativeLayout nextYear = (RelativeLayout) findViewById(R.id.info_equinoxsolstice_nextyear); if (nextYear != null) { btn_flipperNext_nextYear = (ImageButton) nextYear.findViewById(R.id.info_time_nextbtn); btn_flipperNext_nextYear.setVisibility(View.GONE); btn_flipperPrev_nextYear = (ImageButton) nextYear.findViewById(R.id.info_time_prevbtn); btn_flipperPrev_nextYear.setOnClickListener(onPrevCardClick); btn_flipperPrev_nextYear.setOnTouchListener(createButtonListener(btn_flipperPrev_nextYear)); titleNextYear = (TextView) nextYear.findViewById(R.id.text_title); TextView txt_equinox_vernal2_label = (TextView) nextYear .findViewById(R.id.text_date_equinox_vernal_label); TextView txt_equinox_vernal2 = (TextView) nextYear.findViewById(R.id.text_date_equinox_vernal); TextView txt_equinox_vernal2_note = (TextView) nextYear .findViewById(R.id.text_date_equinox_vernal_note); note_equinox_vernal2 = addNote(txt_equinox_vernal2_label, txt_equinox_vernal2, txt_equinox_vernal2_note, 1); TextView txt_solstice_summer2_label = (TextView) nextYear .findViewById(R.id.text_date_solstice_summer_label); TextView txt_solstice_summer2 = (TextView) nextYear.findViewById(R.id.text_date_solstice_summer); TextView txt_solstice_summer2_note = (TextView) nextYear .findViewById(R.id.text_date_solstice_summer_note); note_solstice_summer2 = addNote(txt_solstice_summer2_label, txt_solstice_summer2, txt_solstice_summer2_note, 1); TextView txt_equinox_autumnal2_label = (TextView) nextYear .findViewById(R.id.text_date_equinox_autumnal_label); TextView txt_equinox_autumnal2 = (TextView) nextYear.findViewById(R.id.text_date_equinox_autumnal); TextView txt_equinox_autumnal2_note = (TextView) nextYear .findViewById(R.id.text_date_equinox_autumnal_note); note_equinox_autumnal2 = addNote(txt_equinox_autumnal2_label, txt_equinox_autumnal2, txt_equinox_autumnal2_note, 1); TextView txt_solstice_winter2_label = (TextView) nextYear .findViewById(R.id.text_date_solstice_winter_label); TextView txt_solstice_winter2 = (TextView) nextYear.findViewById(R.id.text_date_solstice_winter); TextView txt_solstice_winter2_note = (TextView) nextYear .findViewById(R.id.text_date_solstice_winter_note); note_solstice_winter2 = addNote(txt_solstice_winter2_label, txt_solstice_winter2, txt_solstice_winter2_note, 1); if (centered) { FrameLayout.LayoutParams lpNextYear = (FrameLayout.LayoutParams) nextYear.getLayoutParams(); lpNextYear.gravity = Gravity.CENTER_HORIZONTAL; nextYear.setLayoutParams(lpNextYear); } } if (isInEditMode()) { updateViews(context, null); } } private View.OnTouchListener createButtonListener(final ImageButton button) { return new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { if (button != null) { if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { button.setColorFilter(ContextCompat.getColor(getContext(), R.color.btn_tint_pressed)); performClick(); } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) { button.setColorFilter(null); } } return false; } }; } private int noteColor; //, springColor, summerColor, fallColor, winterColor; private void initColors(Context context) { int[] colorAttrs = { android.R.attr.textColorPrimary }; //, R.attr.springColor, R.attr.summerColor, R.attr.fallColor, R.attr.winterColor }; TypedArray typedArray = context.obtainStyledAttributes(colorAttrs); int def = R.color.transparent; noteColor = ContextCompat.getColor(context, typedArray.getResourceId(0, def)); //springColor = ContextCompat.getColor(context, typedArray.getResourceId(1, def)); //summerColor = ContextCompat.getColor(context, typedArray.getResourceId(2, def)); //fallColor = ContextCompat.getColor(context, typedArray.getResourceId(3, def)); //winterColor = ContextCompat.getColor(context, typedArray.getResourceId(4, def)); typedArray.recycle(); } public void initLocale(Context context) { isRtl = AppSettings.isLocaleRtl(context); initAnimations(context); } private void initAnimations(Context context) { anim_card_inNext = AnimationUtils.loadAnimation(context, R.anim.fade_in); anim_card_inPrev = AnimationUtils.loadAnimation(context, R.anim.fade_in); anim_card_outNext = AnimationUtils.loadAnimation(context, R.anim.fade_out); anim_card_outPrev = AnimationUtils.loadAnimation(context, R.anim.fade_out); } private EquinoxNote addNote(TextView labelView, TextView timeView, TextView noteView, int pageIndex) { EquinoxNote note = new EquinoxNote(labelView, timeView, noteView, pageIndex); notes.add(note); return note; } public void setTrackingMode(WidgetSettings.TrackingMode mode) { trackingMode = mode; } public WidgetSettings.TrackingMode getTrackingMode() { return trackingMode; } public void setMinimized(boolean value) { this.minimized = value; } public boolean isMinimized() { return minimized; } private EquinoxNote findSoonestNote(Calendar now) { return findClosestNote(now, true); } private EquinoxNote findClosestNote(Calendar now) { return findClosestNote(now, false); } private EquinoxNote findClosestNote(Calendar now, boolean upcoming) { if (notes == null || now == null) { return null; } EquinoxNote closest = null; long timeDeltaMin = Long.MAX_VALUE; for (EquinoxNote note : notes) { Calendar noteTime = note.getTime(); if (noteTime != null) { if (upcoming && !noteTime.after(now)) continue; long timeDelta = Math.abs(noteTime.getTimeInMillis() - now.getTimeInMillis()); if (timeDelta < timeDeltaMin) { timeDeltaMin = timeDelta; closest = note; } } } return closest; } private void showNextPrevButtons(boolean show) { if (show) { btn_flipperNext_thisYear.setVisibility(View.VISIBLE); btn_flipperPrev_thisYear.setVisibility(View.GONE); btn_flipperNext_nextYear.setVisibility(View.GONE); btn_flipperPrev_nextYear.setVisibility(View.VISIBLE); } else { btn_flipperNext_thisYear.setVisibility(View.GONE); btn_flipperPrev_thisYear.setVisibility(View.GONE); btn_flipperNext_nextYear.setVisibility(View.GONE); btn_flipperPrev_nextYear.setVisibility(View.GONE); } } private void showTitle(boolean show) { titleThisYear.setVisibility(show ? View.VISIBLE : View.GONE); titleNextYear.setVisibility(show ? View.VISIBLE : View.GONE); } private void showEmptyView(boolean show) { empty.setVisibility(show ? View.VISIBLE : View.GONE); flipper.setVisibility(show ? View.GONE : View.VISIBLE); } protected void updateViews(Context context, SuntimesEquinoxSolsticeDataset data) { showTitle(!minimized); showNextPrevButtons(!minimized); showEmptyView(false); if (isInEditMode()) { if (minimized) { for (int i = 1; i < notes.size(); i++) { EquinoxNote note = notes.get(i); note.setVisible(false); } } return; } if (data == null) { for (EquinoxNote note : notes) { note.setEnabled(false); note.updateTime(context, null); note.updateNote(context, null); if (minimized) { note.setVisible(false); } } return; } if (data.isCalculated() && data.isImplemented()) { SuntimesUtils.TimeDisplayText thisYear = utils.calendarDateYearDisplayString(context, data.dataEquinoxVernal.eventCalendarThisYear()); titleThisYear.setText(thisYear.toString()); SuntimesUtils.TimeDisplayText nextYear = utils.calendarDateYearDisplayString(context, data.dataEquinoxVernal.eventCalendarOtherYear()); titleNextYear.setText(nextYear.toString()); boolean showSeconds = WidgetSettings.loadShowSecondsPref(context, 0); note_equinox_vernal.updateTime(context, data.dataEquinoxVernal.eventCalendarThisYear(), showSeconds); note_equinox_autumnal.updateTime(context, data.dataEquinoxAutumnal.eventCalendarThisYear(), showSeconds); note_solstice_summer.updateTime(context, data.dataSolsticeSummer.eventCalendarThisYear(), showSeconds); note_solstice_winter.updateTime(context, data.dataSolsticeWinter.eventCalendarThisYear(), showSeconds); note_equinox_vernal2.updateTime(context, data.dataEquinoxVernal.eventCalendarOtherYear(), showSeconds); note_equinox_autumnal2.updateTime(context, data.dataEquinoxAutumnal.eventCalendarOtherYear(), showSeconds); note_solstice_summer2.updateTime(context, data.dataSolsticeSummer.eventCalendarOtherYear(), showSeconds); note_solstice_winter2.updateTime(context, data.dataSolsticeWinter.eventCalendarOtherYear(), showSeconds); for (EquinoxNote note : notes) { note.setEnabled(); note.updateNote(context, data.now()); note.setVisible(!minimized); } EquinoxNote nextNote = (trackingMode == WidgetSettings.TrackingMode.SOONEST ? findSoonestNote(data.now()) : findClosestNote(data.now())); if (nextNote == null) { nextNote = notes.get(0); } if (!userSwappedCard) { flipper.setDisplayedChild(nextNote.pageIndex); } nextNote.setVisible(true); nextNote.setHighlighted(true); } else { if (minimized) { for (EquinoxNote note : notes) { note.setVisible(false); } } else { showEmptyView(true); } } } public boolean saveState(Bundle bundle) { boolean cardIsNextYear = (flipper.getDisplayedChild() != 0); bundle.putBoolean(EquinoxView.KEY_UI_CARDISNEXTYEAR, cardIsNextYear); bundle.putBoolean(EquinoxView.KEY_UI_USERSWAPPEDCARD, userSwappedCard); bundle.putBoolean(EquinoxView.KEY_UI_MINIMIZED, minimized); Log.d("DEBUG", "EquinoxView saveState :: nextyear:" + cardIsNextYear + " :: swapped:" + userSwappedCard + " :: minimized:" + minimized); return true; } public void loadState(Bundle bundle) { boolean cardIsNextYear = bundle.getBoolean(EquinoxView.KEY_UI_CARDISNEXTYEAR, false); flipper.setDisplayedChild((cardIsNextYear ? 1 : 0)); userSwappedCard = bundle.getBoolean(EquinoxView.KEY_UI_USERSWAPPEDCARD, false); minimized = bundle.getBoolean(EquinoxView.KEY_UI_MINIMIZED, minimized); Log.d("DEBUG", "EquinoxView loadState :: nextyear: " + cardIsNextYear + " :: swapped:" + userSwappedCard + " :: minimized:" + minimized); } public boolean showNextCard() { if (hasNextCard()) { flipper.setOutAnimation(anim_card_outNext); flipper.setInAnimation(anim_card_inNext); flipper.showNext(); } return true; } public boolean hasNextCard() { int current = flipper.getDisplayedChild(); return ((current + 1) < flipper.getChildCount()); } public boolean showPreviousCard() { if (hasPreviousCard()) { flipper.setOutAnimation(anim_card_outPrev); flipper.setInAnimation(anim_card_inPrev); flipper.showPrevious(); } return true; } public boolean hasPreviousCard() { int current = flipper.getDisplayedChild(); int prev = current - 1; return (prev >= 0); } public void setOnClickListener(View.OnClickListener listener) { flipper.setOnClickListener(listener); } public void setOnLongClickListener(View.OnLongClickListener listener) { flipper.setOnLongClickListener(listener); } private View.OnClickListener onNextCardClick = new View.OnClickListener() { @Override public void onClick(View view) { userSwappedCard = showNextCard(); } }; private View.OnClickListener onPrevCardClick = new View.OnClickListener() { @Override public void onClick(View view) { userSwappedCard = showPreviousCard(); } }; /** * */ private View.OnTouchListener cardTouchListener = new View.OnTouchListener() { public int MOVE_SENSITIVITY = 150; public int FLING_SENSITIVITY = 25; public float firstTouchX, secondTouchX; @Override public boolean onTouch(View view, MotionEvent event) { if (minimized) return false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: firstTouchX = event.getX(); performClick(); break; case MotionEvent.ACTION_UP: secondTouchX = event.getX(); if ((secondTouchX - firstTouchX) > FLING_SENSITIVITY) { // swipe right; back to previous view userSwappedCard = (isRtl ? showNextCard() : showPreviousCard()); } else if (firstTouchX - secondTouchX > FLING_SENSITIVITY) { // swipe left; advance to next view userSwappedCard = (isRtl ? showPreviousCard() : showNextCard()); } else { // swipe cancel; reset current view final View currentView = flipper.getCurrentView(); currentView.layout(0, currentView.getTop(), currentView.getWidth(), currentView.getBottom()); } break; case MotionEvent.ACTION_MOVE: float currentTouchX = event.getX(); int moveDelta = (int) (currentTouchX - firstTouchX); boolean isSwipeRight = (moveDelta > 0); final View currentView = flipper.getCurrentView(); int currentIndex = flipper.getDisplayedChild(); int otherIndex; if (isRtl) { otherIndex = (isSwipeRight ? currentIndex + 1 : currentIndex - 1); } else { otherIndex = (isSwipeRight ? currentIndex - 1 : currentIndex + 1); } if (otherIndex >= 0 && otherIndex < flipper.getChildCount()) { // in-between child views; flip between them currentView.layout(moveDelta, currentView.getTop(), moveDelta + currentView.getWidth(), currentView.getBottom()); // extended movement; manually trigger swipe/fling if (moveDelta > MOVE_SENSITIVITY || moveDelta < MOVE_SENSITIVITY * -1) { event.setAction(MotionEvent.ACTION_UP); return onTouch(view, event); } } //else { // at-a-boundary (the first/last view); // TODO: animate somehow to let user know there aren't additional views //} break; } return false; } }; /** * EquinoxNote */ protected class EquinoxNote { protected TextView labelView, timeView, noteView; protected Calendar time, now; protected boolean highlighted; protected int pageIndex = 0; public EquinoxNote(TextView labelView, TextView timeView, TextView noteView, int pageIndex) { this.labelView = labelView; this.timeView = timeView; this.noteView = noteView; this.pageIndex = pageIndex; if (this.timeView != null) { this.timeView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (minimized) { flipper.performClick(); } } }); } } public void updateTime(Context context, Calendar time) { updateTime(context, time, false); } public void updateTime(Context context, Calendar time, boolean showSeconds) { this.time = time; if (timeView != null) { SuntimesUtils.TimeDisplayText timeText = utils.calendarDateTimeDisplayString(context, time, showSeconds); timeView.setText(timeText.toString()); } } public void updateNote(Context context, Calendar now) { this.now = now; if (noteView != null) { if (now != null && time != null) { String noteText = utils.timeDeltaDisplayString(now.getTime(), time.getTime()).toString(); if (time.before(Calendar.getInstance())) { String noteString = context.getString(R.string.ago, noteText); SpannableString noteSpan = (noteView.isEnabled() ? SuntimesUtils.createBoldColorSpan(noteString, noteText, noteColor) : SuntimesUtils.createBoldSpan(noteString, noteText)); noteView.setText(noteSpan); } else { String noteString = context.getString(R.string.hence, noteText); SpannableString noteSpan = (noteView.isEnabled() ? SuntimesUtils.createBoldColorSpan(noteString, noteText, noteColor) : SuntimesUtils.createBoldSpan(noteString, noteText)); noteView.setText(noteSpan); } } else { noteView.setText(""); } } } public void setHighlighted(boolean highlighted) { this.highlighted = highlighted; //highlight(labelView, highlighted); highlight(timeView, highlighted); setEnabled(true); } private void highlight(TextView view, boolean value) { if (view != null) { if (value) { view.setTypeface(view.getTypeface(), Typeface.BOLD); view.setPaintFlags(view.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); } else { view.setTypeface(view.getTypeface(), Typeface.NORMAL); view.setPaintFlags(view.getPaintFlags() & (~Paint.UNDERLINE_TEXT_FLAG)); } } } public void setEnabled(boolean value) { labelView.setEnabled(value); timeView.setEnabled(value); noteView.setEnabled(value); } public void setEnabled() { if (time != null) { setEnabled(time.after(Calendar.getInstance())); } else { setEnabled(false); } } public void setVisible(boolean visible) { labelView.setVisibility(visible ? View.VISIBLE : View.GONE); timeView.setVisibility(visible ? View.VISIBLE : View.GONE); noteView.setVisibility(visible ? View.VISIBLE : View.GONE); } public Calendar getTime() { return time; } } }