安卓照相机源码分析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();




  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值