Android原生计算器源码分析

今天逛Google Play无意间发现Google把原生计算器应用单独发布了,

这几年虽然一直坚持使用原生Android系统,但由于计算器这种工具软件使用频率较低(也就每个月填报销的时候打开用一下),一直没怎么特别留意,今天特意使用了一会,发现设计还是有很多亮点的,符合Google简洁、易用的风格。

这是打开应用后的主页:

这是滑动显示数学公式后的页面:

为了描述方便,我们把第一页(也就是包含0-9数字和常用+-×/=操作的页面)称为page1,把显示数学计算公式的页面称为page2,手指在page1向左滑动,page2会从屏幕后侧缓缓滑入,滑动的过程中page1位置保持不动,page2完全滑入后也没有把page1完全覆盖,左边还是留有间距的,想回到page1只需要往右一滑就可以了,而这个过程中page2则是从屏幕右侧缓缓滑出。

有点像水平方向的抽屉,设计比较新颖,看一下它的源码。

先从googlesourse把源码clone到本地

1
git clone https://android.googlesource.com/platform/packages/apps/Calculator

然后导入Android Studio,从清单文件找到应用的入口Activity————Calculator.java,看一下它的布局文件,由于计算器的源码适配了不同分辨率、不同屏幕方向的的手机和pad,我们只分析一种就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
layout="@layout/display"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.android.calculator2.CalculatorPadViewPager
android:id="@+id/pad_pager"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<include layout="@layout/pad_numeric" />
<include layout="@layout/pad_operator_one_col" />
</LinearLayout>
<include layout="@layout/pad_advanced" />
</com.android.calculator2.CalculatorPadViewPager>
</LinearLayout>

根布局是LinearLayout,有两个子view,垂直显示。由第二个CalculatorPadViewPager的android:layout_weight=”1”可以看出这两个子view是按照1:1的高度平分整个屏幕的。@layout/display是显示输入的数字和运算结果的,就是两个EditText,我们不用关注。重点看一下下面的布局。

CalculatorPadViewPager继承于ViewPager,相关代码一会再看。它包含两个子view,一个LinearLayout,一个pad_advanced的layout,打开pad_advanced.xml可以看到这就是显示各种数学公式的view。上面的@layout/pad_numeric是数字的layout,4行3列,@layout/pad_operator_one_col是+-×/=操作的布局,5行1列,这两个布局水平方向占据的宽度根据style里定义的layout_weight进行计算,比较简单这里就不贴代码了。

布局看完之后可能会有疑问,既然计算器底部的布局其实就是一个ViewPager+2两个子View的构成,那么相对于计算器有几个疑问:
1:为什么在显示page1的时候page2会显示出来一部分呢(右侧青色那一条)?
2:为什么向左滑动的时候page1的位置一直保持不动?不应该是向左侧滑出吗?
3:还有就是当page2滑出后宽度为什么没有铺满、左边还要留出一部分透明区域呢?
要搞清这几个问题,我们重点看一下CalculatorPadViewPager的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public class CalculatorPadViewPager extends ViewPager {
private final PagerAdapter mStaticPagerAdapter = new PagerAdapter() {
@Override
public int getCount() {
return getChildCount();
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
return getChildAt(position);
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
removeViewAt(position);
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public float getPageWidth(int position) {
return position == 1 ? 7.0f / 9.0f : 1.0f;
}
};
private final OnPageChangeListener mOnPageChangeListener = new SimpleOnPageChangeListener() {
private void recursivelySetEnabled(View view, boolean enabled) {
if (view instanceof ViewGroup) {
final ViewGroup viewGroup = (ViewGroup) view;
for (int childIndex = 0; childIndex < viewGroup.getChildCount(); ++childIndex) {
recursivelySetEnabled(viewGroup.getChildAt(childIndex), enabled);
}
} else {
view.setEnabled(enabled);
}
}
@Override
public void onPageSelected(int position) {
if (getAdapter() == mStaticPagerAdapter) {
for (int childIndex = 0; childIndex < getChildCount(); ++childIndex) {
// Only enable subviews of the current page.
recursivelySetEnabled(getChildAt(childIndex), childIndex == position);
}
}
}
};
private final PageTransformer mPageTransformer = new PageTransformer() {
@Override
public void transformPage(View view, float position) {
if (position < 0.0f) {
// Pin the left page to the left side.
view.setTranslationX(getWidth() * -position);
view.setAlpha(Math.max(1.0f + position, 0.0f));
} else {
// Use the default slide transition when moving to the next page.
view.setTranslationX(0.0f);
view.setAlpha(1.0f);
}
}
};
public CalculatorPadViewPager(Context context) {
this(context, null);
}
public CalculatorPadViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
setAdapter(mStaticPagerAdapter);
setBackgroundColor(getResources().getColor(android.R.color.black));
setOnPageChangeListener(mOnPageChangeListener);
setPageMargin(getResources().getDimensionPixelSize(R.dimen.pad_page_margin));
setPageTransformer(false, mPageTransformer);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// Invalidate the adapter's data set since children may have been added during inflation.
if (getAdapter() == mStaticPagerAdapter) {
mStaticPagerAdapter.notifyDataSetChanged();
}
}
}

关于第一个疑问,答案在第78行。setPageMargin(-24);由于是负数,这样每个page之间就会有重叠,就出现了明明显示的第一个view,结果在右侧显示了一部分第二个view的layout。看到这里突然想起以前写图片浏览器的时候,PM让两个图片之间不要紧挨着,留有一定的间隙,用的就是这个方法实现的。

第二个疑问涉及到ViewPager的切换动画。看第79行,setPageTransformer(false, mPageTransformer); 这里mPageTransformer的定义在53~66行。在transformPage方法里根据position设置当前view的x轴偏移量。

1
2
3
4
5
6
7
8
9
/**
* Apply a property transformation to the given page.
*
* @param page Apply the transformation to this page
* @param position Position of page relative to the current front-and-center
* position of the pager. 0 is front and center. 1 is one full
* page position to the right, and -1 is one page position to the left.
*/
public void transformPage(View page, float position);

看一下源码里的方法说明,这里的position其实是相对的,并不是常规理解的位置下标,它是以当前屏幕的正中心为坐标原点,当page的中心和屏幕的原点重合(即page正在完全显示时)position=0;
当page向左侧滑动,慢慢淡出屏幕时,该过程中page的中心相对屏幕的中心原点在沿着X轴向左移动,此时positon < 0,当page完全滑出屏幕时,positon = -1;
同理,当page向右滑动,慢慢淡出屏幕时,该过程中page的中心相对屏幕的中心原点在沿着X轴向右移动,此时positon > 0,当page完全滑出屏幕时,positon = 1;

有了上面的分析,其实可以把positon看成正在滑动的page滑出屏幕的比例,正负代表往哪个方向滑动。所以,要想在滑出数学计算公式的page过程中保持当前输入的page位置不动,根据滑出屏幕的比例设置该page在X轴的偏移量就可以了,view.setTranslationX(getWidth() * -position); 同理,把数学计算公式的page滑出时取消这个偏移量,view.setTranslationX(0); 到这里疑问2也解决了。
更多的ViewPager切换动画请参考开源项目JazzyViewPager

最后看下疑问3,为什么page2滑入屏幕后并没有完全占满屏幕的宽度而右侧有一部分的间隙呢?继续看源码,在第26行发现了端倪。原来是复写了PagerAdapter的getPageWidth方法,看一下该方法的说明:

1
2
3
4
5
6
7
8
9
10
/**
* Returns the proportional width of a given page as a percentage of the
* ViewPager's measured width from (0.f-1.f]
*
* @param position The position of the page requested
* @return Proportional width for the given page position
*/
public float getPageWidth(int position) {
return 1.f;
}

说的很清晰了,根据position设置当前页面宽度的百分比。所以疑问3也解决啦。

到这里代码的分析已经结束了,给我最大的感触就是:多读源码!多读源码!多读源码!如果把原生计算器的需求给你,你能第一时间想到会用ViewPager去实现底部的交互吗?只有在对ViewPager源码很了解、对setPageTransformer、getPageWidth()、setPageMargin()这些方法的原理和使用很清晰的时候,你才会想到原来用ViewPager用不了多少行代码就能很优雅的实现啦。