(一)UI部分
UI部分主要包含如下几个方面
1.横竖屏布局
2.线性布局实现Table效果
3.自定义shape绘制效果和style风格
4.扩展FrameLayout实现翻页
5.使用自定义Button扩展Button效果
最上方的带光标的是个EditText, 它被放在一个ViewSwitcher中。通过对ViewSwitcher进行扩展可以对里边的子控件,比如多个EditText进行切换。显示框下方的一片都是按钮。这些按钮与系统默认的按钮外观上相差很大,其实它们还是Button,只不过用了与系统不同的样式。这么Button都被放在了一个FrameLayout中,通过扩展这个FrameLayout可以实现通过手势切换面板。
main.xml的实现:
在Android的UI应用中,一般都在layout中写一个main.xml,这样的结果是在实际环境中,手机横竖屏切换的时候都会采用这个main进行布局。如果需要横竖屏不同的布局,那么需要自己新建2个文件夹。 layout-land ----横屏布局 layout-port -----竖屏
横屏 竖屏
整体layout层次:
一个LinearLayout做整体竖行排列的布局。在它下面有一个view switcher做第一层结构,第二层结构是一个FrameLayout,就是上图的panelswitch,在这个FrameLayout中分为两个面板:简单面板(simplePad)和高级面板(advancedPad)。简单面板包含一些简单的操作运算符和数字,高级面板则包含一些较为复杂的操作符。
简单面板 高级面板
里边的ViewSwitcher、FrameLayout和Button都用了自己定制的控件名称,当使用自定义的控件时需要把包名+类名称做一个XML元素使用,必须是完整的名称。很多控件都使用了style属性。利用style可以定制控件的分格。用法:在res/values中新建style.xml, 然后在resources元素下建立style元素。在style下新建多个需要的item,每个item就是一个布局属性。
<resources>
<style
name=
"button_style">
<item
name=
"android:textSize">40dp</item>
<item
name=
"android:layout_width">match_parent</item>
<item
name=
"android:layout_height">match_parent</item>
<item
name=
"android:layout_weight">1</item>
<item
name=
"android:focusable">true</item>
<item
name=
"android:background">
@drawable/button</item>
<item
name=
"android:layout_marginLeft">1dp</item>
</style>
<style
name=
"digit_button_style"
parent=
"button_style">
<item
name=
"android:background">
@drawable/blue_button</item>
</style>
<style
name=
"button_small_style"
parent=
"button_style">
<item
name=
"android:textSize">30dp</item>
</style>
<style
name=
"display_style">
<item
name=
"android:textSize">40dp</item>
<item
name=
"android:gravity">right|center_vertical</item>
<item
name=
"android:textColor">#
ffffff</item>
<item
name=
"android:maxLines">1</item>
<item
name=
"android:scrollbars">none</item>
<item
name=
"android:scrollHorizontally">true</item>
<item
name=
"android:layout_width">match_parent</item>
<item
name=
"android:layout_height">match_parent</item>
<item
name=
"android:padding">8dp</item>
</style>
</resources>
button_style用到了一个@drawable/button属性,这个属性是需要自己设置的。一般要使用drawable对象时,都是把需要的图片放入这3个文件夹中的某个。如果要使用自己定义的drawable,可以新建一个drawable文件夹,然后新建自己需要的xml。比如新建一个button.xml
<shape
xmlns:android=
"http://schemas.android.com/apk/res/android">
<gradient
android:startColor=
"#000000"
android:endColor=
"#333333"
android:angle=
"90"/>
<corners
android:radius=
"0dp"
/>
</shape>
这里用到了drawable的shape属性,用shape可以很容易地实现需要的drawable的形状。gradient是一个线性渐变属性,使用它可以实现一个渐变颜色的效果,angle为90表示从上往下渐变。Conrners定义边角形状,radius为0表示边角角度为0,即折线型。
(二)表示层部分
表示层部分主要实现两个功能:Button的定制和FramLayout的面板的切换。
首先是Button的定制:现在需要的功能是
- Button上的文本显示的位置需要调整在合适的位置
- Button需要接受触摸点击和长按操作
- Button需要点击后有特殊的动画效果
第一个功能的实现比较简单,就是重写onSizeChanged和onTextChanged方法,然后采用measureText进行调整位置。
private void measureText() {
Paint paint = getPaint();
mTextX = (getWidth() - paint.measureText(getText().toString())) / 2;
mTextY = (getHeight() - paint.ascent() - paint.descent()) / 2;
}
第二个功能的实现:重写onTouchEvent,通过event.getAction判断是哪种触摸操作,然后执行相应的操作。这里有一个点击后调用动画效果的功能。而Button的点击事件则是通过实现OnClick方法,这里有个mListener,就是逻辑部分提到的EventListener。
第三个功能是采用重写onDraw
来实现的。onDraw
是用来在控件上绘制东西的一个方法,首先初始一个画笔,然后定义一些绘制效果,如这里的drawRect
画一个边框。这里的动画效果是采用一个计时器实现的,点击Button
后会在Button
上画一个蓝色边框,然后逐渐消失。显示和消失采用的是改变alpha
值实现。这里涉及到Android
如何进行界面刷新的问题。
Android中在绘图中的多线程中,invalidate和postInvalidate这两个方法是用来刷新界面的,调用这两个方法后,会调用onDraw方法,让界面重绘。一个Android 程序默认情况下也只有一个进程,但一个进程下却可以有许多个线程。在这么多线程当中,把主要是负责控制UI界面的显示、更新和控件交互的线程称为UI线程,由于onCreate()方法是由UI线程执行的,所以也可以把UI线程理解为主线程。其余的线程可以理解为工作者线程。invalidate()得在UI线程中被调动,在工作者线程中可以通过Handler来通知UI线程进行界面更新。而postInvalidate()在工作者线程中被调用。
/**
* 调用postInvalidateDelayed()或者invalidate()进行刷新时调用
*/
@Override
public void onDraw(Canvas canvas) {
if (mAnimStart != -1) {
int animDuration = (
int) (System.
currentTimeMillis() - mAnimStart);
if (animDuration >=
CLICK_FEEDBACK_DURATION) {
mAnimStart = -1;
}
else {
drawMagicFlame(animDuration, canvas);
postInvalidateDelayed(
CLICK_FEEDBACK_INTERVAL);
}
}
else if (isPressed()) {
drawMagicFlame(0, canvas);
}
CharSequence text = getText();
canvas.drawText(text, 0, text.length(), mTextX, mTextY, getPaint());
}
public void animateClickFeedback() {//点击按键后开始计时和动画
mAnimStart = System.
currentTimeMillis();
invalidate();
}
FrameLayout的面板切换:
关键部分是一个手势检测:private GestureDetector mGestureDetector;
和一系列动画效果的实现:private TranslateAnimation inLeft; 。。。通过重写onFling检测向右或者向左滑动屏幕的操作,然后调用相应的moveRight、moveLeft。moveRight、moveLeft操作的实现基本上就是采用一个动画效果,把需要的面板“引进”,把不需要的面板“赶走”,就像四季交替一般。显示面板通过调用setVisibility实现。View.Gone就是把面板放到屏幕外并且隐藏。
mGestureDetector =
new GestureDetector(context,
new GestureDetector.SimpleOnGestureListener() {
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX,
float velocityY) {
int dx = (
int) (e2.getX() - e1.getX());
// don't accept the fling if it's too short
// as it may conflict with a button push
if (Math.
abs(dx) >
MAJOR_MOVE && Math.
abs(velocityX) > Math.
abs(velocityY)) {//达到最小移动距离并且X方向的速率大于Y方向的速率时移动面板
if (velocityX > 0) {
moveRight();
}
else {
moveLeft();
}
return true;
}
else {
return false;
}
}
});
void moveLeft() {// 往左移动面板。条件:还没有到最后一个面板
// <--
if (mCurrentView < mChildren.length - 1 && mPreviousMove !=
LEFT) {
mChildren[mCurrentView+1].setVisibility(View.
VISIBLE);
mChildren[mCurrentView+1].startAnimation(inLeft);
mChildren[mCurrentView].startAnimation(outLeft);
mChildren[mCurrentView].setVisibility(View.
GONE);
mCurrentView++;
mPreviousMove =
LEFT;
}
View在初始化完成之后会调用 protected onFinishInflate()函数,如果要在view初始化完成之后和开始使用这个View对象之间做些事情,可以写个继承此view的 子类,覆盖onFinishInflate方法。
protected void onFinishInflate() {
int count = getChildCount();
mChildren =
new View[count];
for (
int i = 0; i < count; ++i) {
mChildren
= getChildAt(i);
}
updateCurrentView();
}
onInterceptTouchEvent 是一个触摸中断事件,这里不能返回true,否则Button无法得到触摸操作,因为都被中断了,并且返回了CANCEL操作。下面的代码表示如果这个mGestureDetector能够处理这个event事件并且返回false就不应该对这个event进行拦截和中断。
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
(三)历史记录存储和读取部分
历史记录的查看通过键盘上的上、下键操作实现,同时可以通过菜单上的清除历史记录选项将记录清空。涉及的类有Persist、History、HistroyEntry、HistroyAdapter。
1. Persist类的主要作用是从程序私有文件夹/data/data/calculator下读取记录或存储记录,分别通过load()和save()两个方法实现。Load()方法只在程序开始运行时创建Persist时调用,save()在计算器Activity onPasue()方法中调用。
2. HistroyEntry是记录内容,它包含两个重要元素:原始记录(mBase)和编辑过的记录(mEdited)。
3. Histroy类中包含了存有HistroyEntry记录的容器,通过这个类的一些方法可以对记录进行添加,修改,存储等操作。
4. HistroyAdapter定义记录如何显示
(四)逻辑实现部分
逻辑实现部分逻辑部分的功能主要如下:
- 根据数字键和符号键的操作得到运算式,通过等于键得到结果
- 响应短按事件和长按事件,长按事件主要用来清空显示框中的内容
- 过滤掉不合法的运算式,如果是非法的则替换成合法的
首先需要一个按键监听事件,主要是写一个EventListenr,实现View.OnKeyListener,
View.OnLongClickListener, View.OnClickListener接口。这里包括相应硬键盘操作(主要是方向导航键)的onKey()方法,还有两个监听:Click(短按)和LongClik(长按)
主要的逻辑实现是在Logic()这个类里面实现,里面包括了一些运算结果字符的替换,计算器编辑显示框内容的增删,以及对运算表达式做处理后进行运算操作。Android计算器的运算用到了第三方的运算解析工具,直接调用即可。
计算器显示框有一个特别的要求,就是要对运算式进行过滤。比如+-2+是不合法的运算式。这里主要通过扩展android.text的一个SpannableStringBuilder类来定制需要的过滤和替换功能。基本用法是:重写public SpannableStringBuilder replace(int start, int end,CharSequence tb,int tbstart, int tbend) 替换方法来对文本进行过滤和替换。然后定义一个工厂类来实现可编辑字符对象的实例调用。
@Override
public SpannableStringBuilder //按键一次调用两次
replace(int start, int end, CharSequence tb, int tbstart, int tbend) {
//Log.d("spannableStringBuilder", isInsideReplace + "start" + start + "end" + end + tb.toString() + "tbstart" +tbstart + "tbend" + tbend);
if (isInsideReplace) {
return super.replace(start, end, tb, tbstart, tbend);
} else {
isInsideReplace = true;
try {
String delta = tb.subSequence(tbstart, tbend).toString();
return internalReplace(start, end, delta);
} finally {
isInsideReplace = false;
}
}
}
private SpannableStringBuilder internalReplace(int start, int end, String delta) {
if (!mLogic.acceptInsert(delta)) {//不允许插入
mLogic.cleared();
start = 0;
end = length();
}
for (int i = ORIGINALS.length - 1; i >= 0; --i) {//替代 * / -
delta = delta.replace(ORIGINALS, REPLACEMENTS);
}
int length = delta.length();
if (length == 1) {
char text = delta.charAt(0);
//don't allow two dots in the same number
if (text == '.') {
int p = start - 1;
while (p >= 0 && Character.isDigit(charAt(p))) {
--p;
}
if (p >= 0 && charAt(p) == '.') {
return super.replace(start, end, "");
}
}
char prevChar = start > 0 ? charAt(start-1) : '\0';
//don't allow 2 successive minuses
if (text == Logic.MINUS && prevChar == Logic.MINUS) {
return super.replace(start, end, "");
}
//don't allow multiple successive operators
if (Logic.isOperator(text)) {
while (Logic.isOperator(prevChar) &&
(text != Logic.MINUS || prevChar == '+')) {
--start;
prevChar = start > 0 ? charAt(start-1) : '\0';
}
}
//don't allow leading operator + * /
if (start == 0 && Logic.isOperator(text) && text != Logic.MINUS) {
return super.replace(start, end, "");
}
}
return super.replace(start, end, delta);
}
在显示框中需要屏蔽掉软键盘的编辑响应以及光标的一些位置控制,那么需要最后一个定制的控件类:CalculatorDisplay类。它是一个文本交替功能的,就是在这个ViewSwitcher中用2个EditText来交替显示运算式和结果。并且有滚动的动画效果。显示框内要屏蔽掉软键盘的输入而且只能接受特定的字符可以通过以下代码实现
NumberKeyListener calculatorKeyListener =
new NumberKeyListener() {
public int getInputType() {
// Don't display soft keyboard.//屏蔽软键盘输入
return InputType.TYPE_NULL;
}
protected char[] getAcceptedChars() {//只接收特定字符
return ACCEPTED_CHARS;
}
public CharSequence filter(CharSequence source, int start, int end,
Spanned dest, int dstart, int dend) {
// the EditText should still accept letters (eg. 'sin')为了能读取sin等操作符 不进行过滤操作
return null;
}
};
最后需要一个Activity来进行所有的显示层和逻辑层的结合。Calculator类来完成这个任务,在这个Activity中还创建了一些菜单和菜单的操作。创建菜单使用onCreateOptionsMenu重写, 菜单响应使用onMenuItemSelected或者onOptionItemSelected。其中重载了onSaveInstanceState(Bundle state)方法,Activity被终止之前,将状态保留,以便在下次启动后在onCreat()或onRestoreInstanceState(Bundle bundle)恢复先前状态
@Override
protected void onSaveInstanceState(Bundle state) {//Activity被终止前 //将状态保留 以便下次启动后在onCreat()或onRestoreInstanceState(Bundle)恢//复先前状态
super.onSaveInstanceState(state);
state.putInt(STATE_CURRENT_VIEW, mPanelSwitcher.getCurrentIndex());//改变Bundle键值对的值 key保持不变
}