Skip to content

Commit 0c5540d

Browse files
committed
Backport Android N Chronometer
1 parent 14ba5bd commit 0c5540d

3 files changed

Lines changed: 334 additions & 8 deletions

File tree

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
/*
2+
* Copyright (C) 2008 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.akanework.gramophone.ui.components;
18+
19+
import android.content.Context;
20+
import android.content.Intent;
21+
import android.net.Uri;
22+
import android.os.Build;
23+
import android.os.SystemClock;
24+
import android.text.format.DateUtils;
25+
import android.util.AttributeSet;
26+
import android.util.Log;
27+
import android.view.View;
28+
29+
import androidx.annotation.DeprecatedSinceApi;
30+
import androidx.appcompat.widget.AppCompatTextView;
31+
32+
import java.util.Formatter;
33+
import java.util.IllegalFormatException;
34+
import java.util.Locale;
35+
36+
/**
37+
* Class that implements a simple timer.
38+
* <p>
39+
* You can give it a start time in the {@link SystemClock#elapsedRealtime} timebase,
40+
* and it counts up from that, or if you don't give it a base time, it will use the
41+
* time at which you call {@link #start}.
42+
*
43+
* <p>The timer can also count downward towards the base time by
44+
* setting {@link #setCountDown(boolean)} to true.
45+
*
46+
* <p>By default, it will display the current
47+
* timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat}
48+
* to format the timer value into an arbitrary string.
49+
*
50+
* @attr ref android.R.styleable#Chronometer_format
51+
* @attr ref android.R.styleable#Chronometer_countDown
52+
*/
53+
@SuppressWarnings("unused")
54+
@DeprecatedSinceApi(api = Build.VERSION_CODES.N, message = "Use android.widget.Chronometer instead")
55+
public class Chronometer extends AppCompatTextView {
56+
private static final String TAG = "Chronometer";
57+
58+
/**
59+
* A callback that notifies when the chronometer has incremented on its own.
60+
*/
61+
public interface OnChronometerTickListener {
62+
63+
/**
64+
* Notification that the chronometer has changed.
65+
*/
66+
void onChronometerTick(Chronometer chronometer);
67+
68+
}
69+
70+
private long mBase;
71+
private long mNow; // the currently displayed time
72+
private boolean mVisible;
73+
private boolean mStarted;
74+
private boolean mRunning;
75+
private boolean mLogged;
76+
private String mFormat;
77+
private Formatter mFormatter;
78+
private Locale mFormatterLocale;
79+
private final Object[] mFormatterArgs = new Object[1];
80+
private StringBuilder mFormatBuilder;
81+
private OnChronometerTickListener mOnChronometerTickListener;
82+
private final StringBuilder mRecycle = new StringBuilder(8);
83+
private boolean mCountDown;
84+
85+
/**
86+
* Initialize this Chronometer object.
87+
* Sets the base to the current time.
88+
*/
89+
public Chronometer(Context context) {
90+
this(context, null, 0);
91+
}
92+
93+
/**
94+
* Initialize with standard view layout information.
95+
* Sets the base to the current time.
96+
*/
97+
public Chronometer(Context context, AttributeSet attrs) {
98+
this(context, attrs, 0);
99+
}
100+
101+
/**
102+
* Initialize with standard view layout information and style.
103+
* Sets the base to the current time.
104+
*/
105+
public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) {
106+
super(context, attrs, defStyleAttr);
107+
mBase = SystemClock.elapsedRealtime();
108+
updateText(mBase);
109+
}
110+
111+
/**
112+
* Set this view to count down to the base instead of counting up from it.
113+
*
114+
* @param countDown whether this view should count down
115+
*
116+
* @see #setBase(long)
117+
*/
118+
public void setCountDown(boolean countDown) {
119+
mCountDown = countDown;
120+
updateText(SystemClock.elapsedRealtime());
121+
}
122+
123+
/**
124+
* @return whether this view counts down
125+
*
126+
* @see #setCountDown(boolean)
127+
*/
128+
public boolean isCountDown() {
129+
return mCountDown;
130+
}
131+
132+
/**
133+
* @return whether this is the final countdown
134+
*/
135+
public boolean isTheFinalCountDown() {
136+
try {
137+
getContext().startActivity(
138+
new Intent(Intent.ACTION_VIEW, Uri.parse("https://youtu.be/9jK-NcRmVcw"))
139+
.addCategory(Intent.CATEGORY_BROWSABLE)
140+
.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT));
141+
return true;
142+
} catch (Exception e) {
143+
return false;
144+
}
145+
}
146+
147+
/**
148+
* Set the time that the count-up timer is in reference to.
149+
*
150+
* @param base Use the {@link SystemClock#elapsedRealtime} time base.
151+
*/
152+
public void setBase(long base) {
153+
mBase = base;
154+
dispatchChronometerTick();
155+
updateText(SystemClock.elapsedRealtime());
156+
}
157+
158+
/**
159+
* Return the base time as set through {@link #setBase}.
160+
*/
161+
public long getBase() {
162+
return mBase;
163+
}
164+
165+
/**
166+
* Sets the format string used for display. The Chronometer will display
167+
* this string, with the first "%s" replaced by the current timer value in
168+
* "MM:SS" or "H:MM:SS" form.
169+
* <p>
170+
* If the format string is null, or if you never call setFormat(), the
171+
* Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS"
172+
* form.
173+
*
174+
* @param format the format string.
175+
*/
176+
public void setFormat(String format) {
177+
mFormat = format;
178+
if (format != null && mFormatBuilder == null) {
179+
mFormatBuilder = new StringBuilder(format.length() * 2);
180+
}
181+
}
182+
183+
/**
184+
* Returns the current format string as set through {@link #setFormat}.
185+
*/
186+
public String getFormat() {
187+
return mFormat;
188+
}
189+
190+
/**
191+
* Sets the listener to be called when the chronometer changes.
192+
*
193+
* @param listener The listener.
194+
*/
195+
public void setOnChronometerTickListener(OnChronometerTickListener listener) {
196+
mOnChronometerTickListener = listener;
197+
}
198+
199+
/**
200+
* @return The listener (may be null) that is listening for chronometer change
201+
* events.
202+
*/
203+
public OnChronometerTickListener getOnChronometerTickListener() {
204+
return mOnChronometerTickListener;
205+
}
206+
207+
/**
208+
* Start counting up. This does not affect the base as set from {@link #setBase}, just
209+
* the view display.
210+
* <p>
211+
* Chronometer works by regularly scheduling messages to the handler, even when the
212+
* Widget is not visible. To make sure resource leaks do not occur, the user should
213+
* make sure that each start() call has a reciprocal call to {@link #stop}.
214+
*/
215+
public void start() {
216+
mStarted = true;
217+
updateRunning();
218+
}
219+
220+
/**
221+
* Stop counting up. This does not affect the base as set from {@link #setBase}, just
222+
* the view display.
223+
* <p>
224+
* This stops the messages to the handler, effectively releasing resources that would
225+
* be held as the chronometer is running, via {@link #start}.
226+
*/
227+
public void stop() {
228+
mStarted = false;
229+
updateRunning();
230+
}
231+
232+
@Override
233+
protected void onDetachedFromWindow() {
234+
super.onDetachedFromWindow();
235+
mVisible = false;
236+
updateRunning();
237+
}
238+
239+
@Override
240+
protected void onWindowVisibilityChanged(int visibility) {
241+
super.onWindowVisibilityChanged(visibility);
242+
mVisible = visibility == VISIBLE;
243+
updateRunning();
244+
}
245+
246+
@Override
247+
protected void onVisibilityChanged(View changedView, int visibility) {
248+
super.onVisibilityChanged(changedView, visibility);
249+
updateRunning();
250+
}
251+
252+
private synchronized void updateText(long now) {
253+
mNow = now;
254+
long seconds = Math.round((mCountDown ? mBase - now - 499 : now - mBase) / 1000f);
255+
boolean negative = false;
256+
if (seconds < 0) {
257+
seconds = -seconds;
258+
negative = true;
259+
}
260+
String text = DateUtils.formatElapsedTime(mRecycle, seconds);
261+
if (negative) {
262+
text = "-" + text;
263+
}
264+
265+
if (mFormat != null) {
266+
Locale loc = Locale.getDefault();
267+
if (mFormatter == null || !loc.equals(mFormatterLocale)) {
268+
mFormatterLocale = loc;
269+
mFormatter = new Formatter(mFormatBuilder, loc);
270+
}
271+
mFormatBuilder.setLength(0);
272+
mFormatterArgs[0] = text;
273+
try {
274+
mFormatter.format(mFormat, mFormatterArgs);
275+
text = mFormatBuilder.toString();
276+
} catch (IllegalFormatException ex) {
277+
if (!mLogged) {
278+
Log.w(TAG, "Illegal format string: " + mFormat);
279+
mLogged = true;
280+
}
281+
}
282+
}
283+
setText(text);
284+
}
285+
286+
private void updateRunning() {
287+
boolean running = mVisible && mStarted && isShown();
288+
if (running != mRunning) {
289+
if (running) {
290+
updateText(SystemClock.elapsedRealtime());
291+
dispatchChronometerTick();
292+
postTickOnNextSecond();
293+
} else {
294+
removeCallbacks(mTickRunnable);
295+
}
296+
mRunning = running;
297+
}
298+
}
299+
300+
private final Runnable mTickRunnable = new Runnable() {
301+
@Override
302+
public void run() {
303+
if (mRunning) {
304+
updateText(SystemClock.elapsedRealtime());
305+
dispatchChronometerTick();
306+
postTickOnNextSecond();
307+
}
308+
}
309+
};
310+
311+
private void postTickOnNextSecond() {
312+
long nowMillis = mNow;
313+
long delayMillis;
314+
if (mCountDown) {
315+
delayMillis = (mBase - nowMillis) % 1000;
316+
if (delayMillis <= 0) {
317+
delayMillis += 1000;
318+
}
319+
} else {
320+
delayMillis = 1000 - (Math.abs(nowMillis - mBase) % 1000);
321+
}
322+
// Aim for 1 millisecond into the next second so we don't update exactly on the second
323+
delayMillis++;
324+
postDelayed(mTickRunnable, delayMillis);
325+
}
326+
327+
void dispatchChronometerTick() {
328+
if (mOnChronometerTickListener != null) {
329+
mOnChronometerTickListener.onChronometerTick(this);
330+
}
331+
}
332+
}

app/src/main/java/org/akanework/gramophone/ui/components/PlaylistQueueSheet.kt

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package org.akanework.gramophone.ui.components
22

33
import android.content.Context
4-
import android.os.Build
54
import android.os.SystemClock
65
import android.view.View
76
import android.widget.Button
8-
import android.widget.Chronometer
97
import androidx.core.graphics.Insets
108
import androidx.core.view.ViewCompat
119
import androidx.core.view.WindowInsetsCompat
@@ -38,11 +36,7 @@ class PlaylistQueueSheet(
3836
setContentView(R.layout.playlist_bottom_sheet)
3937
behavior.state = BottomSheetBehavior.STATE_EXPANDED
4038
durationView = findViewById(R.id.duration)!!
41-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
42-
durationView.isCountDown = true
43-
} else {
44-
//TODO(ASAP) ???
45-
}
39+
durationView.isCountDown = true
4640
val recyclerView = findViewById<MyRecyclerView>(R.id.recyclerview)!!
4741
ViewCompat.setOnApplyWindowInsetsListener(recyclerView) { v, ic ->
4842
val i = ic.getInsets(

app/src/main/res/layout/playlist_bottom_sheet.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
app:layout_constraintStart_toEndOf="@id/clearQueue"
4444
app:layout_constraintTop_toBottomOf="@id/bottomSheetDragHandle" />
4545

46-
<Chronometer
46+
<org.akanework.gramophone.ui.components.Chronometer
4747
android:id="@+id/duration"
4848
android:layout_width="wrap_content"
4949
android:layout_height="wrap_content"

0 commit comments

Comments
 (0)