diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c18c9208..afdbc3f6 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -19,6 +19,7 @@ + \ No newline at end of file diff --git a/res/drawable/timepicker_down_btn.xml b/res/drawable/timepicker_down_btn.xml new file mode 100644 index 00000000..b77375a8 --- /dev/null +++ b/res/drawable/timepicker_down_btn.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/drawable/timepicker_down_disabled.9.png b/res/drawable/timepicker_down_disabled.9.png new file mode 100644 index 00000000..596294b6 Binary files /dev/null and b/res/drawable/timepicker_down_disabled.9.png differ diff --git a/res/drawable/timepicker_down_disabled_focused.9.png b/res/drawable/timepicker_down_disabled_focused.9.png new file mode 100644 index 00000000..662cffd1 Binary files /dev/null and b/res/drawable/timepicker_down_disabled_focused.9.png differ diff --git a/res/drawable/timepicker_down_normal.9.png b/res/drawable/timepicker_down_normal.9.png new file mode 100644 index 00000000..f17e8f94 Binary files /dev/null and b/res/drawable/timepicker_down_normal.9.png differ diff --git a/res/drawable/timepicker_down_pressed.9.png b/res/drawable/timepicker_down_pressed.9.png new file mode 100644 index 00000000..777bcf5f Binary files /dev/null and b/res/drawable/timepicker_down_pressed.9.png differ diff --git a/res/drawable/timepicker_down_selected.9.png b/res/drawable/timepicker_down_selected.9.png new file mode 100644 index 00000000..b45db621 Binary files /dev/null and b/res/drawable/timepicker_down_selected.9.png differ diff --git a/res/drawable/timepicker_input.xml b/res/drawable/timepicker_input.xml new file mode 100644 index 00000000..355e9d76 --- /dev/null +++ b/res/drawable/timepicker_input.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/timepicker_input_disabled.9.png b/res/drawable/timepicker_input_disabled.9.png new file mode 100644 index 00000000..f73658e7 Binary files /dev/null and b/res/drawable/timepicker_input_disabled.9.png differ diff --git a/res/drawable/timepicker_input_normal.9.png b/res/drawable/timepicker_input_normal.9.png new file mode 100644 index 00000000..8032adac Binary files /dev/null and b/res/drawable/timepicker_input_normal.9.png differ diff --git a/res/drawable/timepicker_input_pressed.9.png b/res/drawable/timepicker_input_pressed.9.png new file mode 100644 index 00000000..30d8d5fa Binary files /dev/null and b/res/drawable/timepicker_input_pressed.9.png differ diff --git a/res/drawable/timepicker_input_selected.9.png b/res/drawable/timepicker_input_selected.9.png new file mode 100644 index 00000000..874f18f2 Binary files /dev/null and b/res/drawable/timepicker_input_selected.9.png differ diff --git a/res/drawable/timepicker_up_btn.xml b/res/drawable/timepicker_up_btn.xml new file mode 100644 index 00000000..22e32a3f --- /dev/null +++ b/res/drawable/timepicker_up_btn.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/timepicker_up_disabled.9.png b/res/drawable/timepicker_up_disabled.9.png new file mode 100644 index 00000000..327b0b5a Binary files /dev/null and b/res/drawable/timepicker_up_disabled.9.png differ diff --git a/res/drawable/timepicker_up_disabled_focused.9.png b/res/drawable/timepicker_up_disabled_focused.9.png new file mode 100644 index 00000000..4c96680f Binary files /dev/null and b/res/drawable/timepicker_up_disabled_focused.9.png differ diff --git a/res/drawable/timepicker_up_normal.9.png b/res/drawable/timepicker_up_normal.9.png new file mode 100644 index 00000000..dcd26e01 Binary files /dev/null and b/res/drawable/timepicker_up_normal.9.png differ diff --git a/res/drawable/timepicker_up_pressed.9.png b/res/drawable/timepicker_up_pressed.9.png new file mode 100644 index 00000000..7dac7786 Binary files /dev/null and b/res/drawable/timepicker_up_pressed.9.png differ diff --git a/res/drawable/timepicker_up_selected.9.png b/res/drawable/timepicker_up_selected.9.png new file mode 100644 index 00000000..35dae8ef Binary files /dev/null and b/res/drawable/timepicker_up_selected.9.png differ diff --git a/res/layout-land/about.xml b/res/layout-land/about.xml new file mode 100644 index 00000000..81ac296a --- /dev/null +++ b/res/layout-land/about.xml @@ -0,0 +1,22 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/layout-port/about.xml b/res/layout-port/about.xml new file mode 100644 index 00000000..28933ac3 --- /dev/null +++ b/res/layout-port/about.xml @@ -0,0 +1,22 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/layout-port/number_picker.xml b/res/layout-port/number_picker.xml new file mode 100644 index 00000000..1b1a31c8 --- /dev/null +++ b/res/layout-port/number_picker.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/menu/main_menu.xml b/res/menu/main_menu.xml index 53b9290d..3998aa42 100644 --- a/res/menu/main_menu.xml +++ b/res/menu/main_menu.xml @@ -1,9 +1,10 @@ + - + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 1fb71d86..e85e9973 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6,10 +6,20 @@ Result copied to clipboard! Settings Help + About + Main settings + Drag buttons settings + + Drag button calibration Allows to calibrate drag button behaviour Down Up Restart + + Copyright (c) 2009-2011.\nCreated by serso aka se.solovyev.\n + For more information please\ncontact me via email\nse.solovyev@gmail.com + \nor visit\nhttp://se.solovyev.org + diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 0ee7d313..07075ce1 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -1,20 +1,15 @@ - - - - - - + + \ No newline at end of file diff --git a/src/main/java/org/solovyev/android/calculator/AboutActivity.java b/src/main/java/org/solovyev/android/calculator/AboutActivity.java new file mode 100644 index 00000000..0fb25b9b --- /dev/null +++ b/src/main/java/org/solovyev/android/calculator/AboutActivity.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2009-2011. Created by serso aka se.solovyev. + * For more information, please, contact se.solovyev@gmail.com + */ + +package org.solovyev.android.calculator; + +import android.app.Activity; +import android.os.Bundle; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.widget.TextView; +import org.jetbrains.annotations.Nullable; + +/** + * User: serso + * Date: 9/16/11 + * Time: 11:52 PM + */ +public class AboutActivity extends Activity { + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.about); + + final TextView about = (TextView) findViewById(R.id.aboutTextView); + about.setMovementMethod(LinkMovementMethod.getInstance()); + + } +} diff --git a/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java b/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java index 10160c89..efe4c966 100644 --- a/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java +++ b/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java @@ -25,15 +25,13 @@ import org.solovyev.common.utils.history.HistoryAction; import java.lang.reflect.Field; import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.List; public class CalculatorActivity extends Activity implements FontSizeAdjuster { private static final int HVGA_WIDTH_PIXELS = 320; @NotNull - private List onDragListeners = new ArrayList(); + private final DragPreferencesChangeListenerRegister dpclRegister = new DragPreferencesChangeListenerRegister(); @NotNull private CalculatorView calculatorView; @@ -64,7 +62,7 @@ public class CalculatorActivity extends Activity implements FontSizeAdjuster { final DragButtonCalibrationActivity.Preferences dragPreferences = DragButtonCalibrationActivity.getPreferences(this); final SimpleOnDragListener onDragListener = new SimpleOnDragListener(new DigitButtonDragProcessor(calculatorView), dragPreferences); - onDragListeners.add(onDragListener); + dpclRegister.addListener(onDragListener); // todo serso: check if there is more convenient method for doing this final R.id ids = new R.id(); @@ -86,12 +84,12 @@ public class CalculatorActivity extends Activity implements FontSizeAdjuster { final SimpleOnDragListener historyOnDragListener = new SimpleOnDragListener(new HistoryDragProcessor(this.calculatorView), dragPreferences); ((DragButton) findViewById(R.id.historyButton)).setOnDragListener(historyOnDragListener); - onDragListeners.add(historyOnDragListener); + dpclRegister.addListener(historyOnDragListener); final SimpleOnDragListener toPositionOnDragListener = new SimpleOnDragListener(new CursorDragProcessor(calculatorView), dragPreferences); ((DragButton) findViewById(R.id.rightButton)).setOnDragListener(toPositionOnDragListener); ((DragButton) findViewById(R.id.leftButton)).setOnDragListener(toPositionOnDragListener); - onDragListeners.add(toPositionOnDragListener); + dpclRegister.addListener(toPositionOnDragListener); preferencesChangesReceiver = new BroadcastReceiver() { @@ -100,9 +98,7 @@ public class CalculatorActivity extends Activity implements FontSizeAdjuster { if (DragButtonCalibrationActivity.INTENT_ACTION.equals(intent.getAction())) { final DragButtonCalibrationActivity.Preferences preferences = DragButtonCalibrationActivity.getPreferences(CalculatorActivity.this); - for (SimpleOnDragListener dragListener : onDragListeners) { - dragListener.setPreferences(preferences); - } + dpclRegister.announce().onDragPreferencesChange(preferences); } } }; @@ -189,9 +185,8 @@ public class CalculatorActivity extends Activity implements FontSizeAdjuster { @Override public boolean onCreateOptionsMenu(Menu menu) { - // todo serso: inflate menu as soon as it will implemented in proper way -/* final MenuInflater menuInflater = getMenuInflater(); - menuInflater.inflate(R.menu.main_menu, menu);*/ + final MenuInflater menuInflater = getMenuInflater(); + menuInflater.inflate(R.menu.main_menu, menu); return true; } @@ -204,8 +199,8 @@ public class CalculatorActivity extends Activity implements FontSizeAdjuster { showSettings(); result = true; break; - case R.id.menu_item_help: - showHelp(); + case R.id.menu_item_about: + showAbout(); result = true; break; default: @@ -219,8 +214,8 @@ public class CalculatorActivity extends Activity implements FontSizeAdjuster { startActivity(new Intent(this, CalculatorPreferencesActivity.class)); } - private void showHelp() { - Log.d(CalculatorActivity.class + "showHelp()", "Show help!"); + private void showAbout() { + startActivity(new Intent(this, AboutActivity.class)); } /** diff --git a/src/main/java/org/solovyev/android/calculator/CalculatorPreferencesActivity.java b/src/main/java/org/solovyev/android/calculator/CalculatorPreferencesActivity.java index cc3d5532..e9df1be7 100644 --- a/src/main/java/org/solovyev/android/calculator/CalculatorPreferencesActivity.java +++ b/src/main/java/org/solovyev/android/calculator/CalculatorPreferencesActivity.java @@ -22,19 +22,5 @@ public class CalculatorPreferencesActivity extends PreferenceActivity { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); - - final Preference dragButtonCalibration = findPreference("dragButtonCalibration"); - dragButtonCalibration.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - - public boolean onPreferenceClick(Preference preference) { - startActivity(new Intent(CalculatorPreferencesActivity.this, DragButtonCalibrationActivity.class)); - return true; - } - }); - } - - @Override - protected void onRestoreInstanceState(Bundle state) { - super.onRestoreInstanceState(state); } } diff --git a/src/main/java/org/solovyev/android/calculator/HelpActivity.java b/src/main/java/org/solovyev/android/calculator/HelpActivity.java deleted file mode 100644 index 45d04900..00000000 --- a/src/main/java/org/solovyev/android/calculator/HelpActivity.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) 2009-2011. Created by serso aka se.solovyev. - * For more information, please, contact se.solovyev@gmail.com - */ - -package org.solovyev.android.calculator; - -import android.app.Activity; - -/** - * User: serso - * Date: 9/16/11 - * Time: 11:52 PM - */ -public class HelpActivity extends Activity { - // todo serso: implement -} diff --git a/src/main/java/org/solovyev/android/view/AutoResizeTextView.java b/src/main/java/org/solovyev/android/view/AutoResizeTextView.java new file mode 100644 index 00000000..207870a0 --- /dev/null +++ b/src/main/java/org/solovyev/android/view/AutoResizeTextView.java @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2009-2011. Created by serso aka se.solovyev. + * For more information, please, contact se.solovyev@gmail.com + * or visit http://se.solovyev.org + */ + +package org.solovyev.android.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.text.Layout.Alignment; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.TextView; + +/** + * Text view that auto adjusts text size to fit within the view. + * If the text size equals the minimum text size and still does not + * fit, append with an ellipsis. + * + * @author Chase Colburn + * @since Apr 4, 2011 + */ +public class AutoResizeTextView extends TextView { + + // Minimum text size for this text view + public static final float MIN_TEXT_SIZE = 20; + + // Interface for resize notifications + public interface OnTextResizeListener { + public void onTextResize(TextView textView, float oldSize, float newSize); + } + + // Off screen canvas for text size rendering + private static final Canvas sTextResizeCanvas = new Canvas(); + + // Our ellipse string + private static final String mEllipsis = "..."; + + // Registered resize listener + private OnTextResizeListener mTextResizeListener; + + // Flag for text and/or size changes to force a resize + private boolean mNeedsResize = false; + + // Text size that is set from code. This acts as a starting point for resizing + private float mTextSize; + + // Temporary upper bounds on the starting text size + private float mMaxTextSize = 0; + + // Lower bounds for text size + private float mMinTextSize = MIN_TEXT_SIZE; + + // Text view line spacing multiplier + private float mSpacingMult = 1.0f; + + // Text view additional line spacing + private float mSpacingAdd = 0.0f; + + // Add ellipsis to text that overflows at the smallest text size + private boolean mAddEllipsis = true; + + // Default constructor override + public AutoResizeTextView(Context context) { + this(context, null); + } + + // Default constructor when inflating from XML file + public AutoResizeTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + // Default constructor override + public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mTextSize = getTextSize(); + } + + /** + * When text changes, set the force resize flag to true and reset the text size. + */ + @Override + protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) { + mNeedsResize = true; + // Since this view may be reused, it is good to reset the text size + resetTextSize(); + } + + /** + * If the text view size changed, set the force resize flag to true + */ + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (w != oldw || h != oldh) { + mNeedsResize = true; + } + } + + /** + * Register listener to receive resize notifications + * @param listener + */ + public void setOnResizeListener(OnTextResizeListener listener) { + mTextResizeListener = listener; + } + + /** + * Override the set text size to update our internal reference values + */ + @Override + public void setTextSize(float size) { + super.setTextSize(size); + mTextSize = getTextSize(); + } + + /** + * Override the set text size to update our internal reference values + */ + @Override + public void setTextSize(int unit, float size) { + super.setTextSize(unit, size); + mTextSize = getTextSize(); + } + + /** + * Override the set line spacing to update our internal reference values + */ + @Override + public void setLineSpacing(float add, float mult) { + super.setLineSpacing(add, mult); + mSpacingMult = mult; + mSpacingAdd = add; + } + + /** + * Set the upper text size limit and invalidate the view + * @param maxTextSize + */ + public void setMaxTextSize(float maxTextSize) { + mMaxTextSize = maxTextSize; + requestLayout(); + invalidate(); + } + + /** + * Return upper text size limit + * @return + */ + public float getMaxTextSize() { + return mMaxTextSize; + } + + /** + * Set the lower text size limit and invalidate the view + * @param minTextSize + */ + public void setMinTextSize(float minTextSize) { + mMinTextSize = minTextSize; + requestLayout(); + invalidate(); + } + + /** + * Return lower text size limit + * @return + */ + public float getMinTextSize() { + return mMinTextSize; + } + + /** + * Set flag to add ellipsis to text that overflows at the smallest text size + * @param addEllipsis + */ + public void setAddEllipsis(boolean addEllipsis) { + mAddEllipsis = addEllipsis; + } + + /** + * Return flag to add ellipsis to text that overflows at the smallest text size + * @return + */ + public boolean getAddEllipsis() { + return mAddEllipsis; + } + + /** + * Reset the text to the original size + */ + public void resetTextSize() { + super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); + mMaxTextSize = mTextSize; + } + + /** + * Resize text after measuring + */ + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if(changed || mNeedsResize) { + resizeText(right - left, bottom - top); + } + super.onLayout(changed, left, top, right, bottom); + } + + /** + * Resize the text size with default width and height + */ + public void resizeText() { + int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop(); + int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight(); + resizeText(widthLimit, heightLimit); + } + + /** + * Resize the text size with specified width and height + * @param width + * @param height + */ + public void resizeText(int width, int height) { + CharSequence text = getText(); + // Do not resize if the view does not have dimensions or there is no text + if(text == null || text.length() == 0 || height <= 0 || width <= 0) { + return; + } + + // Get the text view's paint object + TextPaint textPaint = getPaint(); + + // Store the current text size + float oldTextSize = textPaint.getTextSize(); + // If there is a max text size set, use the lesser of that and the default text size + float targetTextSize = mMaxTextSize > 0 ? Math.min(mTextSize, mMaxTextSize) : mTextSize; + + // Get the required text height + int textHeight = getTextHeight(text, textPaint, width, targetTextSize); + + // Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes + while(textHeight > height && targetTextSize > mMinTextSize) { + targetTextSize = Math.max(targetTextSize - 2, mMinTextSize); + textHeight = getTextHeight(text, textPaint, width, targetTextSize); + } + + // If we had reached our minimum text size and still don't fit, append an ellipsis + if(mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) { + // Draw using a static layout + StaticLayout layout = new StaticLayout(text, textPaint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, false); + layout.draw(sTextResizeCanvas); + int lastLine = layout.getLineForVertical(height) - 1; + int start = layout.getLineStart(lastLine); + int end = layout.getLineEnd(lastLine); + float lineWidth = layout.getLineWidth(lastLine); + float ellipseWidth = textPaint.measureText(mEllipsis); + + // Trim characters off until we have enough room to draw the ellipsis + while(width < lineWidth + ellipseWidth) { + lineWidth = textPaint.measureText(text.subSequence(start, --end + 1).toString()); + } + setText(text.subSequence(0, end) + mEllipsis); + + } + + // Some devices try to auto adjust line spacing, so force default line spacing + // and invalidate the layout as a side effect + textPaint.setTextSize(targetTextSize); + setLineSpacing(mSpacingAdd, mSpacingMult); + + // Notify the listener if registered + if(mTextResizeListener != null) { + mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize); + } + + // Reset force resize flag + mNeedsResize = false; + } + + // Set the text size of the text paint object and use a static layout to render text off screen before measuring + private int getTextHeight(CharSequence source, TextPaint paint, int width, float textSize) { + // Update the text paint object + paint.setTextSize(textSize); + // Draw using a static layout + StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, false); + layout.draw(sTextResizeCanvas); + return layout.getHeight(); + } + +} diff --git a/src/main/java/org/solovyev/android/view/DragPreferencesChangeListener.java b/src/main/java/org/solovyev/android/view/DragPreferencesChangeListener.java new file mode 100644 index 00000000..69266330 --- /dev/null +++ b/src/main/java/org/solovyev/android/view/DragPreferencesChangeListener.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2009-2011. Created by serso aka se.solovyev. + * For more information, please, contact se.solovyev@gmail.com + */ + +package org.solovyev.android.view; + +import org.jetbrains.annotations.NotNull; +import org.solovyev.android.calculator.DragButtonCalibrationActivity; + +import java.util.EventListener; + +/** + * User: serso + * Date: 9/18/11 + * Time: 8:48 PM + */ +public interface DragPreferencesChangeListener extends EventListener{ + + void onDragPreferencesChange(@NotNull DragButtonCalibrationActivity.Preferences preferences ); +} diff --git a/src/main/java/org/solovyev/android/view/DragPreferencesChangeListenerRegister.java b/src/main/java/org/solovyev/android/view/DragPreferencesChangeListenerRegister.java new file mode 100644 index 00000000..0451003a --- /dev/null +++ b/src/main/java/org/solovyev/android/view/DragPreferencesChangeListenerRegister.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2009-2011. Created by serso aka se.solovyev. + * For more information, please, contact se.solovyev@gmail.com + */ + +package org.solovyev.android.view; + +import org.solovyev.common.utils.Announcer; + +/** + * User: serso + * Date: 9/18/11 + * Time: 8:53 PM + */ +public class DragPreferencesChangeListenerRegister extends Announcer { + + public DragPreferencesChangeListenerRegister() { + super(DragPreferencesChangeListener.class); + } +} diff --git a/src/main/java/org/solovyev/android/view/NumberPicker.java b/src/main/java/org/solovyev/android/view/NumberPicker.java new file mode 100644 index 00000000..071b14a6 --- /dev/null +++ b/src/main/java/org/solovyev/android/view/NumberPicker.java @@ -0,0 +1,538 @@ +/* + * Copyright (c) 2009-2011. Created by serso aka se.solovyev. + * For more information, please, contact se.solovyev@gmail.com + * or visit http://se.solovyev.org + */ + +package org.solovyev.android.view; + +/** + * User: serso + * Date: 9/18/11 + * Time: 10:03 PM + */ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import android.content.Context; +import android.os.Handler; +import android.text.InputFilter; +import android.text.InputType; +import android.text.Spanned; +import android.text.method.NumberKeyListener; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import org.solovyev.android.calculator.R; + +/** + * A view for selecting a number + * + * For a dialog using this view, see {@link android.app.TimePickerDialog}. + * @hide + */ +public class NumberPicker extends LinearLayout { + + /** + * The callback interface used to indicate the number value has been adjusted. + */ + public interface OnChangedListener { + /** + * @param picker The NumberPicker associated with this listener. + * @param oldVal The previous value. + * @param newVal The new value. + */ + void onChanged(NumberPicker picker, int oldVal, int newVal); + } + + /** + * Interface used to format the number into a string for presentation + */ + public interface Formatter { + String toString(int value); + } + + /* + * Use a custom NumberPicker formatting callback to use two-digit + * minutes strings like "01". Keeping a static formatter etc. is the + * most efficient way to do this; it avoids creating temporary objects + * on every call to format(). + */ + public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = + new NumberPicker.Formatter() { + final StringBuilder mBuilder = new StringBuilder(); + final java.util.Formatter mFmt = new java.util.Formatter( + mBuilder, java.util.Locale.US); + final Object[] mArgs = new Object[1]; + public String toString(int value) { + mArgs[0] = value; + mBuilder.delete(0, mBuilder.length()); + mFmt.format("%02d", mArgs); + return mFmt.toString(); + } + }; + + private final Handler mHandler; + private final Runnable mRunnable = new Runnable() { + public void run() { + if (mIncrement) { + changeCurrent(mCurrent + 1); + mHandler.postDelayed(this, mSpeed); + } else if (mDecrement) { + changeCurrent(mCurrent - 1); + mHandler.postDelayed(this, mSpeed); + } + } + }; + + private final EditText mText; + private final InputFilter mNumberInputFilter; + + private String[] mDisplayedValues; + + /** + * Lower value of the range of numbers allowed for the NumberPicker + */ + private int mStart; + + /** + * Upper value of the range of numbers allowed for the NumberPicker + */ + private int mEnd; + + /** + * Current value of this NumberPicker + */ + private int mCurrent; + + /** + * Previous value of this NumberPicker. + */ + private int mPrevious; + private OnChangedListener mListener; + private Formatter mFormatter; + private long mSpeed = 300; + + private boolean mIncrement; + private boolean mDecrement; + + /** + * Create a new number picker + * @param context the application environment + */ + public NumberPicker(Context context) { + this(context, null); + } + + /** + * Create a new number picker + * @param context the application environment + * @param attrs a collection of attributes + */ + public NumberPicker(Context context, AttributeSet attrs) { + super(context, attrs); + setOrientation(VERTICAL); + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.number_picker, this, true); + mHandler = new Handler(); + + OnClickListener clickListener = new OnClickListener() { + public void onClick(View v) { + validateInput(mText); + if (!mText.hasFocus()) mText.requestFocus(); + + // now perform the increment/decrement + if (R.id.increment == v.getId()) { + changeCurrent(mCurrent + 1); + } else if (R.id.decrement == v.getId()) { + changeCurrent(mCurrent - 1); + } + } + }; + + OnFocusChangeListener focusListener = new OnFocusChangeListener() { + public void onFocusChange(View v, boolean hasFocus) { + + /* When focus is lost check that the text field + * has valid values. + */ + if (!hasFocus) { + validateInput(v); + } + } + }; + + OnLongClickListener longClickListener = new OnLongClickListener() { + /** + * We start the long click here but rely on the {@link NumberPickerButton} + * to inform us when the long click has ended. + */ + public boolean onLongClick(View v) { + /* The text view may still have focus so clear it's focus which will + * trigger the on focus changed and any typed values to be pulled. + */ + mText.clearFocus(); + + if (R.id.increment == v.getId()) { + mIncrement = true; + mHandler.post(mRunnable); + } else if (R.id.decrement == v.getId()) { + mDecrement = true; + mHandler.post(mRunnable); + } + return true; + } + }; + + InputFilter inputFilter = new NumberPickerInputFilter(); + mNumberInputFilter = new NumberRangeKeyListener(); + mIncrementButton = (NumberPickerButton) findViewById(R.id.increment); + mIncrementButton.setOnClickListener(clickListener); + mIncrementButton.setOnLongClickListener(longClickListener); + mIncrementButton.setNumberPicker(this); + + mDecrementButton = (NumberPickerButton) findViewById(R.id.decrement); + mDecrementButton.setOnClickListener(clickListener); + mDecrementButton.setOnLongClickListener(longClickListener); + mDecrementButton.setNumberPicker(this); + + mText = (EditText) findViewById(R.id.timepicker_input); + mText.setOnFocusChangeListener(focusListener); + mText.setFilters(new InputFilter[] {inputFilter}); + mText.setRawInputType(InputType.TYPE_CLASS_NUMBER); + + if (!isEnabled()) { + setEnabled(false); + } + } + + /** + * Set the enabled state of this view. The interpretation of the enabled + * state varies by subclass. + * + * @param enabled True if this view is enabled, false otherwise. + */ + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + mIncrementButton.setEnabled(enabled); + mDecrementButton.setEnabled(enabled); + mText.setEnabled(enabled); + } + + /** + * Set the callback that indicates the number has been adjusted by the user. + * @param listener the callback, should not be null. + */ + public void setOnChangeListener(OnChangedListener listener) { + mListener = listener; + } + + /** + * Set the formatter that will be used to format the number for presentation + * @param formatter the formatter object. If formatter is null, String.valueOf() + * will be used + */ + public void setFormatter(Formatter formatter) { + mFormatter = formatter; + } + + /** + * Set the range of numbers allowed for the number picker. The current + * value will be automatically set to the start. + * + * @param start the start of the range (inclusive) + * @param end the end of the range (inclusive) + */ + public void setRange(int start, int end) { + setRange(start, end, null/*displayedValues*/); + } + + /** + * Set the range of numbers allowed for the number picker. The current + * value will be automatically set to the start. Also provide a mapping + * for values used to display to the user. + * + * @param start the start of the range (inclusive) + * @param end the end of the range (inclusive) + * @param displayedValues the values displayed to the user. + */ + public void setRange(int start, int end, String[] displayedValues) { + mDisplayedValues = displayedValues; + mStart = start; + mEnd = end; + mCurrent = start; + updateView(); + + if (displayedValues != null) { + // Allow text entry rather than strictly numeric entry. + mText.setRawInputType(InputType.TYPE_CLASS_TEXT | + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + } + } + + /** + * Set the current value for the number picker. + * + * @param current the current value the start of the range (inclusive) + * @throws IllegalArgumentException when current is not within the range + * of of the number picker + */ + public void setCurrent(int current) { + if (current < mStart || current > mEnd) { + throw new IllegalArgumentException( + "current should be >= start and <= end"); + } + mCurrent = current; + updateView(); + } + + /** + * Sets the speed at which the numbers will scroll when the +/- + * buttons are longpressed + * + * @param speed The speed (in milliseconds) at which the numbers will scroll + * default 300ms + */ + public void setSpeed(long speed) { + mSpeed = speed; + } + + private String formatNumber(int value) { + return (mFormatter != null) + ? mFormatter.toString(value) + : String.valueOf(value); + } + + /** + * Sets the current value of this NumberPicker, and sets mPrevious to the previous + * value. If current is greater than mEnd less than mStart, the value of mCurrent + * is wrapped around. + * + * Subclasses can override this to change the wrapping behavior + * + * @param current the new value of the NumberPicker + */ + protected void changeCurrent(int current) { + // Wrap around the values if we go past the start or end + if (current > mEnd) { + current = mStart; + } else if (current < mStart) { + current = mEnd; + } + mPrevious = mCurrent; + mCurrent = current; + notifyChange(); + updateView(); + } + + /** + * Notifies the listener, if registered, of a change of the value of this + * NumberPicker. + */ + private void notifyChange() { + if (mListener != null) { + mListener.onChanged(this, mPrevious, mCurrent); + } + } + + /** + * Updates the view of this NumberPicker. If displayValues were specified + * in {@link #setRange}, the string corresponding to the index specified by + * the current value will be returned. Otherwise, the formatter specified + * in will be used to format the number. + */ + private void updateView() { + /* If we don't have displayed values then use the + * current number else find the correct value in the + * displayed values for the current number. + */ + if (mDisplayedValues == null) { + mText.setText(formatNumber(mCurrent)); + } else { + mText.setText(mDisplayedValues[mCurrent - mStart]); + } + mText.setSelection(mText.getText().length()); + } + + private void validateCurrentView(CharSequence str) { + int val = getSelectedPos(str.toString()); + if ((val >= mStart) && (val <= mEnd)) { + if (mCurrent != val) { + mPrevious = mCurrent; + mCurrent = val; + notifyChange(); + } + } + updateView(); + } + + private void validateInput(View v) { + String str = String.valueOf(((TextView) v).getText()); + if ("".equals(str)) { + + // Restore to the old value as we don't allow empty values + updateView(); + } else { + + // Check the new value and ensure it's in range + validateCurrentView(str); + } + } + + /** + * @hide + */ + public void cancelIncrement() { + mIncrement = false; + } + + /** + * @hide + */ + public void cancelDecrement() { + mDecrement = false; + } + + private static final char[] DIGIT_CHARACTERS = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' + }; + + private NumberPickerButton mIncrementButton; + private NumberPickerButton mDecrementButton; + + private class NumberPickerInputFilter implements InputFilter { + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + if (mDisplayedValues == null) { + return mNumberInputFilter.filter(source, start, end, dest, dstart, dend); + } + CharSequence filtered = String.valueOf(source.subSequence(start, end)); + String result = String.valueOf(dest.subSequence(0, dstart)) + + filtered + + dest.subSequence(dend, dest.length()); + String str = String.valueOf(result).toLowerCase(); + for (String val : mDisplayedValues) { + val = val.toLowerCase(); + if (val.startsWith(str)) { + return filtered; + } + } + return ""; + } + } + + private class NumberRangeKeyListener extends NumberKeyListener { + + // XXX This doesn't allow for range limits when controlled by a + // soft input method! + public int getInputType() { + return InputType.TYPE_CLASS_NUMBER; + } + + @Override + protected char[] getAcceptedChars() { + return DIGIT_CHARACTERS; + } + + @Override + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + + CharSequence filtered = super.filter(source, start, end, dest, dstart, dend); + if (filtered == null) { + filtered = source.subSequence(start, end); + } + + String result = String.valueOf(dest.subSequence(0, dstart)) + + filtered + + dest.subSequence(dend, dest.length()); + + if ("".equals(result)) { + return result; + } + int val = getSelectedPos(result); + + /* Ensure the user can't type in a value greater + * than the max allowed. We have to allow less than min + * as the user might want to delete some numbers + * and then type a new number. + */ + if (val > mEnd) { + return ""; + } else { + return filtered; + } + } + } + + private int getSelectedPos(String str) { + if (mDisplayedValues == null) { + try { + return Integer.parseInt(str); + } catch (NumberFormatException e) { + /* Ignore as if it's not a number we don't care */ + } + } else { + for (int i = 0; i < mDisplayedValues.length; i++) { + /* Don't force the user to type in jan when ja will do */ + str = str.toLowerCase(); + if (mDisplayedValues[i].toLowerCase().startsWith(str)) { + return mStart + i; + } + } + + /* The user might have typed in a number into the month field i.e. + * 10 instead of OCT so support that too. + */ + try { + return Integer.parseInt(str); + } catch (NumberFormatException e) { + + /* Ignore as if it's not a number we don't care */ + } + } + return mStart; + } + + /** + * Returns the current value of the NumberPicker + * @return the current value. + */ + public int getCurrent() { + return mCurrent; + } + + /** + * Returns the upper value of the range of the NumberPicker + * @return the uppper number of the range. + */ + protected int getEndRange() { + return mEnd; + } + + /** + * Returns the lower value of the range of the NumberPicker + * @return the lower number of the range. + */ + protected int getBeginRange() { + return mStart; + } +} \ No newline at end of file diff --git a/src/main/java/org/solovyev/android/view/NumberPickerButton.java b/src/main/java/org/solovyev/android/view/NumberPickerButton.java new file mode 100644 index 00000000..7207a01b --- /dev/null +++ b/src/main/java/org/solovyev/android/view/NumberPickerButton.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2009-2011. Created by serso aka se.solovyev. + * For more information, please, contact se.solovyev@gmail.com + * or visit http://se.solovyev.org + */ + +package org.solovyev.android.view; + +/** + * User: serso + * Date: 9/18/11 + * Time: 10:04 PM + */ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.widget.ImageButton; +import org.solovyev.android.calculator.R; + +/** + * This class exists purely to cancel long click events, that got + * started in NumberPicker + */ +class NumberPickerButton extends ImageButton { + + private NumberPicker mNumberPicker; + + public NumberPickerButton(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + } + + public NumberPickerButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public NumberPickerButton(Context context) { + super(context); + } + + public void setNumberPicker(NumberPicker picker) { + mNumberPicker = picker; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + cancelLongpressIfRequired(event); + return super.onTouchEvent(event); + } + + @Override + public boolean onTrackballEvent(MotionEvent event) { + cancelLongpressIfRequired(event); + return super.onTrackballEvent(event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER) + || (keyCode == KeyEvent.KEYCODE_ENTER)) { + cancelLongpress(); + } + return super.onKeyUp(keyCode, event); + } + + private void cancelLongpressIfRequired(MotionEvent event) { + if ((event.getAction() == MotionEvent.ACTION_CANCEL) + || (event.getAction() == MotionEvent.ACTION_UP)) { + cancelLongpress(); + } + } + + private void cancelLongpress() { + if (R.id.increment == getId()) { + mNumberPicker.cancelIncrement(); + } else if (R.id.decrement == getId()) { + mNumberPicker.cancelDecrement(); + } + } + + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + if (!hasWindowFocus) { + cancelLongpress(); + } + } + +} \ No newline at end of file diff --git a/src/main/java/org/solovyev/android/view/SeekBarPreference.java b/src/main/java/org/solovyev/android/view/SeekBarPreference.java new file mode 100644 index 00000000..01b6cc0a --- /dev/null +++ b/src/main/java/org/solovyev/android/view/SeekBarPreference.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2009-2011. Created by serso aka se.solovyev. + * For more information, please, contact se.solovyev@gmail.com + * or visit http://se.solovyev.org + */ + +package org.solovyev.android.view; + +import android.preference.DialogPreference; +import android.content.Context; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.LinearLayout; +import org.jetbrains.annotations.NotNull; + + +/* The following code was written by Matthew Wiggins + * and is released under the APACHE 2.0 license + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +public class SeekBarPreference extends DialogPreference implements SeekBar.OnSeekBarChangeListener { + + private static final String androidns = "http://schemas.android.com/apk/res/android"; + + @NotNull + private SeekBar seekBar; + + @NotNull + private TextView splashText, valueText; + + @NotNull + private final Context context; + + private String dialogMessage, suffix; + + private int defaultValue, max, value = 0; + + public SeekBarPreference(Context context, AttributeSet attrs) { + super(context, attrs); + this.context = context; + + dialogMessage = attrs.getAttributeValue(androidns, "dialogMessage"); + suffix = attrs.getAttributeValue(androidns, "text"); + defaultValue = attrs.getAttributeIntValue(androidns, "defaultValue", 0); + max = attrs.getAttributeIntValue(androidns, "max", 100); + + } + + @Override + protected View onCreateDialogView() { + LinearLayout.LayoutParams params; + LinearLayout layout = new LinearLayout(context); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(6, 6, 6, 6); + + splashText = new TextView(context); + if (dialogMessage != null) + splashText.setText(dialogMessage); + layout.addView(splashText); + + valueText = new TextView(context); + valueText.setGravity(Gravity.CENTER_HORIZONTAL); + valueText.setTextSize(32); + params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.FILL_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + layout.addView(valueText, params); + + seekBar = new SeekBar(context); + seekBar.setOnSeekBarChangeListener(this); + layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + + if (shouldPersist()) + value = getPersistedInt(defaultValue); + + seekBar.setMax(max); + seekBar.setProgress(value); + return layout; + } + + @Override + protected void onBindDialogView(View v) { + super.onBindDialogView(v); + seekBar.setMax(max); + seekBar.setProgress(value); + } + + @Override + protected void onSetInitialValue(boolean restore, Object defaultValue) { + super.onSetInitialValue(restore, defaultValue); + if (restore) + value = shouldPersist() ? getPersistedInt(this.defaultValue) : 0; + else + value = (Integer) defaultValue; + } + + public void onProgressChanged(SeekBar seek, int value, boolean fromTouch) { + String t = String.valueOf(value); + valueText.setText(suffix == null ? t : t.concat(suffix)); + if (shouldPersist()) + persistInt(value); + callChangeListener(new Integer(value)); + } + + public void onStartTrackingTouch(SeekBar seek) { + } + + public void onStopTrackingTouch(SeekBar seek) { + } + + public void setMax(int max) { + this.max = max; + } + + public int getMax() { + return max; + } + + public void setProgress(int progress) { + value = progress; + if (seekBar != null) + seekBar.setProgress(progress); + } + + public int getProgress() { + return value; + } +} + diff --git a/src/main/java/org/solovyev/android/view/SimpleOnDragListener.java b/src/main/java/org/solovyev/android/view/SimpleOnDragListener.java index 90ff38e1..2f8ebf3b 100644 --- a/src/main/java/org/solovyev/android/view/SimpleOnDragListener.java +++ b/src/main/java/org/solovyev/android/view/SimpleOnDragListener.java @@ -15,7 +15,7 @@ import org.solovyev.common.utils.Point2d; import java.util.Map; -public class SimpleOnDragListener implements OnDragListener { +public class SimpleOnDragListener implements OnDragListener, DragPreferencesChangeListener { @NotNull public static final Point2d axis = new Point2d(0, 1); @@ -35,10 +35,6 @@ public class SimpleOnDragListener implements OnDragListener { this.preferences = preferences; } - public void setPreferences(@NotNull DragButtonCalibrationActivity.Preferences preferences) { - this.preferences = preferences; - } - @Override public boolean onDrag(@NotNull DragButton dragButton, @NotNull DragEvent event) { boolean result = false; @@ -119,6 +115,11 @@ public class SimpleOnDragListener implements OnDragListener { this.dragProcessor = dragProcessor; } + @Override + public void onDragPreferencesChange(@NotNull DragButtonCalibrationActivity.Preferences preferences) { + this.preferences = preferences; + } + public interface DragProcessor { boolean processDragEvent(@NotNull DragDirection dragDirection, @NotNull DragButton dragButton, @NotNull Point2d startPoint2d, @NotNull MotionEvent motionEvent);