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  
25  import sk.baka.ambient.R;
26  import android.app.AlertDialog;
27  import android.app.Dialog;
28  import android.content.Context;
29  import android.content.DialogInterface;
30  import android.content.DialogInterface.OnCancelListener;
31  import android.graphics.Bitmap;
32  import android.graphics.Paint;
33  import android.graphics.Point;
34  import android.graphics.Paint.FontMetricsInt;
35  import android.text.Editable;
36  import android.text.TextWatcher;
37  import android.view.MotionEvent;
38  import android.view.View;
39  import android.view.View.OnClickListener;
40  import android.widget.Button;
41  import android.widget.EditText;
42  import android.widget.TextView;
43  
44  /***
45   * Utility methods for views. Not thread safe.
46   * 
47   * @author Martin Vysny
48   */
49  public final class ViewUtils {
50  	/***
51  	 * Clones given point
52  	 * 
53  	 * @param point
54  	 *            the point to clone
55  	 * @return cloned point
56  	 */
57  	public static final Point clone(final Point point) {
58  		return new Point(point.x, point.y);
59  	}
60  
61  	/***
62  	 * {@link Bitmap#recycle() Recycles} given collection of bitmap. The
63  	 * collection is emptied afterwards.
64  	 * 
65  	 * @param bitmaps
66  	 *            bitmaps to recycle.
67  	 */
68  	public static void recycleBitmaps(final Collection<Bitmap> bitmaps) {
69  		for (final Bitmap b : bitmaps) {
70  			if (!b.isRecycled()) {
71  				b.recycle();
72  			}
73  		}
74  		bitmaps.clear();
75  	}
76  	
77  	/***
78  	 * Here the temporary results of all translate* methods are stored. This is
79  	 * just a cache, to prevent an array creation on each call.
80  	 */
81  	final int[] translatedPoint = new int[] { 0, 0 };
82  
83  	/***
84  	 * Translates given point into target view's coordinate system and stores
85  	 * the result into the {@link #translated} point.
86  	 * 
87  	 * @param point
88  	 *            the point to translate. Will not get modified. If
89  	 *            <code>null</code> then the {@link #translated} point data
90  	 *            will be taken instead.
91  	 * @param view
92  	 *            the point belongs to the coordinate system of this view. If
93  	 *            <code>null</code> then the point is an absolute screen
94  	 *            position.
95  	 * @param targetView
96  	 *            translate the view to this view coordinate system. If
97  	 *            <code>null</code> then the point will be returned as an
98  	 *            absolute screen position.
99  	 */
100 	public void translateCoordinates(final Point point, final View view,
101 			final View targetView) {
102 		if (point != null) {
103 			translated.x = point.x;
104 			translated.y = point.y;
105 		}
106 		if (view == targetView)
107 			return;
108 		if (view != null) {
109 			// transform the point into the absolute screen coordinate system
110 			view.getLocationOnScreen(translatedPoint);
111 			translated.x += translatedPoint[0];
112 			translated.y += translatedPoint[1];
113 		}
114 		if (targetView != null) {
115 			targetView.getLocationOnScreen(translatedPoint);
116 			translated.x -= translatedPoint[0];
117 			translated.y -= translatedPoint[1];
118 		}
119 	}
120 
121 	/***
122 	 * A result of translate* operations is stored here.
123 	 */
124 	public final Point translated = new Point();
125 
126 	/***
127 	 * Translates given point into root view's coordinate system. Does nothing
128 	 * if the supplied view is <code>null</code>.
129 	 * 
130 	 * @param point
131 	 *            the point to translate. Will not get modified.
132 	 * @param view
133 	 *            the point belongs to the coordinate system of this view. If
134 	 *            <code>null</code> then the point is an absolute screen
135 	 *            position.
136 	 */
137 	public void translateCoordinatesToRoot(final Point point, final View view) {
138 		if (view == null)
139 			return;
140 		translateCoordinates(point, view, view.getRootView());
141 	}
142 
143 	/***
144 	 * Returns the text height.
145 	 * 
146 	 * @param paint
147 	 *            the paint to use
148 	 * @param text
149 	 *            the text to measure
150 	 * @return text height, in pixels.
151 	 */
152 	public static int getTextHeight(final Paint paint, final String text) {
153 		final CharacterIterator i = new StringCharacterIterator(text);
154 		int rows = 1;
155 		for (char c = i.current(); c != CharacterIterator.DONE; c = i.next()) {
156 			if (c == '\n')
157 				rows++;
158 		}
159 		final FontMetricsInt f = paint.getFontMetricsInt();
160 		return (f.bottom - f.top) * rows;
161 	}
162 
163 	/***
164 	 * Measures given text width/height and sets them to the given point.
165 	 * 
166 	 * @param paint
167 	 *            the paint
168 	 * @param text
169 	 *            text to measure
170 	 * @param point
171 	 *            overwrites this point.
172 	 */
173 	public static void measureText(final Paint paint, final String text,
174 			final Point point) {
175 		point.x = (int) paint.measureText(text);
176 		point.y = getTextHeight(paint, text);
177 	}
178 
179 	/***
180 	 * Measures given text width/height and returns the result as a point.
181 	 * 
182 	 * @param paint
183 	 *            the paint
184 	 * @param text
185 	 *            text to measure
186 	 * @return the text sizes
187 	 */
188 	public static Point measureText(final Paint paint, final String text) {
189 		final Point result = new Point();
190 		measureText(paint, text, result);
191 		return result;
192 	}
193 
194 	/***
195 	 * Measures given text width/height and sets them to the
196 	 * {@link #measuredText cached point}.
197 	 * 
198 	 * @param paint
199 	 *            the paint
200 	 * @param text
201 	 *            text to measure
202 	 */
203 	public void measureTextCache(final Paint paint, final String text) {
204 		measuredText.x = (int) paint.measureText(text);
205 		measuredText.y = getTextHeight(paint, text);
206 	}
207 
208 	/***
209 	 * The result of {@link #measureTextCache(Paint, String)} is stored here.
210 	 */
211 	public final Point measuredText = new Point();
212 
213 	/***
214 	 * Fired when a text has been successfully entered.
215 	 * 
216 	 * @author Martin Vysny
217 	 */
218 	public static interface OnTextSubmit {
219 		/***
220 		 * Validates given text. If the text is valid then return
221 		 * <code>null</code>.
222 		 * 
223 		 * @param text
224 		 *            the text to validate, never <code>null</code>.
225 		 * @return <code>null</code> if the text is valid, error message
226 		 *         otherwise.
227 		 */
228 		String validate(final String text);
229 
230 		/***
231 		 * A text has been submitted, never <code>null</code>.
232 		 * 
233 		 * @param text
234 		 *            the text.
235 		 */
236 		void submit(final String text);
237 
238 		/***
239 		 * The dialog has been cancelled.
240 		 */
241 		void cancel();
242 	}
243 
244 	/***
245 	 * Listens for dialog events.
246 	 * 
247 	 * @author Martin Vysny
248 	 */
249 	private static class AlertDlgListener implements TextWatcher,
250 			OnClickListener, OnCancelListener {
251 		private final TextView errorText;
252 		private final EditText edit;
253 		private final OnTextSubmit listener;
254 		private final Dialog dlg;
255 
256 		/***
257 		 * Creates new listener.
258 		 * 
259 		 * @param listener
260 		 *            the listener
261 		 * @param dlg
262 		 *            the dialog instance.
263 		 */
264 		public AlertDlgListener(final OnTextSubmit listener, final Dialog dlg) {
265 			super();
266 			edit = (EditText) dlg.findViewById(R.id.texteditText);
267 			errorText = (TextView) dlg.findViewById(R.id.texteditErrorMsg);
268 			this.listener = listener;
269 			this.dlg = dlg;
270 			edit.addTextChangedListener(this);
271 			((Button) dlg.findViewById(R.id.texteditSubmit))
272 					.setOnClickListener(this);
273 			((Button) dlg.findViewById(R.id.texteditCancel))
274 					.setOnClickListener(this);
275 			dlg.setOnCancelListener(this);
276 			validate();
277 		}
278 
279 		/***
280 		 * Returns currently entered text.
281 		 * 
282 		 * @return currently entered text, never <code>null</code>.
283 		 */
284 		public String getText() {
285 			String text = edit.getText().toString();
286 			if (text == null)
287 				text = "";
288 			return text;
289 		}
290 
291 		private boolean validate() {
292 			final String text = getText();
293 			final String validate = listener.validate(text);
294 			final boolean isValid = validate == null;
295 			if (!isValid) {
296 				errorText.setVisibility(View.VISIBLE);
297 				errorText.setText(validate);
298 			} else {
299 				errorText.setText("");
300 				errorText.setVisibility(View.INVISIBLE);
301 			}
302 			return isValid;
303 		}
304 
305 		public void beforeTextChanged(CharSequence s, int start, int count,
306 				int after) {
307 			// do nothing
308 		}
309 
310 		public void onTextChanged(CharSequence s, int start, int before,
311 				int count) {
312 			validate();
313 		}
314 
315 		public void onClick(View arg0) {
316 			switch (arg0.getId()) {
317 			case R.id.texteditSubmit:
318 				if (!validate())
319 					return;
320 				listener.submit(getText());
321 				dlg.dismiss();
322 				break;
323 			case R.id.texteditCancel:
324 				dlg.cancel();
325 				break;
326 			}
327 		}
328 
329 		public void onCancel(DialogInterface arg0) {
330 			listener.cancel();
331 		}
332 
333 		public void afterTextChanged(Editable s) {
334 			// do nothing
335 		}
336 	}
337 
338 	/***
339 	 * Creates new text dialog with a text enter capability. When the dialog is
340 	 * submitted the text submit event is fired.
341 	 * 
342 	 * @param context
343 	 *            the context
344 	 * @param submitButtonCaption
345 	 *            the submit button caption, if <code>null</code> then OK will
346 	 *            be shown.
347 	 * @param cancelButtonCaption
348 	 *            the cancel button caption, if <code>null</code> then Cancel
349 	 *            will be shown.
350 	 * @param dialogCaption
351 	 *            the dialog caption
352 	 * @param text
353 	 *            the text to show in the dialog.
354 	 * @param listener
355 	 *            fire events on this listener
356 	 * @return listener to be registered to the submit button.
357 	 */
358 	public static Dialog showTextEditor(final Context context,
359 			final String submitButtonCaption, final String cancelButtonCaption,
360 			final String dialogCaption, final String text, final OnTextSubmit listener) {
361 		if (listener == null)
362 			throw new IllegalArgumentException("listener is null");
363 		final Dialog dlg = new Dialog(context);
364 		dlg.setTitle(dialogCaption);
365 		dlg.setContentView(R.layout.texteditor);
366 		((TextView) dlg.findViewById(R.id.texteditCaption))
367 				.setText(text);
368 		if (submitButtonCaption != null) {
369 			((Button) dlg.findViewById(R.id.texteditSubmit))
370 					.setText(submitButtonCaption);
371 		}
372 		if (cancelButtonCaption != null) {
373 			((Button) dlg.findViewById(R.id.texteditCancel))
374 					.setText(cancelButtonCaption);
375 		}
376 		dlg.setCancelable(true);
377 		new AlertDlgListener(listener, dlg);
378 		dlg.show();
379 		return dlg;
380 	}
381 
382 	/***
383 	 * Cancels the dialog on invocation.
384 	 */
385 	public final static DialogInterface.OnClickListener CANCEL = new DialogInterface.OnClickListener() {
386 		public void onClick(DialogInterface arg0, int arg1) {
387 			arg0.cancel();
388 		}
389 	};
390 
391 	/***
392 	 * Shows a simple yes/no question dialog.
393 	 * 
394 	 * @param context
395 	 *            the context.
396 	 * @param questionResId
397 	 *            the question body.
398 	 * @param yesButtonListener
399 	 *            invoked when user presses the 'Yes' button.
400 	 * @return the dialog instance.
401 	 */
402 	public static AlertDialog showYesNoDialog(final Context context,
403 			final int questionResId,
404 			final DialogInterface.OnClickListener yesButtonListener) {
405 		final AlertDialog.Builder builder = new AlertDialog.Builder(context);
406 		final AlertDialog dlg = builder.setMessage(questionResId).create();
407 		dlg.setButton(context.getResources().getString(R.string.yes),
408 				yesButtonListener);
409 		dlg.setButton2(context.getResources().getString(R.string.no),
410 				ViewUtils.CANCEL);
411 		dlg.show();
412 		return dlg;
413 	}
414 
415 	/***
416 	 * Checks if given event is a {@link MotionEvent#ACTION_UP} or
417 	 * {@link MotionEvent#ACTION_CANCEL} event.
418 	 * 
419 	 * @param event
420 	 *            the event to check
421 	 * @return <code>true</code> if pen is up or the motion is canceled,
422 	 *         <code>false</code> otherwise.
423 	 */
424 	public static boolean isPenUp(final MotionEvent event) {
425 		final int action = event.getAction();
426 		return (action == MotionEvent.ACTION_UP)
427 				|| (action == MotionEvent.ACTION_CANCEL);
428 	}
429 
430 	/***
431 	 * Checks if given event is a {@link MotionEvent#ACTION_CANCEL} event.
432 	 * 
433 	 * @param event
434 	 *            the event to check
435 	 * @return <code>true</code> if the motion is canceled,
436 	 *         <code>false</code> otherwise.
437 	 */
438 	public static boolean isCancel(final MotionEvent event) {
439 		final int action = event.getAction();
440 		return action == MotionEvent.ACTION_CANCEL;
441 	}
442 }