前言
Calculator作为日常生活的必备工具之一,也被Android收录在了它的原生App之中,但由于Google并没有过多的开发这款App,只实现了简单计算和一般的科学计算,历史记录功能在代码中实现了但是在App的界面上却怎么也找不到历史记录的入口,不懂这是不是Google有意而为之。大伙现在买的Android手机上面的Calculator一般是经过定制的,如果你发现你手机上的Calculaotr和原生的一样的话,那你这款手机的厂商绝对不够良心。
下面先看一下Calculator的界面:
如你所看到的,原生的Calculator的界面就是这么的简单,其实Google这样做也是为开发者着想,让我们这些开发者有口饭吃。Calculator存在横屏界面,与上面的基本一致就不再次多赘述。
Calculator源码下载地址:http://pan.baidu.com/s/1dDnHw2l
工程目录构成
Calculator启动分析
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.app.calculator2">
<original-package android:name="com.android.app.calculator2" />
<uses-sdk android:minSdkVersion="14"
android:targetSdkVersion="20"/>
<application android:label="@string/app_name" android:icon="@mipmap/ic_launcher_calculator">
<activity android:name="Calculator"
android:theme="@android:style/Theme.Holo.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.APP_CALCULATOR" />
</intent-filter>
</activity>
</application>
</manifest>
由上面的代码可以知道,Calculator.java是这个App默认启动Activity,我们就到其中一探究竟。
setContentView(R.layout.main);
mPager = (ViewPager) findViewById(R.id.panelswitch);
if (mPager != null) {
mPager.setAdapter(new PageAdapter(mPager));
} else {
// Single page UI
final TypedArray buttons = getResources().obtainTypedArray(R.array.buttons);
for (int i = 0; i < buttons.length(); i++) {
setOnClickListener(null, buttons.getResourceId(i, 0));
}
buttons.recycle();
}
加载布局文件,获得布局文件根节点下的panelswitch节点赋值给mPager对象,看这名字起的有点玄乎,现在还看不出它的作用。往下看,一般会走if这一段。
class PageAdapter extends PagerAdapter { //果然是一个绑定视图的适配器
private View mSimplePage;
private View mAdvancedPage;
public PageAdapter(ViewPager parent) {
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final View simplePage = inflater.inflate(R.layout.simple_pad, parent, false);
final View advancedPage = inflater.inflate(R.layout.advanced_pad, parent, false);
mSimplePage = simplePage;
mAdvancedPage = advancedPage;
final Resources res = getResources();
final TypedArray simpleButtons = res.obtainTypedArray(R.array.simple_buttons);
for (int i = 0; i < simpleButtons.length(); i++) {
setOnClickListener(simplePage, simpleButtons.getResourceId(i, 0));
}
simpleButtons.recycle();
final TypedArray advancedButtons = res.obtainTypedArray(R.array.advanced_buttons);
for (int i = 0; i < advancedButtons.length(); i++) {
setOnClickListener(advancedPage, advancedButtons.getResourceId(i, 0));
}
advancedButtons.recycle();
final View clearButton = simplePage.findViewById(R.id.clear);
if (clearButton != null) {
mClearButton = clearButton;
}
final View backspaceButton = simplePage.findViewById(R.id.del);
if (backspaceButton != null) {
mBackspaceButton = backspaceButton;
}
}
@Override
public int getCount() {
return 2;
}
@Override
public void startUpdate(View container) {
}
@Override
public Object instantiateItem(View container, int position) {
final View page = position == 0 ? mSimplePage : mAdvancedPage;
((ViewGroup) container).addView(page);
return page;
}
@Override
public void destroyItem(View container, int position, Object object) {
((ViewGroup) container).removeView((View) object);
}
@Override
public void finishUpdate(View container) {
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Parcelable saveState() {
return null;
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
}
}
通过上面的代码可以知道,这个构造函数的作用就是用来获得mSimplePage和mAdvancedPage两个视图类,通过查看R.layout.simple_pad和R.layout.advanced_pad两个布局文件我们可以知道mSimplePage和mAdvancedPage就是基本面板(第一张图所示的输入部分)和高级面板(第二张图的输入部分)
知道了这两个货的作用下面的代码也就好懂了,simpleButtons和advancedButtons是两个数组,存放的元素就是对应mSimplePage和mAdvancedPage上每个按钮的Id。 然后再通过setOnClickListener这个方法给每个按钮加上时间监听mListener,mListener是EventListener的实例,EventListener一共实现了View.OnKeyListener, View.OnClickListener和View.OnLongClickListener三个事件接口为后面的输入做准备。
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="match_parent">
<!-- marginRight has to be 0 to catch border-touch -->
<com.android.app.calculator2.ColorButton
android:id="@+id/clear"
android:text="@string/clear"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginRight="0dp"
android:textSize="15dp"
style="@style/button_style"
android:minWidth="89dip"
/>
<!-- marginRight has to be 0 to catch border-touch -->
<com.android.app.calculator2.ColorButton
android:id="@+id/del"
android:text="@string/del"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginRight="0dp"
android:textSize="15dp"
style="@style/button_style"
android:contentDescription="@string/delDesc"
android:ellipsize="end"
android:minWidth="89dip"
/>
</FrameLayout>
接着是获得Persist的对象mPersist并调用它的load方法,这里我们来看看Persist是干嘛的,load又做了什么事情?
public void load() {
try {
InputStream is = new BufferedInputStream(mContext.openFileInput(FILE_NAME), 8192);
DataInputStream in = new DataInputStream(is);
int version = in.readInt();
if (version > 1) {
mDeleteMode = in.readInt();
} else if (version > LAST_VERSION) {
throw new IOException("data version " + version + "; expected " + LAST_VERSION);
}
history = new History(version, in);
in.close();
} catch (FileNotFoundException e) {
Calculator.log("" + e);
} catch (IOException e) {
Calculator.log("" + e);
}
}
Persist的意思有“坚持”这里应该是“持久化数据”的意思。在load方法中我们可以看到有一系列的流操作。
首先获得一个in,对应的文件是FILE_NAME = "calculator.data",然后读取一个整数version,这里暂时还不知道version是干嘛用的先放一放。
接着获得History的对象history,我们进History的构造函数看看它究竟做了些什么?
History(int version, DataInput in) throws IOException {
if (version >= VERSION_1) {
int size = in.readInt();
for (int i = 0; i < size; ++i) {
mEntries.add(new HistoryEntry(version, in));
}
mPos = in.readInt();
} else {
throw new IOException("invalid version " + version);
}
}
假设这里走if这一段,又是一个循环,根据size的大小,向mEntries中添加HistoryEntry的实例。我们继续进入HistoryEntry的构造函数看看:
HistoryEntry(int version, DataInput in) throws IOException {
if (version >= VERSION_1) {
mBase = in.readUTF();
mEdited = in.readUTF();
Log.d("zql", "" + mBase + " " + mEdited) ;
//Calculator.log("load " + mEdited);
} else {
throw new IOException("invalid version " + version);
}
}
终于到底了,这里通过in输入流获得mBase和mEdited,这两个字段具体是什么东西我们可以用Log打印出来看看:
看出来是什么了吧?这两个字段里面存的都是我之前输入的表达式,但有一些是空的,还有?号,这个这里还看不出来,在后面写数据的时候可以知道。
我们再来看看calculator.data这个文件究竟在哪里?
看到了吧,它在data/data/com.android.app.calculator2/files/目录下。
onCreate方法继续往下走,将前面你的history赋值给当前类的mHistory,这么做的目的大家应该很清楚,是为了在将来用到这里的历史记录。然后是获得CalculatorDisplay的实例mDisplay。mDisplay是啥呢?其实就是Calculator的显示输入表达式的区域。
接着实例化Logic,我们还是进入Logic的构造函数:
Logic(Context context, History history, CalculatorDisplay display) {
mContext = context;
mErrorString = mContext.getResources().getString(R.string.error);
mHistory = history;
mDisplay = display;
mDisplay.setLogic(this);
}
这个Logic相当于一个逻辑的控制器,在构造函数中它获得了前面的历史记录mHistory和mDisplay。这样mLogic就能操作mHistory和mDisplay了。
继续往下走(何处是个头啊~)
给mLogic设置了字段,这里我们不多讨论。接着是获得一个HistoryAdapter对象,然后跟mHistory进行绑定。接着一个if语句是用来显示当前面板是mSimplePage还是mAdvancedPage。
接着看mListener.setHandler(mLogic, mPager)这个方法
void setHandler(Logic handler, ViewPager pager) {
mHandler = handler;
mPager = pager;
}
还记得前面给每一个Button设置事件监听mListener吗?这里给mListener设置了它的逻辑处理器为mLogic,由此我们可以知道,当我们点击按钮后,事件响应的处理部分肯定是由mLogic来完成,是不是这样我们后面继续看。
接着给mDisplay设置监听,这里的监听应该是监听用户点击表达式输入域来改变光标的位置(我这么说能想象出来么?)
if (!ViewConfiguration.get(this).hasPermanentMenuKey()) {
createFakeMenu();
}
这段代码是查看手机是否有“菜单”这个实体按键,如果没有则在App界面上显示mOverflowMenuButton这个按钮,用来显示菜单。这里我用的测试手机是小米1s,是具有“菜单”按键的,所以界面如上面的截图所示,但如果我在这里把if判断去掉,直接调用createFakeMenu,看看界面会变成什么样?
接着看onCreate,mLogic.resumeWithHistory()是去都历史记录,看看具体怎么做的?
public void resumeWithHistory() {
clearWithHistory(false);
}
private void clearWithHistory(boolean scroll) {
String text = mHistory.getText();
if (MARKER_EVALUATE_ON_RESUME.equals(text)) {
if (!mHistory.moveToPrevious()) {
text = "";
}
text = mHistory.getText();
evaluateAndShowResult(text, CalculatorDisplay.Scroll.NONE);
} else {
mResult = "";
mDisplay.setText(
text, scroll ? CalculatorDisplay.Scroll.UP : CalculatorDisplay.Scroll.NONE);
mIsError = false;
}
}
首先调用mHistory.getText()
HistoryEntry current() {
return mEntries.elementAt(mPos);
}
String getText() {
return current().getEdited();
}
mHistory.getText()获得之前读取的历史记录的最后一个HistoryEntry的mEdited字段,接着看,
如果mEdited等于MARKER_EVALUATE_ON_RESUME即?的话历史记录向前移动一个,即mPos-1,去获得前一个历史记录的mEdited字段,然后调用evaluateAndShowResult这个方法来计算和显示你退出Calculator前最后一次计算结果。evaluateAndShowResult这个方法相对较复杂,我们后面分析。
如果mEdited等于“”的话,直接显示“”。这里我猜测?的作用就是用来判断是否显示最后一侧计算结果的。
onCreate中最后一行updateDeleteMode()
private void updateDeleteMode() {
if (mLogic.getDeleteMode() == Logic.DELETE_MODE_BACKSPACE) {
mClearButton.setVisibility(View.GONE);
mBackspaceButton.setVisibility(View.VISIBLE);
} else {
mClearButton.setVisibility(View.VISIBLE);
mBackspaceButton.setVisibility(View.GONE);
}
}
这个方法的作用是根据mDeleteMode这个值来更新删除按钮,选择显示mBackspaceButton或mClearButton。
mClearButton的作用是清楚计算结果,mBackspaceButton的作用的一个一个字符的清除输入表达式。
到这里整个oncCeate方法就分析完了,总结一下onCreate方法做了哪些事情:
- 加载视图
- 加载历史记录
- 判断是否显示最后一次的计算结果
- 更新删除按钮
文章写的比较仓促,有问题的地方欢迎指出。
在下一篇中我会重点分析Calculator的计算过程的实现~