Android单点触控实现图片平移、缩放、旋转功能

内容摘要
相信大家使用多点对图片进行缩放,平移的操作很熟悉了,大部分大图的浏览都具有此功能,有些app还可以对图片进行旋转操作,QQ的大图浏览就可以对图片进行旋转操作,大家都知道对图片
文章正文

相信大家使用多点对图片进行缩放,平移的操作很熟悉了,大部分大图的浏览都具有此功能,有些app还可以对图片进行旋转操作,QQ的大图浏览就可以对图片进行旋转操作,大家都知道对图片进行缩放,平移,旋转等操作可以使用Matrix来实现,Matrix就是一个3X3的矩阵,对图片的处理可分为四个基础变换操作,Translate(平移变换)、Rotate(旋转变换)、Scale (缩放变换)、Skew(错切变换),如果大家对Matrix不太了解的话可以看看这篇文章(点击查看),作者对每一种Matrix的变换写的很清楚,但是如果使用一个手指对图片进行缩放,平移,旋转等操作大家是否了解呢,其实单手指操作跟多手指操作差不多,当然也是使用Matrix来实现的,无非是在缩放比例和旋转角度的计算上面有些不一样,也许你会有疑问,多点操作图片缩放旋转是两个手指操作,平移的时候是一个手指操作,那么你单手在图片即平移,又缩放旋转难道不会有冲突吗?是的,这样子肯定是不行的,我们必须将平移和缩放旋转进行分开。如下图


图片外面的框是一个边框,如果我们手指触摸的是上面的蓝色小图标我们就对其进行缩放旋转操作,如果是触摸到其他的区域我们就对其进行平移操作,这样就避免了上面所说的冲突问题,这里对图片的平移操作并没有使用Matrix来实现,而是使用layout()方法来对其进行位置的变换。

计算缩放比例比较简单,使用手指移动的点到图片所在中心点的距离除以图片对角线的一半就是缩放比例了,接下来就计算旋转角度,如下图


preMove是手指移动前一个点,curMove就是当前手指所在的点,还有一个中心点center,知道三个点求旋转的夹角是不是很简单呢,就是线段a和线段c的一个夹角,假设夹角为o,  o的余弦值 cos o = (a * a + c * c - b * b) / (2 * a * c), 知道余弦值夹角就出来了,但是这里还有一个问题,我们在使用Matrix对图片进行旋转的时候需要区别顺时针旋转还是逆时针旋转,顺时针旋转角度为正,所以上面我们只求出了旋转的角度,并不知道是顺时针还是逆时针。

具体怎么求是顺时针角度还是逆时针角度呢?有些同学可能会根据curMove和ProMove的x ,y 的大小来判断,比如上面的图中,如果curMove.x > proMove.x则为顺时针,否则为逆时针,这当然是一种办法,可是你想过这种方法只适合在第二象限,在第一,第三,第四象限这样子判断就不行了,当然你可以判断当前的点在第几象限,然后在不同的象限采用不同的判断,这样子判断起来会很复杂。

有没有更加简单的方法来判断呢?答案是肯定的,我们可以使用数学中的向量叉乘来判断。假如向量A(x1, y1)和向量B(x2, y2),我们可以使用向量叉乘 |A X B| = x1*y2 - x2*y1 = |A|×|B|×sin(向量A到B的夹角), 所以这个值的正负也就是A到B旋转角sin值的正负, 顺时针旋转角度0~180,sin>0, 顺时针旋转角度180~360或者说逆时针旋转0~180,sin<0, 所以我们可以用个center到proMove的向量 叉乘 center到curMove的向量来判断是顺时针旋转还是逆时针旋转。

接下来我们就开始动手实现此功能,我们采用一个自定义的View来实现,这里就叫SingleTouchView,直接继承View, 从上面的图中我们可以定义出一些自定义的属性,比如用于缩放的图片,控制缩放旋转的小图标,图片边框的颜色等,我定义了如下的属性

<declare-styleable name="SingleTouchView"> 
  <attr name="src" format="reference" />      <!-- 用于缩放旋转的图标 --> 
  <attr name="editable" format="boolean"/>     <!-- 是否处于可编辑状态 --> 
  <attr name="frameColor" format="color" />     <!-- 边框颜色 --> 
  <attr name="frameWidth" format="dimension" />   <!-- 边框线宽度 --> 
  <attr name="framePadding" format="dimension" />  <!-- 边框与图片的间距 --> 
  <attr name="degree" format="float" />       <!-- 旋转角度 --> 
  <attr name="scale" format="float" />       <!-- 缩放比例 --> 
  <attr name="controlDrawable" format="reference"/> <!-- 控制图标 --> 
  <attr name="controlLocation">           <!-- 控制图标的位置 --> 
    <enum name="left_top" value="0" /> 
    <enum name="right_top" value="1" /> 
    <enum name="right_bottom" value="2" /> 
    <enum name="left_bottom" value="3" /> 
  </attr> 
</declare-styleable> 

接下来就是自定义SingleTouchView的代码,代码有点长,注释还是蛮详细的

package com.example.singletouchview; 
 
import java.util.Arrays; 
import java.util.Collections; 
import java.util.List; 
 
import android.content.Context; 
import android.content.res.TypedArray; 
import android.graphics.Bitmap; 
import android.graphics.Bitmap.Config; 
import android.graphics.Canvas; 
import android.graphics.Color; 
import android.graphics.Matrix; 
import android.graphics.Paint; 
import android.graphics.Paint.Style; 
import android.graphics.Path; 
import android.graphics.Point; 
import android.graphics.PointF; 
import android.graphics.drawable.BitmapDrawable; 
import android.graphics.drawable.Drawable; 
import android.util.AttributeSet; 
import android.util.DisplayMetrics; 
import android.util.FloatMath; 
import android.util.TypedValue; 
import android.view.MotionEvent; 
import android.view.View; 
import android.view.ViewGroup; 
 
/** 
 * 单手对图片进行缩放,旋转,平移操作,详情请查看 
 * 
 * @blog http://blog.csdn.net/xiaanming/article/details/42833893 
 * 
 * @author xiaanming 
 * 
 */ 
public class SingleTouchView extends View { 
  /** 
   * 图片的最大缩放比例 
   */ 
  public static final float MAX_SCALE = 4.0f; 
   
  /** 
   * 图片的最小缩放比例 
   */ 
  public static final float MIN_SCALE = 0.3f; 
   
  /** 
   * 控制缩放,旋转图标所在四个点得位置 
   */ 
  public static final int LEFT_TOP = 0; 
  public static final int RIGHT_TOP = 1; 
  public static final int RIGHT_BOTTOM = 2; 
  public static final int LEFT_BOTTOM = 3; 
   
  /** 
   * 一些默认的常量 
   */ 
  public static final int DEFAULT_FRAME_PADDING = 8; 
  public static final int DEFAULT_FRAME_WIDTH = 2; 
  public static final int DEFAULT_FRAME_COLOR = Color.WHITE; 
  public static final float DEFAULT_SCALE = 1.0f; 
  public static final float DEFAULT_DEGREE = 0; 
  public static final int DEFAULT_CONTROL_LOCATION = RIGHT_TOP; 
  public static final boolean DEFAULT_EDITABLE = true; 
  public static final int DEFAULT_OTHER_DRAWABLE_WIDTH = 50; 
  public static final int DEFAULT_OTHER_DRAWABLE_HEIGHT = 50; 
   
   
   
  /** 
   * 用于旋转缩放的Bitmap 
   */ 
  private Bitmap mBitmap; 
   
  /** 
   * SingleTouchView的中心点坐标,相对于其父类布局而言的 
   */ 
  private PointF mCenterPoint = new PointF(); 
   
  /** 
   * View的宽度和高度,随着图片的旋转而变化(不包括控制旋转,缩放图片的宽高) 
   */ 
  private int mViewWidth, mViewHeight; 
   
  /** 
   * 图片的旋转角度 
   */ 
  private float mDegree = DEFAULT_DEGREE; 
   
  /** 
   * 图片的缩放比例 
   */ 
  private float mScale = DEFAULT_SCALE; 
   
  /** 
   * 用于缩放,旋转,平移的矩阵 
   */ 
  private Matrix matrix = new Matrix(); 
   
  /** 
   * SingleTouchView距离父类布局的左间距 
   */ 
  private int mViewPaddingLeft; 
   
  /** 
   * SingleTouchView距离父类布局的上间距 
   */ 
  private int mViewPaddingTop; 
   
  /** 
   * 图片四个点坐标 
   */ 
  private Point mLTPoint; 
  private Point mRTPoint; 
  private Point mRBPoint; 
  private Point mLBPoint; 
   
  /** 
   * 用于缩放,旋转的控制点的坐标 
   */ 
  private Point mControlPoint = new Point(); 
   
  /** 
   * 用于缩放,旋转的图标 
   */ 
  private Drawable controlDrawable; 
   
  /** 
   * 缩放,旋转图标的宽和高 
   */ 
  private int mDrawableWidth, mDrawableHeight; 
   
  /** 
   * 画外围框的Path 
   */ 
  private Path mPath = new Path(); 
   
  /** 
   * 画外围框的画笔 
   */ 
  private Paint mPaint ; 
   
  /** 
   * 初始状态 
   */ 
  public static final int STATUS_INIT = 0; 
   
  /** 
   * 拖动状态 
   */ 
  public static final int STATUS_DRAG = 1; 
   
  /** 
   * 旋转或者放大状态 
   */ 
  public static final int STATUS_ROTATE_ZOOM = 2;  
   
  /** 
   * 当前所处的状态 
   */ 
  private int mStatus = STATUS_INIT; 
   
  /** 
   * 外边框与图片之间的间距, 单位是dip 
   */ 
  private int framePadding = DEFAULT_FRAME_PADDING; 
   
  /** 
   * 外边框颜色 
   */ 
  private int frameColor = DEFAULT_FRAME_COLOR; 
   
  /** 
   * 外边框线条粗细, 单位是 dip 
   */ 
  private int frameWidth = DEFAULT_FRAME_WIDTH; 
   
  /** 
   * 是否处于可以缩放,平移,旋转状态 
   */ 
  private boolean isEditable = DEFAULT_EDITABLE; 
   
  private DisplayMetrics metrics; 
   
   
  private PointF mPreMovePointF = new PointF(); 
  private PointF mCurMovePointF = new PointF(); 
   
  /** 
   * 图片在旋转时x方向的偏移量 
   */ 
  private int offsetX; 
  /** 
   * 图片在旋转时y方向的偏移量 
   */ 
  private int offsetY; 
   
  /** 
   * 控制图标所在的位置(比如左上,右上,左下,右下) 
   */ 
  private int controlLocation = DEFAULT_CONTROL_LOCATION; 
 
   
  public SingleTouchView(Context context, AttributeSet attrs) { 
    this(context, attrs, 0); 
  } 
 
  public SingleTouchView(Context context) { 
    this(context, null); 
  } 
 
  public SingleTouchView(Context context, AttributeSet attrs, int defStyle) { 
    super(context, attrs, defStyle); 
    obtainStyledAttributes(attrs); 
    init(); 
  } 
   
  /** 
   * 获取自定义属性 
   * @param attrs 
   */ 
  private void obtainStyledAttributes(AttributeSet attrs){ 
    metrics = getContext().getResources().getDisplayMetrics(); 
    framePadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_FRAME_PADDING, metrics); 
    frameWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_FRAME_WIDTH, metrics); 
     
    TypedArray mTypedArray = getContext().obtainStyledAttributes(attrs, 
        R.styleable.SingleTouchView); 
     
    Drawable srcDrawble = mTypedArray.getDrawable(R.styleable.SingleTouchView_src); 
    mBitmap = drawable2Bitmap(srcDrawble); 
     
    framePadding = mTypedArray.getDimensionPixelSize(R.styleable.SingleTouchView_framePadding, framePadding); 
    frameWidth = mTypedArray.getDimensionPixelSize(R.styleable.SingleTouchView_frameWidth, frameWidth); 
    frameColor = mTypedArray.getColor(R.styleable.SingleTouchView_frameColor, DEFAULT_FRAME_COLOR); 
    mScale = mTypedArray.getFloat(R.styleable.SingleTouchView_scale, DEFAULT_SCALE); 
    mDegree = mTypedArray.getFloat(R.styleable.SingleTouchView_degree, DEFAULT_DEGREE); 
    controlDrawable = mTypedArray.getDrawable(R.styleable.SingleTouchView_controlDrawable); 
    controlLocation = mTypedArray.getInt(R.styleable.SingleTouchView_controlLocation, DEFAULT_CONTROL_LOCATION); 
    isEditable = mTypedArray.getBoolean(R.styleable.SingleTouchView_editable, DEFAULT_EDITABLE); 
     
    mTypedArray.recycle(); 
     
  } 
   
   
  private void init(){ 
    mPaint = new Paint(); 
    mPaint.setAntiAlias(true); 
    mPaint.setColor(frameColor); 
    mPaint.setStrokeWidth(frameWidth); 
    mPaint.setStyle(Style.STROKE); 
     
    if(controlDrawable == null){ 
      controlDrawable = getContext().getResources().getDrawable(R.drawable.st_rotate_icon); 
    } 
     
    mDrawableWidth = controlDrawable.getIntrinsicWidth(); 
    mDrawableHeight = controlDrawable.getIntrinsicHeight(); 
     
    transformDraw();  
  } 
   
   
  @Override 
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
    super.onMeasure(widthMeasureSpec, heightMeasureSpec); 
     
    //获取SingleTouchView所在父布局的中心点 
    ViewGroup mViewGroup = (ViewGroup) getParent(); 
    if(null != mViewGroup){ 
      int parentWidth = mViewGroup.getWidth(); 
      int parentHeight = mViewGroup.getHeight(); 
      mCenterPoint.set(parentWidth/2, parentHeight/2); 
    } 
  } 
   
   
  /** 
   * 调整View的大小,位置 
   */ 
  private void adjustLayout(){ 
    int actualWidth = mViewWidth + mDrawableWidth; 
    int actualHeight = mViewHeight + mDrawableHeight; 
     
    int newPaddingLeft = (int) (mCenterPoint.x - actualWidth /2); 
    int newPaddingTop = (int) (mCenterPoint.y - actualHeight/2); 
     
    if(mViewPaddingLeft != newPaddingLeft || mViewPaddingTop != newPaddingTop){ 
      mViewPaddingLeft = newPaddingLeft; 
      mViewPaddingTop = newPaddingTop; 
       
//     layout(newPaddingLeft, newPaddingTop, newPaddingLeft + actualWidth, newPaddingTop + actualHeight); 
    } 
     
    layout(newPaddingLeft, newPaddingTop, newPaddingLeft + actualWidth, newPaddingTop + actualHeight); 
  } 
   
   
  /** 
   * 设置旋转图 
   * @param bitmap 
   */ 
  public void setImageBitamp(Bitmap bitmap){ 
    this.mBitmap = bitmap; 
    transformDraw(); 
  } 
   
   
  /** 
   * 设置旋转图 
   * @param drawable 
   */ 
  public void setImageDrawable(Drawable drawable){ 
    this.mBitmap = drawable2Bitmap(drawable); 
    transformDraw(); 
  } 
   
  /** 
   * 从Drawable中获取Bitmap对象 
   * @param drawable 
   * @return 
   */ 
  private Bitmap drawable2Bitmap(Drawable drawable) { 
    try { 
      if (drawable == null) { 
        return null; 
      } 
 
      if (drawable instanceof BitmapDrawable) { 
        return ((BitmapDrawable) drawable).getBitmap(); 
      } 
 
      int intrinsicWidth = drawable.getIntrinsicWidth(); 
      int intrinsicHeight = drawable.getIntrinsicHeight(); 
      Bitmap bitmap = Bitmap.createBitmap( 
          intrinsicWidth <= 0 ? DEFAULT_OTHER_DRAWABLE_WIDTH 
              : intrinsicWidth, 
          intrinsicHeight <= 0 ? DEFAULT_OTHER_DRAWABLE_HEIGHT 
              : intrinsicHeight, Config.ARGB_8888); 
 
      Canvas canvas = new Canvas(bitmap); 
      drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 
      drawable.draw(canvas); 
      return bitmap; 
    } catch (OutOfMemoryError e) { 
      return null; 
    } 
 
  } 
   
  /** 
   * 根据id设置旋转图 
   * @param resId 
   */ 
  public void setImageResource(int resId){ 
    Drawable drawable = getContext().getResources().getDrawable(resId); 
    setImageDrawable(drawable); 
  } 
   
  @Override 
  protected void onDraw(Canvas canvas) { 
    //每次draw之前调整View的位置和大小 
    super.onDraw(canvas); 
     
    if(mBitmap == null) return; 
    canvas.drawBitmap(mBitmap, matrix, mPaint); 
     
     
    //处于可编辑状态才画边框和控制图标 
    if(isEditable){ 
      mPath.reset(); 
      mPath.moveTo(mLTPoint.x, mLTPoint.y); 
      mPath.lineTo(mRTPoint.x, mRTPoint.y); 
      mPath.lineTo(mRBPoint.x, mRBPoint.y); 
      mPath.lineTo(mLBPoint.x, mLBPoint.y); 
      mPath.lineTo(mLTPoint.x, mLTPoint.y); 
      mPath.lineTo(mRTPoint.x, mRTPoint.y); 
      canvas.drawPath(mPath, mPaint); 
      //画旋转, 缩放图标 
       
      controlDrawable.setBounds(mControlPoint.x - mDrawableWidth / 2, 
          mControlPoint.y - mDrawableHeight / 2, mControlPoint.x + mDrawableWidth 
              / 2, mControlPoint.y + mDrawableHeight / 2); 
      controlDrawable.draw(canvas); 
    } 
     
    adjustLayout(); 
     
     
  } 
   
   
   
  /** 
   * 设置Matrix, 强制刷新 
   */ 
  private void transformDraw(){ 
    if(mBitmap == null) return; 
    int bitmapWidth = (int)(mBitmap.getWidth() * mScale); 
    int bitmapHeight = (int)(mBitmap.getHeight()* mScale); 
    computeRect(-framePadding, -framePadding, bitmapWidth + framePadding, bitmapHeight + framePadding, mDegree); 
     
    //设置缩放比例 
    matrix.setScale(mScale, mScale); 
    //绕着图片中心进行旋转 
    matrix.postRotate(mDegree % 360, bitmapWidth/2, bitmapHeight/2); 
    //设置画该图片的起始点 
    matrix.postTranslate(offsetX + mDrawableWidth/2, offsetY + mDrawableHeight/2); 
     
    adjustLayout(); 
  } 
   
   
  public boolean onTouchEvent(MotionEvent event) { 
    if(!isEditable){ 
      return super.onTouchEvent(event); 
    } 
    switch (event.getAction() ) { 
    case MotionEvent.ACTION_DOWN: 
      mPreMovePointF.set(event.getX() + mViewPaddingLeft, event.getY() + mViewPaddingTop); 
       
      mStatus = JudgeStatus(event.getX(), event.getY()); 
 
      break; 
    case MotionEvent.ACTION_UP: 
      mStatus = STATUS_INIT; 
      break; 
    case MotionEvent.ACTION_MOVE: 
      mCurMovePointF.set(event.getX() + mViewPaddingLeft, event.getY() + mViewPaddingTop); 
      if (mStatus == STATUS_ROTATE_ZOOM) { 
        float scale = 1f; 
         
        int halfBitmapWidth = mBitmap.getWidth() / 2; 
        int halfBitmapHeight = mBitmap.getHeight() /2 ; 
         
        //图片某个点到图片中心的距离 
        float bitmapToCenterDistance = FloatMath.sqrt(halfBitmapWidth * halfBitmapWidth + halfBitmapHeight * halfBitmapHeight); 
         
        //移动的点到图片中心的距离 
        float moveToCenterDistance = distance4PointF(mCenterPoint, mCurMovePointF); 
         
        //计算缩放比例 
        scale = moveToCenterDistance / bitmapToCenterDistance; 
         
         
        //缩放比例的界限判断 
        if (scale <= MIN_SCALE) { 
          scale = MIN_SCALE; 
        } else if (scale >= MAX_SCALE) { 
          scale = MAX_SCALE; 
        } 
         
         
        // 角度 
        double a = distance4PointF(mCenterPoint, mPreMovePointF); 
        double b = distance4PointF(mPreMovePointF, mCurMovePointF); 
        double c = distance4PointF(mCenterPoint, mCurMovePointF); 
         
        double cosb = (a * a + c * c - b * b) / (2 * a * c); 
         
        if (cosb >= 1) { 
          cosb = 1f; 
        } 
         
        double radian = Math.acos(cosb); 
        float newDegree = (float) radianToDegree(radian); 
         
        //center -> proMove的向量, 我们使用PointF来实现 
        PointF centerToProMove = new PointF((mPreMovePointF.x - mCenterPoint.x), (mPreMovePointF.y - mCenterPoint.y)); 
         
        //center -> curMove 的向量  
        PointF centerToCurMove = new PointF((mCurMovePointF.x - mCenterPoint.x), (mCurMovePointF.y - mCenterPoint.y)); 
         
        //向量叉乘结果, 如果结果为负数, 表示为逆时针, 结果为正数表示顺时针 
        float result = centerToProMove.x * centerToCurMove.y - centerToProMove.y * centerToCurMove.x; 
 
        if (result < 0) { 
          newDegree = -newDegree; 
        }  
         
        mDegree = mDegree + newDegree; 
        mScale = scale; 
         
        transformDraw(); 
      } 
      else if (mStatus == STATUS_DRAG) { 
        // 修改中心点 
        mCenterPoint.x += mCurMovePointF.x - mPreMovePointF.x; 
        mCenterPoint.y += mCurMovePointF.y - mPreMovePointF.y; 
         
        System.out.println(this + "move = " + mCenterPoint); 
         
        adjustLayout(); 
      } 
       
      mPreMovePointF.set(mCurMovePointF); 
      break; 
    } 
    return true; 
  } 
   
   
   
  /** 
   * 获取四个点和View的大小 
   * @param left 
   * @param top 
   * @param right 
   * @param bottom 
   * @param degree 
   */ 
  private void computeRect(int left, int top, int right, int bottom, float degree){ 
    Point lt = new Point(left, top); 
    Point rt = new Point(right, top); 
    Point rb = new Point(right, bottom); 
    Point lb = new Point(left, bottom); 
    Point cp = new Point((left + right) / 2, (top + bottom) / 2); 
    mLTPoint = obtainRoationPoint(cp, lt, degree); 
    mRTPoint = obtainRoationPoint(cp, rt, degree); 
    mRBPoint = obtainRoationPoint(cp, rb, degree); 
    mLBPoint = obtainRoationPoint(cp, lb, degree); 
     
    //计算X坐标最大的值和最小的值 
    int maxCoordinateX = getMaxValue(mLTPoint.x, mRTPoint.x, mRBPoint.x, mLBPoint.x); 
    int minCoordinateX = getMinValue(mLTPoint.x, mRTPoint.x, mRBPoint.x, mLBPoint.x);; 
     
    mViewWidth = maxCoordinateX - minCoordinateX ; 
     
     
    //计算Y坐标最大的值和最小的值 
    int maxCoordinateY = getMaxValue(mLTPoint.y, mRTPoint.y, mRBPoint.y, mLBPoint.y); 
    int minCoordinateY = getMinValue(mLTPoint.y, mRTPoint.y, mRBPoint.y, mLBPoint.y); 
 
    mViewHeight = maxCoordinateY - minCoordinateY ; 
     
     
    //View中心点的坐标 
    Point viewCenterPoint = new Point((maxCoordinateX + minCoordinateX) / 2, (maxCoordinateY + minCoordinateY) / 2); 
     
    offsetX = mViewWidth / 2 - viewCenterPoint.x; 
    offsetY = mViewHeight / 2 - viewCenterPoint.y; 
     
     
     
    int halfDrawableWidth = mDrawableWidth / 2; 
    int halfDrawableHeight = mDrawableHeight /2; 
     
    //将Bitmap的四个点的X的坐标移动offsetX + halfDrawableWidth 
    mLTPoint.x += (offsetX + halfDrawableWidth); 
    mRTPoint.x += (offsetX + halfDrawableWidth); 
    mRBPoint.x += (offsetX + halfDrawableWidth); 
    mLBPoint.x += (offsetX + halfDrawableWidth); 
 
    //将Bitmap的四个点的Y坐标移动offsetY + halfDrawableHeight 
    mLTPoint.y += (offsetY + halfDrawableHeight); 
    mRTPoint.y += (offsetY + halfDrawableHeight); 
    mRBPoint.y += (offsetY + halfDrawableHeight); 
    mLBPoint.y += (offsetY + halfDrawableHeight); 
     
    mControlPoint = LocationToPoint(controlLocation); 
  } 
   
   
  /** 
   * 根据位置判断控制图标处于那个点 
   * @return 
   */ 
  private Point LocationToPoint(int location){ 
    switch(location){ 
    case LEFT_TOP: 
      return mLTPoint; 
    case RIGHT_TOP: 
      return mRTPoint; 
    case RIGHT_BOTTOM: 
      return mRBPoint; 
    case LEFT_BOTTOM: 
      return mLBPoint; 
    } 
    return mLTPoint; 
  } 
   
   
  /** 
   * 获取变长参数最大的值 
   * @param array 
   * @return 
   */ 
  public int getMaxValue(Integer...array){ 
    List<Integer> list = Arrays.asList(array); 
    Collections.sort(list); 
    return list.get(list.size() -1); 
  } 
   
   
  /** 
   * 获取变长参数最大的值 
   * @param array 
   * @return 
   */ 
  public int getMinValue(Integer...array){ 
    List<Integer> list = Arrays.asList(array); 
    Collections.sort(list); 
    return list.get(0); 
  } 
   
   
   
  /** 
   * 获取旋转某个角度之后的点 
   * @param viewCenter 
   * @param source 
   * @param degree 
   * @return 
   */ 
  public static Point obtainRoationPoint(Point center, Point source, float degree) { 
    //两者之间的距离 
    Point disPoint = new Point(); 
    disPoint.x = source.x - center.x; 
    disPoint.y = source.y - center.y; 
     
    //没旋转之前的弧度 
    double originRadian = 0; 
 
    //没旋转之前的角度 
    double originDegree = 0; 
     
    //旋转之后的角度 
    double resultDegree = 0; 
     
    //旋转之后的弧度 
    double resultRadian = 0; 
     
    //经过旋转之后点的坐标 
    Point resultPoint = new Point(); 
     
    double distance = Math.sqrt(disPoint.x * disPoint.x + disPoint.y * disPoint.y); 
    if (disPoint.x == 0 && disPoint.y == 0) { 
      return center; 
      // 第一象限 
    } else if (disPoint.x >= 0 && disPoint.y >= 0) { 
      // 计算与x正方向的夹角 
      originRadian = Math.asin(disPoint.y / distance); 
       
      // 第二象限 
    } else if (disPoint.x < 0 && disPoint.y >= 0) { 
      // 计算与x正方向的夹角 
      originRadian = Math.asin(Math.abs(disPoint.x) / distance); 
      originRadian = originRadian + Math.PI / 2; 
       
      // 第三象限 
    } else if (disPoint.x < 0 && disPoint.y < 0) { 
      // 计算与x正方向的夹角 
      originRadian = Math.asin(Math.abs(disPoint.y) / distance); 
      originRadian = originRadian + Math.PI; 
    } else if (disPoint.x >= 0 && disPoint.y < 0) { 
      // 计算与x正方向的夹角 
      originRadian = Math.asin(disPoint.x / distance); 
      originRadian = originRadian + Math.PI * 3 / 2; 
    } 
     
    // 弧度换算成角度 
    originDegree = radianToDegree(originRadian); 
    resultDegree = originDegree + degree; 
     
    // 角度转弧度 
    resultRadian = degreeToRadian(resultDegree); 
     
    resultPoint.x = (int) Math.round(distance * Math.cos(resultRadian)); 
    resultPoint.y = (int) Math.round(distance * Math.sin(resultRadian)); 
    resultPoint.x += center.x; 
    resultPoint.y += center.y; 
 
    return resultPoint; 
  } 
 
  /** 
   * 弧度换算成角度 
   * 
   * @return 
   */ 
  public static double radianToDegree(double radian) { 
    return radian * 180 / Math.PI; 
  } 
 
   
  /** 
   * 角度换算成弧度 
   * @param degree 
   * @return 
   */ 
  public static double degreeToRadian(double degree) { 
    return degree * Math.PI / 180; 
  } 
   
  /** 
   * 根据点击的位置判断是否点中控制旋转,缩放的图片, 初略的计算 
   * @param x 
   * @param y 
   * @return 
   */ 
  private int JudgeStatus(float x, float y){ 
    PointF touchPoint = new PointF(x, y); 
    PointF controlPointF = new PointF(mControlPoint); 
     
    //点击的点到控制旋转,缩放点的距离 
    float distanceToControl = distance4PointF(touchPoint, controlPointF); 
     
    //如果两者之间的距离小于 控制图标的宽度,高度的最小值,则认为点中了控制图标 
    if(distanceToControl < Math.min(mDrawableWidth/2, mDrawableHeight/2)){ 
      return STATUS_ROTATE_ZOOM; 
    } 
     
    return STATUS_DRAG; 
     
  } 
   
   
  public float getImageDegree() { 
    return mDegree; 
  } 
 
  /** 
   * 设置图片旋转角度 
   * @param degree 
   */ 
  public void setImageDegree(float degree) { 
    if(this.mDegree != degree){ 
      this.mDegree = degree; 
      transformDraw(); 
    } 
  } 
 
  public float getImageScale() { 
    return mScale; 
  } 
 
  /** 
   * 设置图片缩放比例 
   * @param scale 
   */ 
  public void setImageScale(float scale) { 
    if(this.mScale != scale){ 
      this.mScale = scale; 
      transformDraw(); 
    }; 
  } 
   
 
  public Drawable getControlDrawable() { 
    return controlDrawable; 
  } 
 
  /** 
   * 设置控制图标 
   * @param drawable 
   */ 
  public void setControlDrawable(Drawable drawable) { 
    this.controlDrawable = drawable; 
    mDrawableWidth = drawable.getIntrinsicWidth(); 
    mDrawableHeight = drawable.getIntrinsicHeight(); 
    transformDraw(); 
  } 
 
  public int getFramePadding() { 
    return framePadding; 
  } 
 
  public void setFramePadding(int framePadding) { 
    if(this.framePadding == framePadding) 
      return; 
    this.framePadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, framePadding, metrics); 
    transformDraw(); 
  } 
 
  public int getFrameColor() { 
    return frameColor; 
  } 
 
  public void setFrameColor(int frameColor) { 
    if(this.frameColor == frameColor) 
      return; 
    this.frameColor = frameColor; 
    mPaint.setColor(frameColor); 
    invalidate(); 
  } 
 
  public int getFrameWidth() { 
    return frameWidth; 
  } 
 
  public void setFrameWidth(int frameWidth) { 
    if(this.frameWidth == frameWidth)  
      return; 
    this.frameWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, frameWidth, metrics); 
    mPaint.setStrokeWidth(frameWidth); 
    invalidate(); 
  } 
   
  /** 
   * 设置控制图标的位置, 设置的值只能选择LEFT_TOP ,RIGHT_TOP, RIGHT_BOTTOM,LEFT_BOTTOM 
   * @param controlLocation 
   */ 
  public void setControlLocation(int location) { 
    if(this.controlLocation == location) 
      return; 
    this.controlLocation = location; 
    transformDraw(); 
  } 
 
  public int getControlLocation() { 
    return controlLocation; 
  } 
   
   
 
  public PointF getCenterPoint() { 
    return mCenterPoint; 
  } 
 
  /** 
   * 设置图片中心点位置,相对于父布局而言 
   * @param mCenterPoint 
   */ 
  public void setCenterPoint(PointF mCenterPoint) { 
    this.mCenterPoint = mCenterPoint; 
    adjustLayout(); 
  } 
   
 
  public boolean isEditable() { 
    return isEditable; 
  } 
 
  /** 
   * 设置是否处于可缩放,平移,旋转状态 
   * @param isEditable 
   */ 
  public void setEditable(boolean isEditable) { 
    this.isEditable = isEditable; 
    invalidate(); 
  } 
 
  /** 
   * 两个点之间的距离 
   * @param x1 
   * @param y1 
   * @param x2 
   * @param y2 
   * @return 
   */ 
  private float distance4PointF(PointF pf1, PointF pf2) { 
    float disX = pf2.x - pf1.x; 
    float disY = pf2.y - pf1.y; 
    return FloatMath.sqrt(disX * disX + disY * disY); 
  } 
   
 
} 

为了让SingleTouchView居中,我们需要获取父布局的长和宽,我们在onMeasure()中来获取,当然如果我们不需要居中显示我们也可以调用setCenterPoint方法来设置其位置.

onTouchEvent()方法中,mPreMovePointF和mCurMovePointF点的坐标不是相对View来的,首先如果采用相对于View本身(getX(), getY())肯定是不行的,假如你往x轴方向移动一段距离,这个SingleTouchView也会移动一段距离,mPreMovePointF和mCurMovePointF点和SingleTouchView的中心点都是会变化的,所以在移动的时候会不停的闪烁,相对于屏幕左上角(getRawX(), getRawY())是可以的,但是由于mCenterPointF并不是相对于屏幕的坐标,而是相对于父类布局的,所以将需要将mPreMovePointF和mCurMovePointF的坐标换算成相对于父类布局。

这里面最重要的方法就是transformDraw()方法,它主要做的是调用computeRect()方法求出图片的四个角的坐标点mLTPoint,mRTPoint,mRBPoint,mLBPoint(这几点的坐标是相对于SingleTouchView本身)和SingleTouchView的宽度和高度,以及控制图标所在图标四个点中的哪个点。如下图


上面的图忽略了控制旋转,缩放图标,黑色的框是开始的View的大小,而经过旋转之后,VIew的大小变成最外层的虚线框了,所以我们需要调用adjustLayout()方法来重新设置View的位置和大小,接下来就是设置Matrix了

matrix.setScale(mScale, mScale); 
matrix.postRotate(mDegree % 360, bitmapWidth/2, bitmapHeight/2); 
matrix.postTranslate(offsetX + mDrawableWidth/2, offsetY + mDrawableHeight/2); 

先设置缩放比例, 然后设置围绕图片的中心点旋转mDegree,postTranslate( float dx, float dy)方法是画该图片的起始点进行平移dx, dy个单位,而不是移动到dx,dy这个点。
接下来就来使用,定义一个xml布局文件

<merge xmlns:android="http://schemas.android.com/apk/res/android" 
  xmlns:tools="http://schemas.android.com/tools"> 
 
    <com.example.singletouchview.SingleTouchView 
      xmlns:app="http://schemas.android.com/apk/res-auto" 
      android:id="@+id/SingleTouchView" 
      android:layout_width="match_parent" 
      android:layout_height="match_parent" 
      app:scale="1.2" 
      app:src="@drawable/scale" 
      app:frameColor="#0022ff" 
      app:controlLocation="right_top"/> 
 
</merge> 

在里面写了一些自定义的属性,写自定义属性之前需要声明xmlns:app="http://schemas.android.com/apk/res-auto",Activity只需要设置这个布局文件作为ContentView就行了,接下来运行程序看下效果。


怎么样?效果还是不错的吧,如果我们想去掉蓝色的边框和用于缩放旋转的小图标,直接调用setEditable(false)就可以了,设置了setEditable(false)该View的点击事件,长按事件是正常的。

以上就是本文的全部内容,希望对大家学习Android软件编程有所帮助。


代码注释

作者:喵哥笔记

IDC笔记

学的不仅是技术,更是梦想!