65.9K
CodeProject 正在变化。 阅读更多。
Home

Android 开发入门:TouchCalculator

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (155投票s)

Aug 26, 2010

CPOL

15分钟阅读

viewsIcon

635479

downloadIcon

14009

Android 教程,附带示例样式计算器实现

TouchCalc2.jpg

下载 TouchCalculator.zip - 294.81 KB

简介

我设计和开发移动应用程序的时间不到一年,尽管我是一名专业的软件开发者已有十多年了,但这个新的移动时代让我感到兴奋。时至今日,有两个半主要平台因其在移动用户中的良好声誉而备受关注。其中两个是 iOS(前身为 iPhone OS)和 Android,而另一半则是长期争论的 Windows Phone 7。

在本教程文章中,我将通过构建一个真实的计算器应用程序(TouchCalculator)来向您介绍 Android 的通用开发原则。在本教程结束时,您将能够构建具有简单用户界面和后端业务逻辑的 Android 应用程序。

文章组织结构 

  1. 可视化(草图或模型)您的应用(初学者) 
  2. 写下您的应用程序将做什么(初学者)
  3. 编写第一个草图 UI,它很简单并且模仿您的模型(初学者)
  4. 设计您的数据结构(需要最少的 Java 经验,但不是必须的)
  5. 改进 UI 以匹配您的初始模型(Android 特有的初学者级别代码)
  6. 实现业务逻辑(需要 Java 经验,但您不必理解所有代码,只需意识到您必须单独考虑您的业务逻辑) 

准备工作和开发环境 

在开始之前,如果您是 Android 新手,我建议您阅读 Android 开发者网站上的“什么是 Android?”部分,并且我还建议您查看 Android 开发者博客。

如果您还没有配置好 Android 开发环境,请按照此链接中的说明进行操作。

TouchCalculator 模型

TouchCalculator,我们的示例简单计算器,是一款标准计算器,支持四种算术运算和三种数学函数(即平方根、倒数和百分比),以及一个内存缓冲区。

让我们从计算器的模型开始。下面是我们应用程序的彩色编码草图。我已经将 UI 分成了逻辑部分,每个部分都用颜色高亮显示。概念分离背后有原因,这将在本教程的后续部分中显现。   

tcMockup.png

那么,让我们看看模型中每个区域的意图。

  1. 我们将显示后续用户输入,直到用户按下 (=) 进行计算结果或 C 清除输入缓冲区
  2. 将显示即时用户输入和缓冲区结果
  3. 内存缓冲区值将在此区域显示
  4. 内存缓冲区操作按钮
  5. 其他操作按钮
  6. 计算按钮
  7. 算术运算按钮
  8. 数字键盘按钮
  9. 即时输入和缓冲区清除按钮

TouchCalculator 用例

我们的计算器应支持以下用例

  • 计算器应初始化输入值为 0,并且一旦用户开始按数字键盘按钮,输入捕获即开始
  • 用户将按下数字键盘按钮,我们将每个数字值追加到区域 #2 中当前的值
  • 如果用户按下区域 #5 中的小数点分隔符按钮,我们将分隔符追加到区域 #2 中当前的值
  • 当用户按下区域 #7 中的算术运算符按钮时,我们将输入缓冲区内容显示在区域 #1 中。如果输入缓冲区有足够的输入来产生结果,我们也将计算结果并将其显示在区域 #2 中
  • 当用户按下 (=) 按钮且输入缓冲区有足够的输入来产生结果时,我们将计算结果并将其显示在区域 #2 中,否则不执行任何操作。
  • 当用户按下退格按钮 (<- ) 时,我们将修剪显示在区域 #2 中值的最后一个数字
  • 当用户按下 CE 按钮时,我们将重置显示在区域 #2 中的值,使其变为 0
  • 当用户按下 C 按钮时,我们将重置输入缓冲区和显示在区域 #2 中的值,使其变为 0
  • 当用户按下 (±) 时,我们将切换显示在区域 #2 中用户输入值的符号
  • 当用户按下 MC 按钮时,我们将清除内存缓冲区
  • 当用户按下 MR 按钮时,我们将读取内存缓冲区中的值并将其显示在区域 #2 中
  • 当用户按下 MS 按钮时,我们将内存缓冲区的值设置为显示在区域 #2 中的值 
  • 当用户按下 M+ 或 M- 按钮时,我们将把显示在区域 #2 中的值加/减到内存缓冲区中现有的值
  • 当用户按下 SQRT 按钮时,我们将计算显示在区域 #2 中的值的平方根并将其显示在区域 #2 中
  • 当用户按下 1/x 按钮时,我们将计算显示在区域 #2 中的值的倒数并将其显示在区域 # 
  • 当用户按下 % 按钮时,我们将计算输入缓冲区的结果并找到结果的百分比,然后将其显示在区域 #2 中

 

创建基础用户界面

既然我们有了计算器的模型,我们就可以开始创建计算器的基础 Android 用户界面了。当您在 Eclipse 中创建一个新的 Android 项目时,您将在 res/layout 文件夹下有一个名为 main.xml 的默认布局 XML 文件。我们将在该 XML 文件中以声明式方式(而不是通过代码)创建基础计算器用户界面。使用声明式 XML 文件、布局文件来构建用户界面具有优势。使用 XML 的主要优点是您可以将应用程序的呈现与控制应用程序行为的代码分开。在团队环境中,这种分离使得设计人员和编码人员能够同时处理同一个应用程序。熟悉 HTML 的设计人员一旦熟悉 Android 布局词汇,就可以轻松地设计 Android 用户界面。您可以在此处阅读有关 Android 布局的更多信息。

现在,让我们看看如何实现一个 Android UI 来表示我们在 TouchCalculator 模型中草图中的不同 UI 部分。Android UI 的构建块是 Views 和 ViewGroups。Views 是提供屏幕布局和用户交互的基本用户界面组件。例如,Button、TextView 和 CheckBox 是扩展抽象 View 类的简单视图。ViewGroups 是复合用户界面组件,可以包含多个 Views 和其他 ViewGroups。LinearLayout 和 GridView 是我们将用于构建计算器的两个视图组。

在我们的模型中,我们实际上有四个垂直方向的线性布局部分。前三个部分用于标记为 1、2 和 3 的区域,第四个部分用于键盘,包含标记为 4、5、6、7、8 和 9 的部分。因此,我们将使用 LinearLayout ViewGroup,以便我们可以为每个部分放置 Views。这是我们的基础布局 XML

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
	
	<TextView  
	    android:id="@+id/txtStack"
	    android:layout_width="fill_parent" 
	    android:layout_height="wrap_content" 
	   	android:textSize="15sp"
	    android:gravity="right"
	    android:layout_marginTop = "3sp"
	    android:layout_marginLeft = "5sp"
	    android:layout_marginRight = "5sp"/>
   

    <TextView  
    	android:id="@+id/txtInput"
    	android:layout_width="fill_parent" 
    	android:layout_height="wrap_content" 
    	android:textSize="25sp"
    	android:gravity="right"
    	android:layout_marginLeft = "5sp"
   		android:layout_marginRight = "5sp"/>


    <TextView  
    	android:id="@+id/txtMemory"
    	android:layout_width="fill_parent" 
    	android:layout_height="wrap_content" 
    	android:textSize="15sp"
    	android:gravity="left"
        android:layout_marginLeft = "5sp"
   		android:layout_marginRight = "5sp"/>
   		
<GridView xmlns:android="http://schemas.android.com/apk/res/android" 
    android:id="@+id/grdButtons"
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent"
    android:columnWidth="90dp"
    android:numColumns="5"
    android:verticalSpacing="10dp"
    android:horizontalSpacing="10dp"
    android:stretchMode="columnWidth"
    android:gravity="center"/>
</LinearLayout>

我们将使用 TextView 来表示模型中标记为 1、2 和 3 的区域。我们将使用 GridView ViewGroup 来表示键盘。我们将通过使用 Adapter 在运行时将我们的键盘按钮放置在 GridView 的单元格中。

大多数 View 和 ViewGroup 属性都是不言自明的。您可以轻松地推断出属性的用途,实际上 Eclipse XML 编辑器在您按下 Ctrl+Space 时也会帮助您,它会显示一个属性列表。我将明确提及 id 属性,因为它具有特殊的重要性。任何视图或视图组都可以有一个 id 属性,但这并非必需。如果您想从代码中获取对 View 或 ViewGroup 的引用,您应该设置 Views 和 ViewGroups 的 id 属性。id 属性具有某种特殊符号,您可以在此处阅读更多关于它的信息。

如果您在 Eclipse XML 编辑器中我们定义的布局的“Layout”选项卡,您将看到如下所示的基础 UI。请注意,通过使用 LinearLayout,我们已成功地将每个逻辑部分垂直放置在一条直线上。

bareBones.PNG

加载和显示布局

我们定义的 XML 布局本身并不足够,因为用户无法直接通过布局与我们的应用程序进行交互。顾名思义,布局 XML 仅用于定义 UI 组件的放置和视觉方面。为了实现用户交互,我们需要 Activity 类,它控制我们 UI 的行为并实现用户交互。

当您使用 Eclipse 创建一个新的 Android 项目时,系统会要求您输入项目的详细信息,其中一项是可选的“Create Activity”复选框以及第一个 Android activity 类的名称。出于所有实际目的,我们将我们的 activity 命名为 main。Eclipse 将在 src/package 节点下自动创建一个“main.java”文件。这是我们的 main activity 类的初始内容

package com.pragmatouch.calculator;

import android.app.Activity;
import android.os.Bundle;

public class main extends Activity {

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

	}
}

如您所见,我们覆盖了基类 Activity 的 onCreate(第 10 行)方法,并执行 activity 初始化,加载布局是我们初始化过程的一部分,在覆盖的方法内部。

Line 12 中调用的 setContentView 方法属于基类 Activity,用于从由整数标识的资源(main.xml)加载 UI 布局。请注意我们如何获取 main.xml 的整数标识符。当 Android 构建您的应用程序时,它会自动生成一个名为 R 的类(可以在 /TouchCalculator/gen/com/pragmatouch/calculator/R.java 下找到),并为您的资源生成标识符常量,因为 main.xml 是一个资源,Android 会为我们生成一个常量,我们可以在代码中使用该常量来加载我们的布局。

如果您运行应用程序,您将看到一个空 UI,顶部有两个线性 TextView,没有键盘。

建模键盘按钮

在本节中,我们将使用 Java enum 类型来建模我们的键盘按钮。Java 枚举对我来说很有趣。我必须承认,我在 C# 中度过了太多年的时间,Java 枚举比 C# 枚举强大得多,因为 Java 枚举类主体可以包含方法和其他字段。

为了建模键盘按钮,我们将声明一个 KeypadButton 枚举类,该类还将包含按钮文本和类别信息。

package com.pragmatouch.calculator;

public enum KeypadButton {
  MC("MC",KeypadButtonCategory.MEMORYBUFFER)
, MR("MR",KeypadButtonCategory.MEMORYBUFFER)
, MS("MS",KeypadButtonCategory.MEMORYBUFFER)
, M_ADD("M+",KeypadButtonCategory.MEMORYBUFFER)
, M_REMOVE("M-",KeypadButtonCategory.MEMORYBUFFER)
, BACKSPACE("<-",KeypadButtonCategory.CLEAR)
, CE("CE",KeypadButtonCategory.CLEAR)
, C("C",KeypadButtonCategory.CLEAR)
, ZERO("0",KeypadButtonCategory.NUMBER)
, ONE("1",KeypadButtonCategory.NUMBER)
, TWO("2",KeypadButtonCategory.NUMBER)
, THREE("3",KeypadButtonCategory.NUMBER)
, FOUR("4",KeypadButtonCategory.NUMBER)
, FIVE("5",KeypadButtonCategory.NUMBER)
, SIX("6",KeypadButtonCategory.NUMBER)
, SEVEN("7",KeypadButtonCategory.NUMBER)
, EIGHT("8",KeypadButtonCategory.NUMBER)
, NINE("9",KeypadButtonCategory.NUMBER)
, PLUS(" + ",KeypadButtonCategory.OPERATOR)
, MINUS(" - ",KeypadButtonCategory.OPERATOR)
, MULTIPLY(" * ",KeypadButtonCategory.OPERATOR)
, DIV(" / ",KeypadButtonCategory.OPERATOR)
, RECIPROC("1/x",KeypadButtonCategory.OTHER)
, DECIMAL_SEP(",",KeypadButtonCategory.OTHER)
, SIGN("±",KeypadButtonCategory.OTHER)
, SQRT("SQRT",KeypadButtonCategory.OTHER)
, PERCENT("%",KeypadButtonCategory.OTHER)
, CALCULATE("=",KeypadButtonCategory.RESULT)
, DUMMY("",KeypadButtonCategory.DUMMY);

 CharSequence mText; // Display Text
 KeypadButtonCategory mCategory;
	
  KeypadButton(CharSequence text,KeypadButtonCategory category) {
    mText = text;
    mCategory = category;
  }

  public CharSequence getText() {
    return mText;
  }
}

我们模型中显示的每个键盘按钮都由一个枚举常量表示,每个常量还包含一个文本和一个类别值。我们还使用 Java 枚举来表示键盘按钮类别,并声明一个 KeypadButtonCategory 枚举类。

package com.pragmatouch.calculator;

public enum KeypadButtonCategory {
  MEMORYBUFFER
  , NUMBER
  , OPERATOR
  , DUMMY
  , CLEAR
  , RESULT
  , OTHER
}

我们将利用 KeypadButtonCategory 枚举为每个类别应用不同的样式,使我们的示例计算器看起来更丰富多彩。

创建键盘按钮

正如我在前面几节中所提到的,我们将使用 GridView 来显示我们的键盘。我们将使用一个五列的 GridView,并且我们将声明一个扩展 BaseAdapter 类的 Adapter,该类负责为我们的 AdapterView(在本例中为 GridView)提供数据。

package com.pragmatouch.calculator;

import android.widget.*;
import android.content.*;
import android.view.*;
import android.view.View.OnClickListener;

public class KeypadAdapter extends BaseAdapter {
  private Context mContext;
	
  public KeypadAdapter(Context c) {
    mContext = c;
  }

  public int getCount() {
    return mButtons.length;
  }

  public Object getItem(int position) {
    return mButtons[position];
  }

  public long getItemId(int position) {
    return 0;
  }

// create a new ButtonView for each item referenced by the Adapter
public View getView(int position, View convertView, ViewGroup parent) {
  Button btn;
  if (convertView == null) { // if it's not recycled, initialize some attributes
    btn = new Button(mContext);
    KeypadButton keypadButton = mButtons[position];
						
    // Set CalculatorButton enumeration as tag of the button so that we
    // will use this information from our main view to identify what to do
    btn.setTag(keypadButton);
  } 
  else {
    btn = (Button) convertView;
  }

  btn.setText(mButtons[position].getText());
  return btn;
}

// Create and populate keypad buttons array with CalculatorButton values
private KeypadButton[] mButtons = { KeypadButton.MC, KeypadButton.MR,KeypadButton.MS, KeypadButton.M_ADD, KeypadButton.M_REMOVE,
 KeypadButton.BACKSPACE, KeypadButton.CE, KeypadButton.C,KeypadButton.SIGN, KeypadButton.SQRT, 
 KeypadButton.SEVEN,KeypadButton.EIGHT, KeypadButton.NINE, KeypadButton.DIV,KeypadButton.PERCENT, 
 KeypadButton.FOUR, KeypadButton.FIVE,KeypadButton.SIX, KeypadButton.MULTIPLY, KeypadButton.RECIPROC,
 KeypadButton.ONE, KeypadButton.TWO, KeypadButton.THREE,KeypadButton.MINUS, KeypadButton.DECIMAL_SEP, 
 KeypadButton.DUMMY, KeypadButton.ZERO,KeypadButton.DUMMY,KeypadButton.PLUS, KeypadButton.CALCULATE };
}

Adapter 负责为 AdapterView 提供数据,在本例中,我们将提供给 GridView 的数据是键盘按钮实例。为了能够创建这些按钮实例,我们在第 47-52 行定义了一个 KeypadButton 枚举数组。

我们的 KeypadAdapter 通过 getView(第 28-44 行)方法实现为我们的 GridView 提供键盘按钮实例,在该方法中我们实例化或撤销我们的键盘按钮并根据它们在 GridView 上的位置设置它们的属性。

请注意代码中第 36 行的作用。我们将 KeypadButton 枚举实例设置为我们 Button 的 tag,我们将在业务逻辑实现中使用该 tag 值来识别用户按下键盘按钮时要做什么。

显示键盘

现在,我们已经定义了我们的 Adapter,是时候将 GridView 与我们的 KeypadAdapter 连接起来了。这是我们的 main.java 文件。

package com.pragmatouch.calculator;

import android.app.Activity;
import android.os.Bundle;
import android.widget.AdapterView;
import android.widget.GridView;
import android.view.View;
import android.view.View.OnClickListener;

public class main extends Activity {
 GridView mKeypadGrid;
 KeypadAdapter mKeypadAdapter;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.main);

 // Get reference to the keypad button GridView
 mKeypadGrid = (GridView) findViewById(R.id.grdButtons);


 // Create Keypad Adapter
 mKeypadAdapter = new KeypadAdapter(this);

 // Set adapter of the keypad grid
 mKeypadGrid.setAdapter(mKeypadAdapter);

 });

 mKeypadGrid.setOnItemClickListener(new OnItemClickListener() {
     public void onItemClick(AdapterView<?> parent, View v,int position, long id) {
       // This will not help us catch button clicks!
     }
 });

 }

}

请注意,我们在第 25 行创建了 KeypadAdapter 的一个实例,并在第 28 行将其设置为 GridView 的 adapter。我希望您注意到的另一件事是第 21 行的 findViewById 调用。正如我在上一节中指出的,我们使用了在布局 XML(main.xml)中为 GridView 分配的 id 属性的值。

此时运行 TouchCalculator,您将看到以下 UI,与我们的模型几乎相同。

tc1.PNG

 

实现业务逻辑

现在是时候实现我们计算器的业务逻辑了。我们将利用 Java Stack 类来实现我们的计算逻辑。我们将有两个 Stack 实例;第一个实例将保存用户输入,第二个栈将保存中间计算结果。我们将业务逻辑封装在我们 main 类中的一个实例方法中。这是我们的 ProcessKeypadInput 方法以及一些实用方法。

private void ProcessKeypadInput(KeypadButton keypadButton) {
		// Toast.makeText(this, keypadButton.getText(),
		// Toast.LENGTH_SHORT).show();
		String text = keypadButton.getText().toString();
		String currentInput = userInputText.getText().toString();

		int currentInputLen = currentInput.length();
		String evalResult = null;
		double userInputValue = Double.NaN;

		switch (keypadButton) {
		case BACKSPACE: // Handle backspace
			// If has operand skip backspace
			if (resetInput)
				return;

			int endIndex = currentInputLen - 1;

			// There is one character at input so reset input to 0
			if (endIndex < 1) {
				userInputText.setText("0");
			}
			// Trim last character of the input text
			else {
				userInputText.setText(currentInput.subSequence(0, endIndex));
			}
			break;
		case SIGN: // Handle -/+ sign
			// input has text and is different than initial value 0
			if (currentInputLen > 0 && currentInput != "0") {
				// Already has (-) sign. Remove that sign
				if (currentInput.charAt(0) == '-') {
					userInputText.setText(currentInput.subSequence(1,
							currentInputLen));
				}
				// Prepend (-) sign
				else {
					userInputText.setText("-" + currentInput.toString());
				}
			}
			break;
		case CE: // Handle clear input
			userInputText.setText("0");
			break;
		case C: // Handle clear input and stack
			userInputText.setText("0");
			clearStacks();
			break;
		case DECIMAL_SEP: // Handle decimal seperator
			if (hasFinalResult || resetInput) {
				userInputText.setText("0" + mDecimalSeperator);
				hasFinalResult = false;
				resetInput = false;
			} else if (currentInput.contains("."))
				return;
			else
				userInputText.append(mDecimalSeperator);
			break;
		case DIV:
		case PLUS:
		case MINUS:
		case MULTIPLY:
			if (resetInput) {
				mInputStack.pop();
				mOperationStack.pop();
			} else {
				if (currentInput.charAt(0) == '-') {
					mInputStack.add("(" + currentInput + ")");
				} else {
					mInputStack.add(currentInput);
				}
				mOperationStack.add(currentInput);
			}

			mInputStack.add(text);
			mOperationStack.add(text);

			dumpInputStack();
			evalResult = evaluateResult(false);
			if (evalResult != null)
				userInputText.setText(evalResult);

			resetInput = true;
			break;
		case CALCULATE:
			if (mOperationStack.size() == 0)
				break;

			mOperationStack.add(currentInput);
			evalResult = evaluateResult(true);
			if (evalResult != null) {
				clearStacks();
				userInputText.setText(evalResult);
				resetInput = false;
				hasFinalResult = true;
			}
			break;
		case M_ADD: // Add user input value to memory buffer
			userInputValue = tryParseUserInput();
			if (Double.isNaN(userInputValue))
				return;
			if (Double.isNaN(memoryValue))
				memoryValue = 0;
			memoryValue += userInputValue;
			displayMemoryStat();

			hasFinalResult = true;

			break;
		case M_REMOVE: // Subtract user input value to memory buffer
			userInputValue = tryParseUserInput();
			if (Double.isNaN(userInputValue))
				return;
			if (Double.isNaN(memoryValue))
				memoryValue = 0;
			memoryValue -= userInputValue;
			displayMemoryStat();
			hasFinalResult = true;
			break;
		case MC: // Reset memory buffer to 0
			memoryValue = Double.NaN;
			displayMemoryStat();
			break;
		case MR: // Read memoryBuffer value
			if (Double.isNaN(memoryValue))
				return;
			userInputText.setText(doubleToString(memoryValue));
			displayMemoryStat();
			break;
		case MS: // Set memoryBuffer value to user input
			userInputValue = tryParseUserInput();
			if (Double.isNaN(userInputValue))
				return;
			memoryValue = userInputValue;
			displayMemoryStat();
			hasFinalResult = true;
			break;
		default:
			if (Character.isDigit(text.charAt(0))) {
				if (currentInput.equals("0") || resetInput || hasFinalResult) {
					userInputText.setText(text);
					resetInput = false;
					hasFinalResult = false;
				} else {
					userInputText.append(text);
					resetInput = false;
				}

			}
			break;

		}

	}

	private void clearStacks() {
		mInputStack.clear();
		mOperationStack.clear();
		mStackText.setText("");
	}

	private void dumpInputStack() {
		Iterator<String> it = mInputStack.iterator();
		StringBuilder sb = new StringBuilder();

		while (it.hasNext()) {
			CharSequence iValue = it.next();
			sb.append(iValue);

		}

		mStackText.setText(sb.toString());
	}

	private String evaluateResult(boolean requestedByUser) {
		if ((!requestedByUser && mOperationStack.size() != 4)
				|| (requestedByUser && mOperationStack.size() != 3))
			return null;

		String left = mOperationStack.get(0);
		String operator = mOperationStack.get(1);
		String right = mOperationStack.get(2);
		String tmp = null;
		if (!requestedByUser)
			tmp = mOperationStack.get(3);

		double leftVal = Double.parseDouble(left.toString());
		double rightVal = Double.parseDouble(right.toString());
		double result = Double.NaN;

		if (operator.equals(KeypadButton.DIV.getText())) {
			result = leftVal / rightVal;
		} else if (operator.equals(KeypadButton.MULTIPLY.getText())) {
			result = leftVal * rightVal;

		} else if (operator.equals(KeypadButton.PLUS.getText())) {
			result = leftVal + rightVal;
		} else if (operator.equals(KeypadButton.MINUS.getText())) {
			result = leftVal - rightVal;

		}

		String resultStr = doubleToString(result);
		if (resultStr == null)
			return null;

		mOperationStack.clear();
		if (!requestedByUser) {
			mOperationStack.add(resultStr);
			mOperationStack.add(tmp);
		}

		return resultStr;
	}

	private String doubleToString(double value) {
		if (Double.isNaN(value))
			return null;

		long longVal = (long) value;
		if (longVal == value)
			return Long.toString(longVal);
		else
			return Double.toString(value);

	}

	private double tryParseUserInput() {
		String inputStr = userInputText.getText().toString();
		double result = Double.NaN;
		try {
			result = Double.parseDouble(inputStr);

		} catch (NumberFormatException nfe) {}
		return result;

	}

	private void displayMemoryStat() {
		if (Double.isNaN(memoryValue)) {
			memoryStatText.setText("");
		} else {
			memoryStatText.setText("M = " + doubleToString(memoryValue));
		}
	}

每次用户按下键盘按钮时都会调用 ProcessKeypadInput 方法,我们通过 switch/case 代码块决定该做什么。我们还有一些辅助方法

  • clearStacks,用于清除我们的堆栈和用户输入 TextView
  • dumpInputStack,用于将输入堆栈作为单行字符串转储到我们模型中区域 #1 表示的 TextView
  • evaluateResult,在用户按下算术运算符键盘按钮或 (=) 键盘按钮时调用。在此方法中,我们尝试通过从操作堆栈中弹出值来计算结果,如果我们能够计算出结果,则将结果作为第一项推到操作堆栈
  • doubleToString,用于将 double 值转换为 String 的实用方法
  • tryParseUserInput,我们尝试将用户输入(在我们的模型中由区域 #2 表示)解析为有效 double 值的实用方法
  • displayMemoryStat,用于将内存缓冲区状态和值转储到我们模型中区域 #3 的实用方法

调用 ProcessKeypadInput 方法的正确位置

在“显示键盘”部分,请查看第 34 行的注释。如果您阅读 Android 开发者网站上的 GridView 示例,您可能会认为这一行是放置 ProcessKeypadInput 方法调用的正确位置,因为用户将单击键盘按钮,而键盘按钮反过来是我们 GridView 的项,这将触发 OnItemClick 事件。在我们的情况下,这个假设是完全错误的。如果您将 ProcessKeypadInput 方法的调用放在该行并运行您的应用程序,即使按下了按钮,我们的 GridView 的 OnItemClick 也不会被触发。造成这种误解的原因是:由于放置在 GridView 单元格中的 Button 视图是可点击的,当用户单击按钮时,Button 视图会处理该单击,并且该操作不会传播到 GridView。

这个问题的解决方案非常直接;我们必须设置键盘 Button 视图的 OnClickListener,我们将在 KeypadAdapter 类中执行此操作。下面是我们修改后的 KeypadAdapter 类版本。

package com.pragmatouch.calculator;

import android.widget.*;
import android.content.*;
import android.view.*;
import android.view.View.OnClickListener;

public class KeypadAdapter extends BaseAdapter {
	private Context mContext;

	// Declare button click listener variable
	private OnClickListener mOnButtonClick;

	public KeypadAdapter(Context c) {
		mContext = c;
	}

	// Method to set button click listener variable
	public void setOnButtonClickListener(OnClickListener listener) {
		mOnButtonClick = listener;
	}

	public int getCount() {
		return mButtons.length;
	}

	public Object getItem(int position) {
		return mButtons[position];
	}

	public long getItemId(int position) {
		return 0;
	}

	// create a new ButtonView for each item referenced by the Adapter
	public View getView(int position, View convertView, ViewGroup parent) {
		Button btn;
		if (convertView == null) { // if it's not recycled, initialize some
									// attributes

			btn = new Button(mContext);
			KeypadButton keypadButton = mButtons[position];
			if (keypadButton != KeypadButton.DUMMY)
               btn.setOnClickListener(mOnButtonClick);

			// Set CalculatorButton enumeration as tag of the button so that we
			// will use this information from our main view to identify what to
			// do
			btn.setTag(keypadButton);
		} else {
			btn = (Button) convertView;
		}

		btn.setText(mButtons[position].getText());
		return btn;
	}

	// Create and populate keypad buttons array with CalculatorButton enum
	// values
	private KeypadButton[] mButtons = { KeypadButton.MC, KeypadButton.MR,
	  KeypadButton.MS, KeypadButton.M_ADD, KeypadButton.M_REMOVE,
	  KeypadButton.BACKSPACE, KeypadButton.CE, KeypadButton.C,
	  KeypadButton.SIGN, KeypadButton.SQRT, KeypadButton.SEVEN,
	  KeypadButton.EIGHT, KeypadButton.NINE, KeypadButton.DIV,
	  KeypadButton.PERCENT, KeypadButton.FOUR, KeypadButton.FIVE,
	  KeypadButton.SIX, KeypadButton.MULTIPLY, KeypadButton.RECIPROC,
	  KeypadButton.ONE, KeypadButton.TWO, KeypadButton.THREE,
	  KeypadButton.MINUS, KeypadButton.DECIMAL_SEP, KeypadButton.DUMMY,
	  KeypadButton.ZERO, KeypadButton.DUMMY, KeypadButton.PLUS,
	KeypadButton.CALCULATE };

}

我们在第 12 行声明了 mOnButtonClick 字段,并在第 19-21 行定义了一个该字段的 setter 方法。然后我们在第 43-44 行将我们的 Button 视图的 OnClickListener 设置为 mOnButtonClick 值。

为了捕获按钮单击并调用 ProcessKeypadInput,我们还必须修改我们的 main 类。

package com.pragmatouch.calculator;

import android.app.Activity;
import android.os.Bundle;
import android.widget.AdapterView;
import android.widget.GridView;
import android.view.View;
import android.view.View.OnClickListener;

public class main extends Activity {
 GridView mKeypadGrid;
 KeypadAdapter mKeypadAdapter;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.main);

 // Get reference to the keypad button GridView
 mKeypadGrid = (GridView) findViewById(R.id.grdButtons);


 // Create Keypad Adapter
 mKeypadAdapter = new KeypadAdapter(this);

 // Set adapter of the keypad grid
 mKeypadGrid.setAdapter(mKeypadAdapter);

 // Set button click listener of the keypad adapter
 mKeypadAdapter.setOnButtonClickListener(new OnClickListener() {
 @Override
 public void onClick(View v) {
   Button btn = (Button) v;
   // Get the KeypadButton value which is used to identify the
   // keypad button from the Button's tag
   KeypadButton keypadButton = (KeypadButton) btn.getTag();
   
   // Process keypad button
   ProcessKeypadInput(keypadButton);
  }});
 });

 mKeypadGrid.setOnItemClickListener(new OnItemClickListener() {
     public void onItemClick(AdapterView<?> parent, View v,int position, long id) {
       // This will not help us catch button clicks!
     }
 });

 }

}

我们在第 31-42 行设置了 KeypadAdapter 类的 OnButtonClickListener。

我们完成了吗?

我们有了一个功能齐全的标准计算器(实际上,我将 SQRT、1/x 和 % 的实现留给您作为练习),但总有改进的空间和视觉样式。在最后一个部分,我将向您展示一个用于样式化我们的键盘按钮和我们在 UI 中使用的 TextView 的简单示例。有关使用样式和主题的更多信息,请访问此链接。

为键盘应用样式

Android 拥有许多不同的资源类型,通过利用这些资源,可以在不同级别上样式化您的 Android 应用程序的 UI。作为简单示例,我们将使用 Drawable 资源类型(更具体地说,是 State List 可绘制资源类型)进行样式设置。您可以在此链接中查看更多资源类型。

首先,我们必须将 State List 可绘制项定义为 res/drawable 下的一个 XML 文件。如果您安装了最新的 Android SDK,您会在 res 文件夹下看到 drawable-hdpi、drawable-ldpi,但没有 drawable 文件夹。如果 drawable 文件夹不存在,您可以安全地创建一个。在确保 drawable 文件夹存在后,添加一个名为 keypadclear1.xml 的 XML 文件。在 keypadclear1.xml 中,您将有以下标记代码,它为 Button 视图定义了一个样式。

<?xml version="1.0" encoding="utf-8"?>
<selector
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" >
        <shape>
            <gradient
                android:startColor="#ff8c00"
                android:endColor="#FFFFFF"
                android:angle="270" />
            <stroke
                android:width="2dp"
                android:color="#dcdcdc" />
            <corners
                android:radius="2dp" />
            <padding
                android:left="10dp"
                android:top="10dp"
                android:right="10dp"
                android:bottom="10dp" />
        </shape>
    </item>

    <item android:state_focused="true" >
        <shape>
            <gradient
                android:startColor="#ffc2b7"
                android:endColor="#ffc2b7"
                android:angle="270" />
            <stroke
                android:width="2dp"
                android:color="#dcdcdc" />
            <corners
                android:radius="2dp" />
            <padding
                android:left="10dp"
                android:top="10dp"
                android:right="10dp"
                android:bottom="10dp" />
        </shape>
    </item>

    <item>        
        <shape>
            <gradient
                android:startColor="#ff9d77"
                android:endColor="#ff9d77"
                android:angle="270" />
            <stroke
                android:width="2dp"
                android:color="#fad3cf" />
            <corners
                android:radius="2dp" />
            <padding
                android:left="10dp"
                android:top="10dp"
                android:right="10dp"
                android:bottom="10dp" />
        </shape>
    </item>
</selector>

keypadclear1.xml 包含 Button 视图的三个可能状态的单独样式定义。现在我们已经准备好了 State List 可绘制资源,我们必须编写一些代码来使用此资源。我们将设置 KeypadButtonCategory.CLEAR 的键盘按钮的背景,以根据 keypadclear1.xml 的定义进行样式设置。我们将稍微修改 KeypadAdapter 类的 getView 方法

	// create a new ButtonView for each item referenced by the Adapter
	public View getView(int position, View convertView, ViewGroup parent) {
		Button btn;
		if (convertView == null) { // if it's not recycled, initialize some attributes

			btn = new Button(mContext);
			KeypadButton keypadButton = mButtons[position];
			
			if (keypadButton != KeypadButton.DUMMY)
               btn.setOnClickListener(mOnButtonClick);
            
			if(keypadButton != KeypadButton.CLEAR)
			   btn.setBackgroundResource(R.drawable.keypadclear1);
			
			// Set CalculatorButton enumeration as tag of the button so that we
			// will use this information from our main view to identify what to do
			btn.setTag(keypadButton);
		} else {
			btn = (Button) convertView;
		}

		btn.setText(mButtons[position].getText());
		return btn;
	}

在第 13 行,如果我们的 Button 视图被归类为 KeypadButtonCategory.CLEAR,我们就会设置它们的背景资源,仅此而已,我们就有了模型中区域 #9 中草图的粉红色圆角按钮。

成就

我们熟悉了 Android 应用程序开发,并评估了 Android 应用程序 UI 的不同方面。我们开发了一个样式化的计算器,它看起来像下面附带的图片。

TouchCalc.PNG

TouchCalculator 是否有改进的空间?

是的。总有改进的空间,我故意将一些业务逻辑的实现留给您。以下是可能的改进列表

  • 实现 SQRT
  • 实现 1/x(倒数)
  • 实现 %(百分比)
  • 妥善处理除以零的情况
  • 实现不同的样式并向用户提供“切换主题”对话框
  • 实现科学、程序员和统计模式,并允许用户切换模式

 

其他说明

如何修复编辑 res/values/strings.xml 资源时出现的 Eclipse SDK 3.6 null pointer exception?

使用文本编辑器打开 res\values\strings.xml,并将 <resources> 替换为 <resources xmlns:android="http://schemas.android.com/apk/res/android">

 

© . All rights reserved.