package com.example.abc.myfirstapplication.SelfView; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; /** * 作者:abc on 2016/6/21 14:39 * 邮箱:liyuchong@kocla.com */ public class SimpleView1 extends View implements View.OnClickListener { //用于绘制的paint对象 private Paint mPaint; private Rect mBounds; //蓝色圆中间显示的数字,每次点击,数字都会增加 private int mCount; //白色圆点(以下称:小圆)的圆心与蓝色圆(以下称:大圆)的圆心的距离 private float mInnerCircleRadius = 150; //定义了大圆圆心和小圆圆心坐标对应的点 private float center_x, center_y, whitepoint_x, whitepoint_y; //小圆 private Point innerPoint = new Point(whitepoint_x, whitepoint_y); //大圆 private Point centerPoint = new Point(center_x, center_y); //定义layout的半径...即大圆的半径 private int mRadius; //定义初始角度...用于onTouchEvent中计算小圆的圆心坐标 private double StartAngle; /* * 加速度检测部分 * 用于检测手指滑动并抬起后,小圆是否需要继续滑动 * */ //手指按下和抬起时对应点的角度值 private float DownAngle; private float UpAngle; //手指按下时的系统时间 private long DownTime; //每个move动作经过的角度 private float TmpAngle; //判断是否在自由旋转... private boolean isFling; //每个动作的初始点坐标 private float mLastX; private float mLastY; //用于处理后续小圆滑动的线程 private FlingRunnable mFlingRunnable; public SimpleView1(Context context) { super(context); init(); } public SimpleView1(Context context, AttributeSet attrs) { super(context, attrs); init(); } public SimpleView1(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } /** * 初始化 */ private void init() { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBounds = new Rect(); this.setOnTouchListener(onTouchListener); } /** * <p> * Measure the view and its content to determine the measured width and the * measured height. This method is invoked by {@link #measure(int, int)} and * should be overridden by subclasses to provide accurate and efficient * measurement of their contents. * </p> * <p> * <p> * <strong>CONTRACT:</strong> When overriding this method, you * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the * measured width and height of this view. Failure to do so will trigger an * <code>IllegalStateException</code>, thrown by * {@link #measure(int, int)}. Calling the superclass' * {@link #onMeasure(int, int)} is a valid use. * </p> * <p> * <p> * The base class implementation of measure defaults to the background size, * unless a larger size is allowed by the MeasureSpec. Subclasses should * override {@link #onMeasure(int, int)} to provide better measurements of * their content. * </p> * <p> * <p> * If this method is overridden, it is the subclass's responsibility to make * sure the measured height and width are at least the view's minimum height * and width ({@link #getSuggestedMinimumHeight()} and * {@link #getSuggestedMinimumWidth()}). * </p> * * @param widthMeasureSpec horizontal space requirements as imposed by the parent. * The requirements are encoded with * {@link MeasureSpec}. * @param heightMeasureSpec vertical space requirements as imposed by the parent. * The requirements are encoded with * {@link MeasureSpec}. * @see #getMeasuredWidth() * @see #getMeasuredHeight() * @see #setMeasuredDimension(int, int) * @see #getSuggestedMinimumHeight() * @see #getSuggestedMinimumWidth() * @see MeasureSpec#getMode(int) * @see MeasureSpec#getSize(int) */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //设定该view的宽和高,并设定大小圆的圆心坐标 super.onMeasure(widthMeasureSpec, heightMeasureSpec); // int withMode = MeasureSpec.getMode(widthMeasureSpec); // int withSize = MeasureSpec.getSize(widthMeasureSpec); mRadius = getWidth() < getHeight() ? getWidth() / 2 : getHeight() / 2; centerPoint.x = getWidth() / 2; centerPoint.y = getHeight() / 2; innerPoint.x = centerPoint.x + mInnerCircleRadius; innerPoint.y = centerPoint.y; } /** * Called from layout when this view should * assign a size and position to each of its children. * <p/> * Derived classes with children should override * this method and call layout on each of * their children. * * @param changed This is a new size or position for this view * @param left Left position, relative to parent * @param top Top position, relative to parent * @param right Right position, relative to parent * @param bottom Bottom position, relative to parent */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); } /** * 绘制该view * <p/> * Implement this to do your drawing. * * @param canvas the canvas on which the background will be drawn */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaint.setColor(Color.BLUE); canvas.drawCircle(centerPoint.x, centerPoint.y, mRadius, mPaint); mPaint.setColor(Color.WHITE); canvas.drawCircle(innerPoint.x, innerPoint.y, 10, mPaint); // mPaint.setColor(Color.RED); // canvas.drawLine(centerPoint.x - mRadius, centerPoint.y, centerPoint.x + mRadius, centerPoint.y, mPaint); mPaint.setColor(Color.YELLOW); mPaint.setTextSize(60); String text = String.valueOf(mCount); mPaint.getTextBounds(text, 0, text.length(), mBounds); float textWidth = mBounds.width(); float textHeight = mBounds.height(); canvas.drawText(text, getWidth() / 2 - textWidth / 2, getHeight() / 2 + textHeight / 2, mPaint); // StartAngle = getAngle(whitepoint_x, whitepoint_y); // Log.d("SimpleView1", "---------------innerPoint.x " + innerPoint.x); // Log.d("SimpleView1", "---------------innerPoint.y " + innerPoint.y); } @Override public void onClick(View v) { } /** * Touch动作的监听器 * 随着手指的滑动,计算出小圆圆心的坐标并不断重绘该view */ private OnTouchListener onTouchListener = new OnTouchListener() { // float oldx, oldy; //这个flag用于判断是否发生了点击事件 boolean flag = true; @Override public boolean onTouch(View view, MotionEvent event) { float x = event.getX(); float y = event.getY(); //用于判断该点是否在大圆内部 if (!isContained(x, y, centerPoint, mRadius)) { return true; } int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mLastX = x; mLastY = y; // float start1 = getAngle(mLastX, mLastY); // Log.d("SimpleView1", "---------------start1角度是 " + start1); DownAngle = getAngle(x, y, centerPoint);//获取角度... DownTime = System.currentTimeMillis(); //系统的当前时间... TmpAngle = 0; flag = true; //如果当前在进行快速滚动,那么移除对快速移动的回调...其实就是如果这个界面正在旋转,在旋转的期间我DOWN了一下,那么直接就停止旋转... if (isFling) { removeCallbacks(mFlingRunnable); isFling = false; return true; } break; case MotionEvent.ACTION_MOVE: flag = false; float start = getAngle(mLastX, mLastY, centerPoint); // Log.d("SimpleView1", "---------------start角度是 " + start); float end = getAngle(x, y, centerPoint); // Log.d("SimpleView1", "---------------end角度是 " + end); TmpAngle = end - start; // Log.d("SimpleView1", "---------------tmp角度是 " + TmpAngle); //新的角度值,根据这个角度值可以判断现在内部原点应该所在的象限及原点坐标 StartAngle += TmpAngle; //更新小圆的圆心坐标并更新StartAngle的值 StartAngle = updateInnerCircleOriginalPoint(StartAngle, innerPoint, mInnerCircleRadius, centerPoint); //将初始值设置为旋转后的值... mLastX = x; mLastY = y; break; case MotionEvent.ACTION_UP: //获取手指抬起时的角度... UpAngle = getAngle(x, y, centerPoint); //计算每秒钟移动的角度... float anglePrMillionSecond = (UpAngle - DownAngle) * 1000 / (System.currentTimeMillis() - DownTime); Log.d("SimpleView1", "---------------anglePrMillionSecond " + anglePrMillionSecond); //如果数值大于这个指定的数值,那么就会认为是加速滚动... if (Math.abs(anglePrMillionSecond) > 230 && !isFling) { //开启一个新的线程,让其进行自由滚动... post(mFlingRunnable = new FlingRunnable(anglePrMillionSecond)); } if (Math.abs(anglePrMillionSecond) > 230 || isFling) { return true; } if (flag) { mCount++; invalidate(); } flag = true; break; } return true; } };/** * 该函数根据小圆圆心新的角度值计算圆心的坐标 * 根据圆心所在角度转换为弧度,计算sin和cos值,再结合大圆圆心坐标计算出小圆圆心坐标。 * * @param newAngle * @param point * @param innerRadius * @param centerPoint * @return */ private double updateInnerCircleOriginalPoint(double newAngle, Point point, float innerRadius, Point centerPoint) { double StartAngle_PI; StartAngle_PI = newAngle * Math.PI / 180; double y_distance = Math.sin(StartAngle_PI) * innerRadius; double x_distance = Math.cos(StartAngle_PI) * innerRadius; point.x = (float) (x_distance + centerPoint.x); point.y = (float) (centerPoint.y - y_distance); invalidate(); return newAngle; }/** * 判断给定的x和y所对应的点是否在以centerPoint为圆心以radius为半径的圆内部 * * @param x * @param y * @param centerPoint * @return */ private boolean isContained(float x, float y, Point centerPoint, float radius) { Point now = new Point(x, y); if (centerPoint.getDistance(now) > radius) return false; return true; } /** * 自己定义的Point类 */ class Point { public float getX() { return x; } public void setX(float x) { this.x = x; } public float getY() { return y; } public void setY(float y) { this.y = y; } private float x; private float y; public Point(float x, float y) { this.x = x; this.y = y; } public double getDistance(Point p) { double _x = Math.abs(this.x - p.x); double _y = Math.abs(this.y - p.y); return Math.sqrt(_x * _x + _y * _y); } } /** * 计算手指触摸的点在以point为圆心的点到x轴正向的角度 * 我这里处理的发杂了,其实可以直接返回弧度值,那样在 updateInnerCircleOriginalPoint()方法中就不需要重新计算弧度了 * @param xTouch * @param yTouch * @return */ private float getAngle(float xTouch, float yTouch, Point point) { float ac = (float) (Math.asin((point.y - yTouch) / Math.hypot(Math.abs(xTouch - point.x), Math.abs(yTouch - point.y))) * 180 / Math.PI); if (xTouch >= point.x) { return ac; } else { if (yTouch < point.y) return 180 - ac; else return -180 - ac; } } private class FlingRunnable implements Runnable { private float velocity; public FlingRunnable(float velocity) { this.velocity = velocity; } public void run() { if ((int) Math.abs(velocity) < 20) { isFling = false; return; } //减速旋转... isFling = true; StartAngle += (velocity / 30); velocity /= 1.0666F; postDelayed(this, 30);//需要保证时刻对页面进行刷新..因为始终要进行新的布局... StartAngle = updateInnerCircleOriginalPoint(StartAngle, innerPoint, mInnerCircleRadius, centerPoint); // invalidate(); } } }
注释挺清楚的,如果有什么疑问,还请多多在评论中提出!
补充一个自定义ViewGroup的参考链接:http://www.cnblogs.com/RGogoing/p/4676654.html?utm_source=tuicool&utm_medium=referral