效果图以及相关的两张图片资源:

实现步骤:

  1. 继承View
  2. 重写onTouchEvent,根据触摸坐标计算角度
  3. 重写onDraw,根据角度旋转并绘制图片

代码如下:

  1 import android.annotation.SuppressLint;
  2 import android.content.Context;
  3 import android.content.res.Resources;
  4 import android.graphics.Bitmap;
  5 import android.graphics.BitmapFactory;
  6 import android.graphics.Canvas;
  7 import android.graphics.Matrix;
  8 import android.graphics.Paint;
  9 import android.util.AttributeSet;
 10 import android.view.MotionEvent;
 11 import android.view.View;
 12 
 13 import androidx.annotation.IntDef;
 14 
 15 /**
 16  * 自定义云台控件
 17  * <p>2020-06-02: create by zenghm
 18  */
 19 public class PTZView extends View {
 20 
 21     public static final int NONE = 0;
 22     public static final int TOP = 1;
 23     public static final int BOTTOM = 2;
 24     public static final int LEFT = 3;
 25     public static final int RIGHT = 4;
 26 
 27     @IntDef({NONE, LEFT, TOP, RIGHT, BOTTOM})
 28     public @interface Direction {
 29     }
 30 
 31     public interface DirectionChangedListener {
 32         /**
 33          * 方向变化回调
 34          *
 35          * @param oldDirection 之前的方向
 36          * @param curDirection 当前的方向
 37          */
 38         void onDirectionChanged(PTZView view, @Direction int oldDirection, @Direction int curDirection);
 39     }
 40 
 41     private DirectionChangedListener mListener;
 42     @Direction
 43     private int mDirection = NONE;
 44     private float mCenterX, mCenterY;
 45     private float mAngel = 0;
 46 
 47     private Bitmap mDefaultBitmap;
 48     private Bitmap mPressBitmap;
 49     private Matrix mMatrix = new Matrix();
 50     private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 51 
 52     public PTZView(Context context) {
 53         this(context, null, 0);
 54     }
 55 
 56     public PTZView(Context context, AttributeSet attrs) {
 57         this(context, attrs, 0);
 58     }
 59 
 60     public PTZView(Context context, AttributeSet attrs, int defStyle) {
 61         super(context, attrs, defStyle);
 62         // 加载资源
 63         Resources res = context.getResources();
 64         mDefaultBitmap = BitmapFactory.decodeResource(res, R.drawable.ptz_default);
 65         mPressBitmap = BitmapFactory.decodeResource(res, R.drawable.ptz_press);
 66     }
 67 
 68     public void setOnDirectionChangedListener(DirectionChangedListener listener) {
 69         mListener = listener;
 70     }
 71 
 72     @Override
 73     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
 74         super.onSizeChanged(w, h, oldw, oldh);
 75         mCenterX = (float) w / 2;
 76         mCenterY = (float) h / 2;
 77     }
 78 
 79     @Override
 80     protected void onDraw(Canvas canvas) {
 81         super.onDraw(canvas);
 82         Bitmap bitmap = isPressed() ? mPressBitmap : mDefaultBitmap;
 83         // 计算图片缩放
 84         float sx = (float) getWidth() / bitmap.getWidth();
 85         float sy = (float) getHeight() / bitmap.getHeight();
 86         // 设置变换矩阵(缩放、旋转)
 87         mMatrix.reset();
 88         mMatrix.postScale(sx, sy);
 89         mMatrix.postRotate(-mAngel, mCenterX, mCenterY);
 90         // 绘制图片
 91         canvas.drawBitmap(bitmap, mMatrix, mPaint);
 92     }
 93 
 94     @SuppressLint("ClickableViewAccessibility")
 95     @Override
 96     public boolean onTouchEvent(MotionEvent event) {
 97         //super.onTouchEvent(event);
 98         if (!isEnabled())
 99             return false;
100 
101         switch (event.getAction()) {
102             case MotionEvent.ACTION_DOWN:
103                 setPressed(true);
104                 postInvalidate();
105                 break;
106 
107             case MotionEvent.ACTION_UP:
108             case MotionEvent.ACTION_CANCEL:
109                 if (mListener != null) {
110                     mListener.onDirectionChanged(this, mDirection, NONE);
111                 }
112                 mDirection = NONE;
113                 mAngel = 0;
114                 setPressed(false);
115                 postInvalidate();
116                 break;
117 
118             case MotionEvent.ACTION_MOVE:
119                 // 计算角度
120                 float x = event.getX() - mCenterX;
121                 float y = mCenterY - event.getY();
122                 mAngel = (float) (Math.atan2(y, x) / Math.PI * 180);
123                 // 重新绘制
124                 postInvalidate();
125                 // 根据角度判断方向
126                 int direction;
127                 if (mAngel > -45 && mAngel <= 45) {
128                     direction = LEFT;
129                 } else if (mAngel > 45 && mAngel <= 135) {
130                     direction = TOP;
131                 } else if (mAngel <= -45 && mAngel > -135) {
132                     direction = BOTTOM;
133                 } else {
134                     direction = RIGHT;
135                 }
136                 // 回调方向变化
137                 if (mListener != null && mDirection != direction) {
138                     mListener.onDirectionChanged(this, mDirection, direction);
139                 }
140                 mDirection = direction;
141                 break;
142 
143             default:
144                 break;
145         }
146         return true;
147     }
148 }

 【注】可以通过自定义属性传入图片,增强控件的自定义能力。自定义属性不在本文的讨论范围。

谢谢阅读,如有谬误,多谢指正。