Java tutorial
/* * Copyright 2017 Oleksandr Finchuk * * This file is part of ClockPlus. * * ClockPlus 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. * * ClockPlus 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 ClockPlus. If not, see <http://www.gnu.org/licenses/>. */ package com.finchuk.clock2.timers.ui; import android.animation.ObjectAnimator; import android.content.Intent; import android.graphics.drawable.Drawable; import android.support.annotation.IdRes; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.PopupMenu; import android.util.Log; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.TextView; import com.finchuk.clock2.dialogs.AddLabelDialog; import com.finchuk.clock2.list.BaseViewHolder; import com.finchuk.clock2.timers.Timer; import com.finchuk.clock2.timers.TimerController; import com.finchuk.clock2.timers.data.AsyncTimersTableUpdateHandler; import com.finchuk.clock2.util.FragmentTagUtils; import com.finchuk.clock2.dialogs.AddLabelDialogController; import com.finchuk.clock2.ringtone.playback.TimerRingtoneService; import com.finchuk.clock2.util.ProgressBarUtils; import com.finchuk.clock2.list.OnListItemInteractionListener; import com.finchuk.clock2.R; import butterknife.Bind; import butterknife.OnClick; /** * Created by Oleksandr Finchuk on 07/06/2017. */ public class TimerViewHolder extends BaseViewHolder<Timer> { private static final String TAG = "TimerViewHolder"; private final AsyncTimersTableUpdateHandler mAsyncTimersTableUpdateHandler; private TimerController mController; private ObjectAnimator mProgressAnimator; private final Drawable mStartIcon; private final Drawable mPauseIcon; private final PopupMenu mPopupMenu; private final AddLabelDialogController mAddLabelDialogController; @Bind(R.id.label) TextView mLabel; @Bind(R.id.duration) CountdownChronometer mChronometer; @Bind(R.id.seek_bar) SeekBar mSeekBar; @Bind(R.id.add_one_minute) TextView mAddOneMinute; @Bind(R.id.start_pause) ImageButton mStartPause; @Bind(R.id.stop) ImageButton mStop; @Bind(R.id.menu) ImageButton mMenuButton; public TimerViewHolder(ViewGroup parent, OnListItemInteractionListener<Timer> listener, AsyncTimersTableUpdateHandler asyncTimersTableUpdateHandler) { super(parent, R.layout.item_timer, listener); Log.d(TAG, "New TimerViewHolder"); mAsyncTimersTableUpdateHandler = asyncTimersTableUpdateHandler; mStartIcon = ContextCompat.getDrawable(getContext(), R.drawable.ic_start_24dp); mPauseIcon = ContextCompat.getDrawable(getContext(), R.drawable.ic_pause_24dp); // TODO: This is bad! Use a Controller/Presenter instead... // or simply pass in an instance of FragmentManager to the ctor. AppCompatActivity act = (AppCompatActivity) getContext(); mAddLabelDialogController = new AddLabelDialogController(act.getSupportFragmentManager(), new AddLabelDialog.OnLabelSetListener() { @Override public void onLabelSet(String label) { mController.updateLabel(label); } }); // The item layout is inflated in the super ctor, so we can safely reference our views. mPopupMenu = new PopupMenu(getContext(), mMenuButton); mPopupMenu.inflate(R.menu.menu_timer_viewholder); mPopupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.action_delete: mController.deleteTimer(); return true; } return false; } }); } @Override public void onBind(final Timer timer) { super.onBind(timer); Log.d(TAG, "Binding TimerViewHolder"); // TOneverDO: create before super mController = new TimerController(timer, mAsyncTimersTableUpdateHandler); // Items that are not in view will not be bound. If in one orientation the item was in view // and in another it is out of view, then the callback for that item will not be restored // for the new orientation. mAddLabelDialogController.tryRestoreCallback(makeTag(R.id.label)); Log.d(TAG, "timer.label() = " + timer.label()); bindLabel(timer.label()); bindChronometer(timer); bindButtonControls(timer); bindProgressBar(timer); } @OnClick(R.id.start_pause) void startPause() { mController.startPause(); } @OnClick(R.id.add_one_minute) void addOneMinute() { mController.addOneMinute(); } @OnClick(R.id.stop) void stop() { mController.stop(); getContext().stopService(new Intent(getContext(), TimerRingtoneService.class)); } @OnClick(R.id.label) void openLabelEditor() { mAddLabelDialogController.show(mLabel.getText(), makeTag(R.id.label)); } @OnClick(R.id.menu) void openMenu() { mPopupMenu.show(); } private void bindLabel(String label) { if (!label.isEmpty()) { mLabel.setText(label); } } private void bindChronometer(Timer timer) { // In case we're reusing a chronometer instance that could be running: // If the Timer instance is not running, this just guarantees the chronometer // won't tick, regardless of whether it was running. // If the Timer instance is running, we don't care whether the chronometer is // also running, because we call start() right after. Stopping it just // guarantees that, if it was running, we don't deliver another set of // concurrent messages to its handler. mChronometer.stop(); // TODO: I think we can simplify all this to just: // mChronometer.setDuration(timer.timeRemaining()) // if we make the modification to the method as // described in the Timer class. if (!timer.hasStarted()) { // Set the initial text mChronometer.setDuration(timer.duration()); } else if (timer.isRunning()) { // Re-initialize the base mChronometer.setBase(timer.endTime()); // Previously stopped, so no old messages will interfere. mChronometer.start(); } else { // Set the text as last displayed before we stopped. // When you call stop() on a Chronometer, it freezes the current text shown, // so why do we need this? While that is sufficient for a static View layout, // VH recycling will reuse the same Chronometer widget across multiple VHs, // so we would have invalid data across those VHs. // If a new VH is created, then the chronometer it contains will be in its // uninitialized state. We will always need to set the Chronometer's base // every time VHs are bound/recycled. mChronometer.setDuration(timer.timeRemaining()); } } private void bindButtonControls(Timer timer) { mStartPause.setImageDrawable(timer.isRunning() ? mPauseIcon : mStartIcon); int visibility = timer.hasStarted() ? View.VISIBLE : View.INVISIBLE; mAddOneMinute.setVisibility(visibility); mStop.setVisibility(visibility); } private void bindProgressBar(Timer timer) { final long timeRemaining = timer.timeRemaining(); double ratio = (double) timeRemaining / timer.duration(); // In case we're reusing an animator instance that could be running if (mProgressAnimator != null && mProgressAnimator.isRunning()) { mProgressAnimator.end(); } if (!timer.isRunning()) { // If our scale were 1, then casting ratio to an int will ALWAYS // truncate down to zero. // mSeekBar.setMax(100); // final int progress = (int) (100 * ratio); // mSeekBar.setProgress(progress); ProgressBarUtils.setProgress(mSeekBar, ratio); // mSeekBar.getThumb().mutate().setAlpha(progress == 0 ? 0 : 255); } else { // mSeekBar.getThumb().mutate().setAlpha(255); mProgressAnimator = ProgressBarUtils.startNewAnimator(mSeekBar, ratio, timeRemaining); } mSeekBar.getThumb().mutate().setAlpha(timeRemaining <= 0 ? 0 : 255); } private String makeTag(@IdRes int viewId) { return FragmentTagUtils.makeTag(TimerViewHolder.class, viewId, getItemId()); } }