安卓照相機原始碼分析1——Switcher類,ShutterButton類,RotateImageView類
最近做的專案與安卓照相機有關,所以在網上下了安卓照相機的原始碼,個人對安卓開發也只是個初學者,照相機原始碼對本人而言還是很複雜(大概有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,在程式碼裡並定義了一個介面:
在程式碼中定義了這個介面的變數mListener,並定義了公有辦法來監聽這個介面:public interface OnSwitchListener { // Returns true if the listener agrees that the switch can be changed. public boolean onSwitchChanged(Switcher source, boolean onOff); }
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();