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
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
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
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 }