1 /***
2 * Ambient - A music player for the Android platform
3 Copyright (C) 2007 Martin Vysny
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 package sk.baka.ambient.views;
20
21 import java.text.CharacterIterator;
22 import java.text.StringCharacterIterator;
23 import java.util.Collection;
24 import java.util.StringTokenizer;
25
26 import sk.baka.ambient.R;
27 import android.app.AlertDialog;
28 import android.app.Dialog;
29 import android.content.Context;
30 import android.content.DialogInterface;
31 import android.content.DialogInterface.OnCancelListener;
32 import android.graphics.Bitmap;
33 import android.graphics.Paint;
34 import android.graphics.Point;
35 import android.graphics.Paint.FontMetricsInt;
36 import android.text.Editable;
37 import android.text.SpannableStringBuilder;
38 import android.text.Spanned;
39 import android.text.TextWatcher;
40 import android.text.style.ImageSpan;
41 import android.text.util.Linkify;
42 import android.view.MotionEvent;
43 import android.view.View;
44 import android.view.View.OnClickListener;
45 import android.widget.Button;
46 import android.widget.EditText;
47 import android.widget.TextView;
48
49 /***
50 * Utility methods for views. Not thread safe.
51 *
52 * @author Martin Vysny
53 */
54 public final class ViewUtils {
55 /***
56 * Clones given point
57 *
58 * @param point
59 * the point to clone
60 * @return cloned point
61 */
62 public static final Point clone(final Point point) {
63 return new Point(point.x, point.y);
64 }
65
66 /***
67 * {@link Bitmap#recycle() Recycles} given collection of bitmap. The
68 * collection is emptied afterwards.
69 *
70 * @param bitmaps
71 * bitmaps to recycle.
72 */
73 public static void recycleBitmaps(final Collection<Bitmap> bitmaps) {
74 for (final Bitmap b : bitmaps) {
75 if (!b.isRecycled()) {
76 b.recycle();
77 }
78 }
79 bitmaps.clear();
80 }
81
82 /***
83 * Here the temporary results of all translate* methods are stored. This is
84 * just a cache, to prevent an array creation on each call.
85 */
86 final int[] translatedPoint = new int[] { 0, 0 };
87
88 /***
89 * Translates given point into target view's coordinate system and stores
90 * the result into the {@link #translated} point.
91 *
92 * @param point
93 * the point to translate. Will not get modified. If
94 * <code>null</code> then the {@link #translated} point data will
95 * be taken instead.
96 * @param view
97 * the point belongs to the coordinate system of this view. If
98 * <code>null</code> then the point is an absolute screen
99 * position.
100 * @param targetView
101 * translate the view to this view coordinate system. If
102 * <code>null</code> then the point will be returned as an
103 * absolute screen position.
104 */
105 public void translateCoordinates(final Point point, final View view,
106 final View targetView) {
107 if (point != null) {
108 translated.x = point.x;
109 translated.y = point.y;
110 }
111 if (view == targetView)
112 return;
113 if (view != null) {
114
115 view.getLocationOnScreen(translatedPoint);
116 translated.x += translatedPoint[0];
117 translated.y += translatedPoint[1];
118 }
119 if (targetView != null) {
120 targetView.getLocationOnScreen(translatedPoint);
121 translated.x -= translatedPoint[0];
122 translated.y -= translatedPoint[1];
123 }
124 }
125
126 /***
127 * A result of translate* operations is stored here.
128 */
129 public final Point translated = new Point();
130
131 /***
132 * Translates given point into root view's coordinate system. Does nothing
133 * if the supplied view is <code>null</code>.
134 *
135 * @param point
136 * the point to translate. Will not get modified.
137 * @param view
138 * the point belongs to the coordinate system of this view. If
139 * <code>null</code> then the point is an absolute screen
140 * position.
141 */
142 public void translateCoordinatesToRoot(final Point point, final View view) {
143 if (view == null)
144 return;
145 translateCoordinates(point, view, view.getRootView());
146 }
147
148 /***
149 * Returns the text height.
150 *
151 * @param paint
152 * the paint to use
153 * @param text
154 * the text to measure
155 * @return text height, in pixels.
156 */
157 public static int getTextHeight(final Paint paint, final String text) {
158 final CharacterIterator i = new StringCharacterIterator(text);
159 int rows = 1;
160 for (char c = i.current(); c != CharacterIterator.DONE; c = i.next()) {
161 if (c == '\n')
162 rows++;
163 }
164 final FontMetricsInt f = paint.getFontMetricsInt();
165 return (f.bottom - f.top) * rows;
166 }
167
168 /***
169 * Measures given text width/height and sets them to the given point.
170 *
171 * @param paint
172 * the paint
173 * @param text
174 * text to measure
175 * @param point
176 * overwrites this point.
177 */
178 public static void measureText(final Paint paint, final String text,
179 final Point point) {
180 point.x = intMeasureText(paint, text);
181 point.y = getTextHeight(paint, text);
182 }
183
184 /***
185 * Measures text with given paint. Correctly handles newlines.
186 *
187 * @param paint
188 * the paint
189 * @param text
190 * the text to measure
191 * @return text width.
192 */
193 public static int intMeasureText(final Paint paint, final String text) {
194 int result = 0;
195 final StringTokenizer t = new StringTokenizer(text, "\n");
196 while (t.hasMoreElements()) {
197 int measured = (int) paint.measureText(t.nextToken());
198 if (result < measured) {
199 result = measured;
200 }
201 }
202 return result;
203 }
204
205 /***
206 * Measures given text width/height and returns the result as a point.
207 *
208 * @param paint
209 * the paint
210 * @param text
211 * text to measure
212 * @return the text sizes
213 */
214 public static Point measureText(final Paint paint, final String text) {
215 final Point result = new Point();
216 measureText(paint, text, result);
217 return result;
218 }
219
220 /***
221 * Measures given text width/height and sets them to the
222 * {@link #measuredText cached point}.
223 *
224 * @param paint
225 * the paint
226 * @param text
227 * text to measure
228 */
229 public void measureTextCache(final Paint paint, final String text) {
230 measuredText.x = intMeasureText(paint, text);
231 measuredText.y = getTextHeight(paint, text);
232 }
233
234 /***
235 * The result of {@link #measureTextCache(Paint, String)} is stored here.
236 */
237 public final Point measuredText = new Point();
238
239 /***
240 * Fired when a text has been successfully entered.
241 *
242 * @author Martin Vysny
243 */
244 public static interface OnTextSubmit {
245 /***
246 * Validates given text. If the text is valid then return
247 * <code>null</code>.
248 *
249 * @param text
250 * the text to validate, never <code>null</code>.
251 * @return <code>null</code> if the text is valid, error message
252 * otherwise.
253 */
254 String validate(final String text);
255
256 /***
257 * A text has been submitted, never <code>null</code>.
258 *
259 * @param text
260 * the text.
261 */
262 void submit(final String text);
263
264 /***
265 * The dialog has been canceled.
266 */
267 void cancel();
268 }
269
270 /***
271 * Listens for dialog events.
272 *
273 * @author Martin Vysny
274 */
275 private static class AlertDlgListener implements TextWatcher,
276 OnClickListener, OnCancelListener {
277 private final TextView errorText;
278 private final EditText edit;
279 private final OnTextSubmit listener;
280 private final Dialog dlg;
281
282 /***
283 * Creates new listener.
284 *
285 * @param listener
286 * the listener
287 * @param dlg
288 * the dialog instance.
289 */
290 public AlertDlgListener(final OnTextSubmit listener, final Dialog dlg) {
291 super();
292 edit = (EditText) dlg.findViewById(R.id.texteditText);
293 errorText = (TextView) dlg.findViewById(R.id.texteditErrorMsg);
294 this.listener = listener;
295 this.dlg = dlg;
296 edit.addTextChangedListener(this);
297 ((Button) dlg.findViewById(R.id.texteditSubmit))
298 .setOnClickListener(this);
299 ((Button) dlg.findViewById(R.id.texteditCancel))
300 .setOnClickListener(this);
301 dlg.setOnCancelListener(this);
302 validate();
303 }
304
305 /***
306 * Returns currently entered text.
307 *
308 * @return currently entered text, never <code>null</code>.
309 */
310 public String getText() {
311 String text = edit.getText().toString();
312 if (text == null)
313 text = "";
314 return text;
315 }
316
317 private boolean validate() {
318 final String text = getText();
319 final String validate = listener.validate(text);
320 final boolean isValid = validate == null;
321 if (!isValid) {
322 errorText.setVisibility(View.VISIBLE);
323 errorText.setText(validate);
324 } else {
325 errorText.setText("");
326 errorText.setVisibility(View.INVISIBLE);
327 }
328 return isValid;
329 }
330
331 public void beforeTextChanged(CharSequence s, int start, int count,
332 int after) {
333
334 }
335
336 public void onTextChanged(CharSequence s, int start, int before,
337 int count) {
338 validate();
339 }
340
341 public void onClick(View arg0) {
342 switch (arg0.getId()) {
343 case R.id.texteditSubmit:
344 if (!validate())
345 return;
346 listener.submit(getText());
347 dlg.dismiss();
348 break;
349 case R.id.texteditCancel:
350 dlg.cancel();
351 break;
352 }
353 }
354
355 public void onCancel(DialogInterface arg0) {
356 listener.cancel();
357 }
358
359 public void afterTextChanged(Editable s) {
360
361 }
362 }
363
364 /***
365 * Creates new text dialog with a text enter capability. When the dialog is
366 * submitted the text submit event is fired.
367 *
368 * @param context
369 * the context
370 * @param submitButtonCaption
371 * the submit button caption, if <code>null</code> then OK will
372 * be shown.
373 * @param cancelButtonCaption
374 * the cancel button caption, if <code>null</code> then Cancel
375 * will be shown.
376 * @param dialogCaption
377 * the dialog caption
378 * @param text
379 * the text to show in the dialog.
380 * @param listener
381 * fire events on this listener
382 * @return the dialog instance. The dialog is already shown.
383 */
384 public static Dialog showTextEditor(final Context context,
385 final String submitButtonCaption, final String cancelButtonCaption,
386 final String dialogCaption, final String text,
387 final OnTextSubmit listener) {
388 if (listener == null)
389 throw new IllegalArgumentException("listener is null");
390 final Dialog dlg = new Dialog(context);
391 dlg.setTitle(dialogCaption);
392 dlg.setContentView(R.layout.texteditor);
393 ((TextView) dlg.findViewById(R.id.texteditCaption)).setText(text);
394 if (submitButtonCaption != null) {
395 ((Button) dlg.findViewById(R.id.texteditSubmit))
396 .setText(submitButtonCaption);
397 }
398 if (cancelButtonCaption != null) {
399 ((Button) dlg.findViewById(R.id.texteditCancel))
400 .setText(cancelButtonCaption);
401 }
402 dlg.setCancelable(true);
403 new AlertDlgListener(listener, dlg);
404 dlg.show();
405 return dlg;
406 }
407
408 /***
409 * Cancels the dialog on invocation.
410 */
411 public final static DialogInterface.OnClickListener CANCEL = new DialogInterface.OnClickListener() {
412 public void onClick(DialogInterface arg0, int arg1) {
413 arg0.cancel();
414 }
415 };
416
417 /***
418 * Shows a simple yes/no question dialog.
419 *
420 * @param context
421 * the context.
422 * @param question
423 * the question body.
424 * @param yesButtonListener
425 * invoked when user presses the 'Yes' button.
426 * @return the dialog instance. The dialog is already shown.
427 */
428 public static AlertDialog showYesNoDialog(final Context context,
429 final String question,
430 final DialogInterface.OnClickListener yesButtonListener) {
431 final AlertDialog.Builder builder = new AlertDialog.Builder(context);
432 final AlertDialog dlg = builder.setMessage(question).create();
433 dlg.setButton(context.getResources().getString(R.string.yes),
434 yesButtonListener);
435 dlg.setButton2(context.getResources().getString(R.string.no),
436 ViewUtils.CANCEL);
437 dlg.show();
438 return dlg;
439 }
440
441 /***
442 * Shows a simple OK dialog.
443 *
444 * @param context
445 * the context.
446 * @param caption
447 * the dialog caption
448 * @param text
449 * the text body.
450 * @param icon
451 * optional dialog icon, may be -1 if it should not be shown.
452 * @return the dialog instance. The dialog is already shown.
453 */
454 public static AlertDialog showOkDialog(final Context context,
455 final CharSequence caption, final CharSequence text, final int icon) {
456 final AlertDialog.Builder builder = new AlertDialog.Builder(context);
457 if (icon != -1) {
458 builder.setIcon(icon);
459 }
460 final SpannableStringBuilder b = new SpannableStringBuilder(text);
461 Linkify.addLinks(b, Linkify.ALL);
462 final AlertDialog dlg = builder.setTitle(caption).setMessage(b)
463 .create();
464 dlg.setButton(context.getResources().getString(R.string.ok),
465 ViewUtils.CANCEL);
466 dlg.show();
467 return dlg;
468 }
469
470 /***
471 * Checks if given event is a {@link MotionEvent#ACTION_UP} or
472 * {@link MotionEvent#ACTION_CANCEL} event.
473 *
474 * @param event
475 * the event to check
476 * @return <code>true</code> if pen is up or the motion is canceled,
477 * <code>false</code> otherwise.
478 */
479 public static boolean isPenUp(final MotionEvent event) {
480 final int action = event.getAction();
481 return (action == MotionEvent.ACTION_UP)
482 || (action == MotionEvent.ACTION_CANCEL);
483 }
484
485 /***
486 * Checks if given event is a {@link MotionEvent#ACTION_CANCEL} event.
487 *
488 * @param event
489 * the event to check
490 * @return <code>true</code> if the motion is canceled, <code>false</code>
491 * otherwise.
492 */
493 public static boolean isCancel(final MotionEvent event) {
494 final int action = event.getAction();
495 return action == MotionEvent.ACTION_CANCEL;
496 }
497
498 /***
499 * Appends an {@link ImageSpan} to given builder.
500 *
501 * @param c
502 * the context
503 * @param s
504 * the builder
505 * @param resId
506 * image id.
507 */
508 public static void addImage(final Context c,
509 final SpannableStringBuilder s, final int resId) {
510 s.append("W");
511 s.setSpan(new ImageSpan(c, resId), s.length() - 1, s.length(),
512 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
513 }
514 }