Materialize dialogs
Bug: 26857695
Test: manual - view all four dialog types. 1) Display as fraction,
2) Display leading digits, 3) Timeout and 4) cancellation of a
long calculation.
Change-Id: I3454ae843a5e265b64d5c1b092e655a6e13180ad
diff --git a/src/com/android/calculator2/CalculatorFormula.java b/src/com/android/calculator2/CalculatorFormula.java
new file mode 100644
index 0000000..88f677c
--- /dev/null
+++ b/src/com/android/calculator2/CalculatorFormula.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+package com.android.calculator2;
+
+import android.annotation.TargetApi;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.os.Build;
+import android.text.Layout;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.ActionMode;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * TextView adapted for displaying the formula and allowing pasting.
+ */
+public class CalculatorFormula extends AlignedTextView implements MenuItem.OnMenuItemClickListener,
+ ClipboardManager.OnPrimaryClipChangedListener {
+
+ public static final String TAG_ACTION_MODE = "ACTION_MODE";
+
+ // Temporary paint for use in layout methods.
+ private final TextPaint mTempPaint = new TextPaint();
+
+ private final float mMaximumTextSize;
+ private final float mMinimumTextSize;
+ private final float mStepTextSize;
+
+ private final ClipboardManager mClipboardManager;
+
+ private int mWidthConstraint = -1;
+ private ActionMode mActionMode;
+ private ActionMode.Callback mPasteActionModeCallback;
+ private ContextMenu mContextMenu;
+ private OnPasteListener mOnPasteListener;
+ private OnTextSizeChangeListener mOnTextSizeChangeListener;
+
+ public CalculatorFormula(Context context) {
+ this(context, null /* attrs */);
+ }
+
+ public CalculatorFormula(Context context, AttributeSet attrs) {
+ this(context, attrs, 0 /* defStyleAttr */);
+ }
+
+ public CalculatorFormula(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ mClipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.CalculatorFormula, defStyleAttr, 0);
+ mMaximumTextSize = a.getDimension(
+ R.styleable.CalculatorFormula_maxTextSize, getTextSize());
+ mMinimumTextSize = a.getDimension(
+ R.styleable.CalculatorFormula_minTextSize, getTextSize());
+ mStepTextSize = a.getDimension(R.styleable.CalculatorFormula_stepTextSize,
+ (mMaximumTextSize - mMinimumTextSize) / 3);
+ a.recycle();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ setupActionMode();
+ } else {
+ setupContextMenu();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (!isLaidOut()) {
+ // Prevent shrinking/resizing with our variable textSize.
+ setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, mMaximumTextSize,
+ false /* notifyListener */);
+ setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
+ + getCompoundPaddingTop());
+ }
+
+ // Ensure we are at least as big as our parent.
+ final int width = MeasureSpec.getSize(widthMeasureSpec);
+ if (getMinimumWidth() != width) {
+ setMinimumWidth(width);
+ }
+
+ // Re-calculate our textSize based on new width.
+ mWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
+ - getPaddingLeft() - getPaddingRight();
+ final float textSize = getVariableTextSize(getText());
+ if (getTextSize() != textSize) {
+ setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, textSize, false /* notifyListener */);
+ }
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mClipboardManager.addPrimaryClipChangedListener(this);
+ onPrimaryClipChanged();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ mClipboardManager.removePrimaryClipChangedListener(this);
+ }
+
+ @Override
+ protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+ super.onTextChanged(text, start, lengthBefore, lengthAfter);
+
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString()));
+ }
+
+ private void setTextSizeInternal(int unit, float size, boolean notifyListener) {
+ final float oldTextSize = getTextSize();
+ super.setTextSize(unit, size);
+ if (notifyListener && mOnTextSizeChangeListener != null && getTextSize() != oldTextSize) {
+ mOnTextSizeChangeListener.onTextSizeChanged(this, oldTextSize);
+ }
+ }
+
+ @Override
+ public void setTextSize(int unit, float size) {
+ setTextSizeInternal(unit, size, true);
+ }
+
+ public float getMinimumTextSize() {
+ return mMinimumTextSize;
+ }
+
+ public float getMaximumTextSize() {
+ return mMaximumTextSize;
+ }
+
+ public float getVariableTextSize(CharSequence text) {
+ if (mWidthConstraint < 0 || mMaximumTextSize <= mMinimumTextSize) {
+ // Not measured, bail early.
+ return getTextSize();
+ }
+
+ // Capture current paint state.
+ mTempPaint.set(getPaint());
+
+ // Step through increasing text sizes until the text would no longer fit.
+ float lastFitTextSize = mMinimumTextSize;
+ while (lastFitTextSize < mMaximumTextSize) {
+ mTempPaint.setTextSize(Math.min(lastFitTextSize + mStepTextSize, mMaximumTextSize));
+ if (Layout.getDesiredWidth(text, mTempPaint) > mWidthConstraint) {
+ break;
+ }
+ lastFitTextSize = mTempPaint.getTextSize();
+ }
+
+ return lastFitTextSize;
+ }
+
+ private static boolean startsWith(CharSequence whole, CharSequence prefix) {
+ int wholeLen = whole.length();
+ int prefixLen = prefix.length();
+ if (prefixLen > wholeLen) {
+ return false;
+ }
+ for (int i = 0; i < prefixLen; ++i) {
+ if (prefix.charAt(i) != whole.charAt(i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Functionally equivalent to setText(), but explicitly announce changes.
+ * If the new text is an extension of the old one, announce the addition.
+ * Otherwise, e.g. after deletion, announce the entire new text.
+ */
+ public void changeTextTo(CharSequence newText) {
+ final CharSequence oldText = getText();
+ final char separator = KeyMaps.translateResult(",").charAt(0);
+ final CharSequence added = StringUtils.getExtensionIgnoring(newText, oldText, separator);
+ if (added != null) {
+ if (added.length() == 1) {
+ // The algorithm for pronouncing a single character doesn't seem
+ // to respect our hints. Don't give it the choice.
+ final char c = added.charAt(0);
+ final int id = KeyMaps.keyForChar(c);
+ final String descr = KeyMaps.toDescriptiveString(getContext(), id);
+ if (descr != null) {
+ announceForAccessibility(descr);
+ } else {
+ announceForAccessibility(String.valueOf(c));
+ }
+ } else if (added.length() != 0) {
+ announceForAccessibility(added);
+ }
+ } else {
+ announceForAccessibility(newText);
+ }
+ setText(newText, BufferType.SPANNABLE);
+ }
+
+ public boolean stopActionModeOrContextMenu() {
+ if (mActionMode != null) {
+ mActionMode.finish();
+ return true;
+ }
+ if (mContextMenu != null) {
+ mContextMenu.close();
+ return true;
+ }
+ return false;
+ }
+
+ public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
+ mOnTextSizeChangeListener = listener;
+ }
+
+ public void setOnPasteListener(OnPasteListener listener) {
+ mOnPasteListener = listener;
+ }
+
+ /**
+ * Use ActionMode for paste support on M and higher.
+ */
+ @TargetApi(Build.VERSION_CODES.M)
+ private void setupActionMode() {
+ mPasteActionModeCallback = new ActionMode.Callback2() {
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ if (onMenuItemClick(item)) {
+ mode.finish();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ mode.setTag(TAG_ACTION_MODE);
+ final MenuInflater inflater = mode.getMenuInflater();
+ return createPasteMenu(inflater, menu);
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ mActionMode = null;
+ }
+
+ @Override
+ public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
+ super.onGetContentRect(mode, view, outRect);
+ outRect.top += getTotalPaddingTop();
+ outRect.right -= getTotalPaddingRight();
+ outRect.bottom -= getTotalPaddingBottom();
+ // Encourage menu positioning over the rightmost 10% of the screen.
+ outRect.left = (int) (outRect.right * 0.9f);
+ }
+ };
+ setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ mActionMode = startActionMode(mPasteActionModeCallback, ActionMode.TYPE_FLOATING);
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Use ContextMenu for paste support on L and lower.
+ */
+ private void setupContextMenu() {
+ setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
+ @Override
+ public void onCreateContextMenu(ContextMenu contextMenu, View view,
+ ContextMenu.ContextMenuInfo contextMenuInfo) {
+ final MenuInflater inflater = new MenuInflater(getContext());
+ createPasteMenu(inflater, contextMenu);
+ mContextMenu = contextMenu;
+ for(int i = 0; i < contextMenu.size(); i++) {
+ contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorFormula.this);
+ }
+ }
+ });
+ setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ return showContextMenu();
+ }
+ });
+ }
+
+ private boolean createPasteMenu(MenuInflater inflater, Menu menu) {
+ final ClipboardManager clipboard = (ClipboardManager) getContext()
+ .getSystemService(Context.CLIPBOARD_SERVICE);
+ if (clipboard.hasPrimaryClip()) {
+ bringPointIntoView(length());
+ inflater.inflate(R.menu.paste, menu);
+ return true;
+ }
+ // Prevents the selection action mode on double tap.
+ return false;
+ }
+
+ private void paste() {
+ final ClipboardManager clipboard = (ClipboardManager) getContext()
+ .getSystemService(Context.CLIPBOARD_SERVICE);
+ final ClipData primaryClip = clipboard.getPrimaryClip();
+ if (primaryClip != null && mOnPasteListener != null) {
+ mOnPasteListener.onPaste(primaryClip);
+ }
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ if (item.getItemId() == R.id.menu_paste) {
+ paste();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onPrimaryClipChanged() {
+ final ClipData clip = mClipboardManager.getPrimaryClip();
+ if (clip == null || clip.getItemCount() == 0) {
+ setLongClickable(false);
+ return;
+ }
+ final CharSequence clipText = clip.getItemAt(0).coerceToText(getContext());
+ setLongClickable(!TextUtils.isEmpty(clipText));
+ }
+
+ public interface OnTextSizeChangeListener {
+ void onTextSizeChanged(TextView textView, float oldSize);
+ }
+
+ public interface OnPasteListener {
+ boolean onPaste(ClipData clip);
+ }
+}