最近做的项目与安卓照相机有关,所以在网上下了安卓照相机的源码,个人对安卓开发也只是个初学者,照相机源码对本人而言还是很复杂(大概有70-80个类)。计划以后每天研究几个类,主要学习里面编程的思想与经验。今天首先对3个与界面有关的view类进行学习分析。
主要的xml文件:res/layout/camera_control.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/control_bar"
android:orientation="vertical"
android:layout_height="match_parent"
android:layout_width="76dp"
android:layout_marginTop="13dp"
android:layout_marginBottom="10dp"
android:layout_alignParentRight="true">
<com.android.camera.RotateImageView
android:id="@+id/review_thumbnail"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_height="52dp"
android:layout_width="52dp"
android:clickable="true"
android:focusable="false"
android:background="@drawable/border_last_picture"/>
<LinearLayout android:id="@+id/camera_switch_set"
android:orientation="vertical"
android:gravity="center"
android:layout_centerInParent="true"
android:layout_height="wrap_content"
android:layout_width="wrap_content">
<com.android.camera.RotateImageView android:id="@+id/video_switch_icon"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/btn_ic_mode_switch_video"/>
<com.android.camera.Switcher android:id="@+id/camera_switch"
android:layout_width="wrap_content"
android:layout_height="70dp"
android:src="@drawable/btn_mode_switch_knob"
android:background="@drawable/btn_mode_switch_bg" />
<com.android.camera.RotateImageView
android:id="@+id/camera_switch_icon"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginBottom="3dp"
android:src="@drawable/btn_ic_mode_switch_camera"/>
</LinearLayout>
<com.android.camera.ShutterButton android:id="@+id/shutter_button"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:scaleType="center"
android:clickable="true"
android:focusable="true"
android:src="@drawable/btn_ic_camera_shutter"
android:background="@drawable/btn_shutter"/>
</RelativeLayout>
布局效果如上图所示 ,依次向下分别为:RotateImageView类,RotateImageView类,Switcher类,RotateImageView类,ShutterButton类
一,先说Switcher类,这个是照相机里切换Camera与Video的按钮。
这个类继承于ImageView类,为了达到切换的效果实现了View.OnTouchListener,在代码里并定义了一个接口:
public interface OnSwitchListener {
// Returns true if the listener agrees that the switch can be changed.
public boolean onSwitchChanged(Switcher source, boolean onOff);
}
在代码中定义了这个接口的变量mListener,并定义了公有办法来监听这个接口:
public void setOnSwitchListener(OnSwitchListener listener) {
mListener = listener;
}
1,手势位置的确定:
获得src图片的宽度和高度:
Drawable drawable = getDrawable();
int drawableHeight = drawable.getIntrinsicHeight();
int drawableWidth = drawable.getIntrinsicWidth();
考虑到图片背景的宽度以及高度,可以确定按钮的有效位置为:
final int available = getHeight() - getPaddingTop()
- getPaddingBottom() - drawableHeight;
其中getHeight()是获得该Switcher控件的高度,减去上下的padding值,再减去src图片的高度,就得到上图中Switcher中的圆框移动的范围。
2,onTouchEvent事件:
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) return false; //若Switcher设置无效,则不响应触摸事件
final int available = getHeight() - getPaddingTop() - getPaddingBottom()
- getDrawable().getIntrinsicHeight();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mAnimationStartTime = NO_ANIMATION; //由于是手指直接控制Switcher切换,不需要动画
setPressed(true); //状态变为按下
trackTouchEvent(event); //响应事件
break;
case MotionEvent.ACTION_MOVE:
trackTouchEvent(event);
break;
case MotionEvent.ACTION_UP:
trackTouchEvent(event);
tryToSetSwitch(mPosition >= available / 2); //根据按钮是否大于有效值一半决定是否切换,并产生动画
setPressed(false);
break;
case MotionEvent.ACTION_CANCEL:
tryToSetSwitch(mSwitch);
setPressed(false);
break;
}
return true;
}
其中比较重要的是trackTouchEvent方法:主要是计算mPosition的位置,并时刻刷新Switcher中圆框的位置
private void trackTouchEvent(MotionEvent event) {
Drawable drawable = getDrawable();
int drawableHeight = drawable.getIntrinsicHeight();
final int height = getHeight();
final int available = height - getPaddingTop() - getPaddingBottom()
- drawableHeight;
int x = (int) event.getY();
mPosition = x - getPaddingTop() - drawableHeight / 2;
if (mPosition < 0) mPosition = 0;
if (mPosition > available) mPosition = available;
invalidate();
}
这里面有个问题就是还没有与mListener联系起来,所以还不能响应OnSwitchListener事件,但已经可以响应OnTouch事件了。而这就与tryToSetSwitch方法有关了。
private void tryToSetSwitch(boolean onOff) {
try {
if (mSwitch == onOff) return;
if (mListener != null) {
if (!mListener.onSwitchChanged(this, onOff)) { //1
return;
}
}
mSwitch = onOff;
} finally {
startParkingAnimation();
}
}
从代码1处中可知,若设了mListener的值,则会调用onSwitchChanged方法,并且会根据这个方法的返回值决定是否使Switcher的切换有效,可以使用一种更直接的方式setSwitch来切换。
public void setSwitch(boolean onOff) {
if (mSwitch == onOff) return;
mSwitch = onOff;
invalidate(); //刷新mSwitch的状态
}
3,接下来就是最重要的方法了onDraw()
protected void onDraw(Canvas canvas) {
Drawable drawable = getDrawable();
int drawableHeight = drawable.getIntrinsicHeight();
int drawableWidth = drawable.getIntrinsicWidth();
if (drawableWidth == 0 || drawableHeight == 0) {
return; // nothing to draw (empty bounds)
}
final int available = getHeight() - getPaddingTop()
- getPaddingBottom() - drawableHeight;
if (mAnimationStartTime != NO_ANIMATION) {
long time = AnimationUtils.currentAnimationTimeMillis();
int deltaTime = (int) (time - mAnimationStartTime);
mPosition = mAnimationStartPosition +
ANIMATION_SPEED * (mSwitch ? deltaTime : -deltaTime) / 1000;
if (mPosition < 0) mPosition = 0;
if (mPosition > available) mPosition = available;
boolean done = (mPosition == (mSwitch ? available : 0));
if (!done) {
invalidate();
} else {
mAnimationStartTime = NO_ANIMATION;
}
} else if (!isPressed()){
mPosition = mSwitch ? available : 0;
}
int offsetTop = getPaddingTop() + mPosition;
int offsetLeft = (getWidth()
- drawableWidth - getPaddingLeft() - getPaddingRight()) / 2;
int saveCount = canvas.getSaveCount();
canvas.save();
canvas.translate(offsetLeft, offsetTop);
drawable.draw(canvas);
canvas.restoreToCount(saveCount);
}
其中mAnimationStartTime主要是由于手指放开后,Switcher不处于两状态之一,所以需要利用动画来实现Switcher最终到两状态之一。
其中的canvas.save()与canvas.translate(offsetLeft,offsetTop),canvas.restoreToCount(saveCount)方法可以参考相关资料,这个主要是画Switcher中圆框的位置。
private void startParkingAnimation() {
mAnimationStartTime = AnimationUtils.currentAnimationTimeMillis();
mAnimationStartPosition = mPosition;
}
这个方法确定动画开始时间与动画开始位置。
4,最后就是两个公有方法,主要提供给其他类使用
// Consume the touch events for the specified view.
public void addTouchView(View v) {
v.setOnTouchListener(this);
}
// This implements View.OnTouchListener so we intercept the touch events
// and pass them to ourselves.
public boolean onTouch(View v, MotionEvent event) {
onTouchEvent(event);
return true;
}
其中addTouchView可以通过使用其他View来实现这个Switcher的切换,onTouch方法可以响应其他view的MotionEvent事件
到此Switcher类分析完。
二,ShutterButton类
ShutterButton类代码比较短,同样继承于ImageView,定义了OnShutterButtonListener监听器
public interface OnShutterButtonListener {
/**
* Called when a ShutterButton has been pressed.
*
* @param b The ShutterButton that was pressed.
*/
void onShutterButtonFocus(ShutterButton b, boolean pressed);
void onShutterButtonClick(ShutterButton b);
}
其中两个方法分别在如下两个方法中调用。
private void callShutterButtonFocus(boolean pressed) {
if (mListener != null) {
mListener.onShutterButtonFocus(this, pressed);
}
}
@Override
public boolean performClick() {
boolean result = super.performClick();
if (mListener != null) {
mListener.onShutterButtonClick(this);
}
return result;
}
其中callShutterButtonFocus在drawableStateChanged()中调用,而performClick()则在代码中模拟按键事件时调用。
drawableStateChanged()方法;
protected void drawableStateChanged() {
super.drawableStateChanged();
final boolean pressed = isPressed();
if (pressed != mOldPressed) {
if (!pressed) {
post(new Runnable() {
public void run() {
callShutterButtonFocus(pressed);
}
});
} else {
callShutterButtonFocus(pressed);
}
mOldPressed = pressed;
}
}
这段代码很难理解,根据代码的注释,自己理解大致是这样:
这里是通过pressed的状态改变来确定是否调用callShutterButtonFocus方法
当使用物理按键时,事件流程:focus pressed, optional camera pressed, focus released
当使用ShutterButton时,事件流程:pressed(true), optional click, pressed(false)
当直接触摸屏幕时,事件流程: pressed(true), pressed(false), optional click
为了使三者保持一致,使用物理按键的标准,也就是optional click在press(false)之前响应,也就是pressed(true), optional click, pressed(false)的流程。
所以当pressed为true时,则直接执行callShutterButtonFocus,而当pressed为false时,则在UI线程中执行callShutterButtonFocus
个人还不是很理解。
三,RotateImageView类
这个类主要是实现了一个ImageView旋转的效果,主要方法是setDegree与onDraw两个方法:
setDegree方法:
public void setDegree(int degree) {
// make sure in the range of [0, 359]
degree = degree >= 0 ? degree % 360 : degree % 360 + 360;
if (degree == mTargetDegree) return;
mTargetDegree = degree;
mStartDegree = mCurrentDegree;
mAnimationStartTime = AnimationUtils.currentAnimationTimeMillis();
int diff = mTargetDegree - mCurrentDegree;
diff = diff >= 0 ? diff : 360 + diff; // make it in range [0, 359]
// Make it in range [-179, 180]. That's the shorted distance between the
// two angles
diff = diff > 180 ? diff - 360 : diff;
mClockwise = diff >= 0;
mAnimationEndTime = mAnimationStartTime
+ Math.abs(diff) * 1000 / ANIMATION_SPEED;
invalidate();
}
这里主要是角度的计算问题,主要涉及mCurrentDegree存储此刻的旋转值,mTargetDegree存储目标值,mStartDegree存储旋转开始值,diff存储要旋转的角度(-180度到180度)mClockwise为旋转方向(顺,逆)。
protected void onDraw(Canvas canvas) {
Drawable drawable = getDrawable();
if (drawable == null) return;
Rect bounds = drawable.getBounds();
int w = bounds.right - bounds.left;
int h = bounds.bottom - bounds.top;
if (w == 0 || h == 0) return; // nothing to draw
if (mCurrentDegree != mTargetDegree) {
long time = AnimationUtils.currentAnimationTimeMillis();
if (time < mAnimationEndTime) {
int deltaTime = (int)(time - mAnimationStartTime);
int degree = mStartDegree + ANIMATION_SPEED
* (mClockwise ? deltaTime : -deltaTime) / 1000;
degree = degree >= 0 ? degree % 360 : degree % 360 + 360;
mCurrentDegree = degree;
invalidate();
} else {
mCurrentDegree = mTargetDegree;
}
}
int left = getPaddingLeft();
int top = getPaddingTop();
int right = getPaddingRight();
int bottom = getPaddingBottom();
int width = getWidth() - left - right;
int height = getHeight() - top - bottom;
int saveCount = canvas.getSaveCount();
canvas.translate(left + width / 2, top + height / 2);
canvas.rotate(-mCurrentDegree);
canvas.translate(-w / 2, -h / 2);
drawable.draw(canvas);
canvas.restoreToCount(saveCount);
}
如果mTargetDegree与mCurrentDegree不相等,则进行旋转,原理和Switcher相同,通过canvas的translate和rotate方法实现旋转。从代码中可以看出invalidate方法当执行完一个完整的onDraw()后再执行下一个onDraw();