1. 程式人生 > >安卓照相機原始碼分析1——Switcher類,ShutterButton類,RotateImageView類

安卓照相機原始碼分析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,在程式碼裡並定義了一個介面:

 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();