View Javadoc

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 			// transform the point into the absolute screen coordinate system
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 			// do nothing
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 			// do nothing
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 }