Android 旋转控件

自定义控件

Posted by River on December 20, 2018

前言

18年回顾之前的工作,比较折磨人的某次需求编写一个自定义控件;带有旋转的交互表示用户等级。

1、看下效果静态图

自定义控件效果图

1.1 第一个坑,无论怎么尝试;自控件无法展示在父控件范围之外。

需要在父控件设置一个属性,android:clipChildren=”false”

这个属性默认设置是true,表示不允许扩展绘制;需要关闭这个属性。

1.2 核心代码UserLevelView


public class UserLevelView extends ViewGroup {

    static final String TAG = "UserLevelView";


    private final int cx = DisplayUtil.getDisplayWidth(getContext()) / 2;

    /**
     * 虚线圆弧的切线距离顶部(状态栏)的距离240
     */
    private final int cy = DisplayUtil.dip2px(getContext(),MARGIN_TOP + USER_TITLE_ICON_WIDTH / 2 - RotateCons.USER_LEVEL_RADIUS);

    /**
     * 最左边控件的起始角度
     */
    private float firstChildViewAngle;
    /**
     * 两个控件之间的夹角
     */
    private final float angleBetween = 15.0f;

    private int titleValue;
    private Paint mPaint;

    /**
     * 白色虚线和灰色虚线交界处位置距离左边最近一个图片的角度
     */
    private float locationAngle = 9;

    /**
     * 对位置图标的位置的修正
     */
    private int locationOffset = DisplayUtil.dip2px(getContext(),1);
    // ---------------------------------------

    public UserLevelView(Context context) {
        super(context);
        initView();
    }

    public UserLevelView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public UserLevelView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        layoutChild();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        Log.e(TAG, "getDisplayWidth == " + DisplayUtil.getDisplayWidth(getContext()));
        Log.e(TAG, "getDisplayHeight == " + DisplayUtil.getDisplayHeight(getContext()));
        Log.e(TAG, "widthMeasureSpec == " + MeasureSpec.getSize(widthMeasureSpec));
        Log.e(TAG, "heightMeasureSpec == " + MeasureSpec.getSize(heightMeasureSpec));
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childAt = getChildAt(i);
            if (childAt != null && i > 0) {
                LayoutParams layoutParams = childAt.getLayoutParams();

                childAt.measure(MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY));
            } else if (childAt != null) {
                Log.e(TAG, "测量location");
                childAt.measure(MeasureSpec.makeMeasureSpec(DisplayUtil.dip2px(getContext(),7), MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(DisplayUtil.dip2px(getContext(),10), MeasureSpec.EXACTLY));
            }
        }
    }

    private void layoutChild() {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childAt = getChildAt(i);
            if (childAt != null && i > 0) {

                int width = DisplayUtil.dip2px(getContext(),45);
                int height = DisplayUtil.dip2px(getContext(),53);


                int marginLeft = (DisplayUtil.getDisplayWidth(getContext()) - width) / 2;
                int marginTop = DisplayUtil.dip2px(getContext(),MARGIN_TOP);

                childAt.layout(marginLeft, marginTop,
                        marginLeft + width,
                        marginTop + height);

            } else if (i == 0 && childAt != null) {
                Log.e(TAG, "摆放location");
                int marginLeft = (DisplayUtil.getDisplayWidth(getContext()) - childAt.getMeasuredWidth()) / 2;
                int marginBottom = DisplayUtil.dip2px(getContext(),MARGIN_TOP + USER_TITLE_ICON_WIDTH / 2 - locationOffset);

                childAt.layout(marginLeft, marginBottom - childAt.getMeasuredHeight(),
                        marginLeft + childAt.getMeasuredWidth(),
                        marginBottom);

                Log.e(TAG + "onLayout", "marginLeft = " + marginLeft);
                Log.e(TAG + "onLayout", "heightTop = " + (marginBottom - childAt.getMeasuredHeight()));
                Log.e(TAG + "onLayout", "marginRight = " + (marginLeft + childAt.getMeasuredWidth()));
                Log.e(TAG + "onLayout", "marginBottom = " + marginBottom);
            }
        }
    }


    private void rotateView(UserTitleBean userTitleBean, int titleValue) {
        setPivotX(cx);
        setPivotY(cy);

        int childCount = getChildCount();

        for (int i = 0; i < childCount; i++) {

            View childAt = getChildAt(i);
            if (i > 0) {
                childAt.setPivotY(DisplayUtil.dip2px(getContext(),USER_TITLE_ICON_WIDTH) / 2 - DisplayUtil.dip2px(getContext(),RotateCons.USER_LEVEL_RADIUS));
                childAt.setPivotX(DisplayUtil.dip2px(getContext(),45) / 2);

                childAt.setRotation(firstChildViewAngle);
                firstChildViewAngle -= angleBetween;

                TextView tv_level_name = (TextView) childAt.findViewById(R.id.tv_level_name);

                if (i <= titleValue) {
                    tv_level_name.setTextColor(ContextCompat.getColor(getContext(), R.color.white));
                } else {
                    tv_level_name.setTextColor(ContextCompat.getColor(getContext(), R.color.text_color_white30));
                }
                tv_level_name.setText(userTitleBean.getData().get(i - 1).getTitle());

                ImageView iv_level = (ImageView) childAt.findViewById(R.id.iv_level);

                if (i <= titleValue) {
                    iv_level.setImageResource(ConstantsUserLevel.PORTRAITS[i - 1]);
                } else {
                    iv_level.setImageResource(ConstantsUserLevel.PORTRAITS_DISABLED[i - 1]);
                }
            } else if (i == 0) {
                Log.e(TAG, "旋转location");
                childAt.setPivotY(DisplayUtil.dip2px(getContext(),10) / 2 - DisplayUtil.dip2px(getContext(),RotateCons.USER_LEVEL_RADIUS));
                childAt.setPivotX(DisplayUtil.dip2px(getContext(),7) / 2);

                childAt.setRotation(-locationAngle);
            }
        }
    }

    private void initView() {
        if (mPaint == null) {
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mPaint.setStrokeWidth(DisplayUtil.dip2px(getContext(),2));
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setColor(Color.GRAY);
            // 5像素实线,5像素空白。0表示偏移量,动态改变这个值可以让虚线动起来
            mPaint.setPathEffect(new DashPathEffect(new float[]{DisplayUtil.dip2px(getContext(),3),
                    DisplayUtil.dip2px(getContext(),3)}, 0));
        }
        for (int i = 0; i < RotateCons.USER_LEVEL_VIEW_CHILD_COUNT; i++) {
            if (i == 0) {
                Log.e(TAG, "inflate(getContext() == " + i);
                inflate(getContext(), R.layout.item_level_location, this);
            } else {
                Log.e(TAG, "inflate(getContext() == " + i);
                inflate(getContext(), R.layout.item_user_level, this);
            }
        }
    }

    public void freshViewState(int titleValue, UserTitleBean userTitleBean, float rate) {
        firstChildViewAngle = 15.0f * (titleValue - 1);
        locationAngle = rate * angleBetween;
        this.titleValue = titleValue;
        LogUtils.e(TAG, "firstChildViewAngle == " + firstChildViewAngle);
        LogUtils.e(TAG, "locationAngle == " + locationAngle);
        rotateView(userTitleBean, titleValue);
        setVisibility(VISIBLE);
    }

}
    

1.3 核心代码手势识别控制

记录坐标AspectRatio


public class UserLevelRotateGesture {

    static final String TAG = "UserLevelRotateGesture";

    /**
     * 圆弧的圆心坐标
     */
    private final int ox = DisplayUtil.getDisplayWidth(SdkConfig.getApplicationContext()) / 2;
    private final int oy = DisplayUtil.dip2px(SdkConfig.getApplicationContext(),RotateCons.MARGIN_TOP + 15 - RotateCons.USER_LEVEL_RADIUS);

    /**
     * 点击在view中所在坐标
     */
    private float x, y;


    private OnRotateListener mOnRotateListener;

    public UserLevelRotateGesture(OnRotateListener mOnRotateListener) {
        this.mOnRotateListener = mOnRotateListener;
    }

    public void onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 初始点击xy值
                x = event.getX();
                y = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                //求伪瞬时速度

                float nowX = event.getX();
                float nowY = event.getY();

                // 计算三边的平方
                float ab2 = (x - nowX) * (x - nowX) + (y - nowY) * (y - nowY);
                float oa2 = (x - ox) * (x - ox) + (y - oy) * (y - oy);
                float ob2 = (nowX - ox) * (nowX - ox) + (nowY - oy) * (nowY - oy);

                // 根据两向量的叉乘来判断顺逆时针
                boolean isClockwise = ((x - ox) * (nowY - oy) - (y - oy) * (nowX - ox)) > 0;

                // 根据余弦定理计算旋转角的余弦值
                double cosDegree = (oa2 + ob2 - ab2) / (2 * Math.sqrt(oa2) * Math.sqrt(ob2));

                // 异常处理,因为算出来会有误差绝对值可能会超过一,所以需要处理一下
                if (cosDegree > 1) {
                    cosDegree = 1;
                } else if (cosDegree < -1) {
                    cosDegree = -1;
                }

                // 计算弧度
                double radian = Math.acos(cosDegree);

                // 计算旋转过的角度,顺时针为正,逆时针为负
                float degree = (float) (isClockwise ? Math.toDegrees(radian) : -Math.toDegrees(radian));

                // 累加角度
                if (mOnRotateListener != null) {
                        mOnRotateListener.onMoveDegrees(degree);
                }
                // 更新触摸点
                x = nowX;
                y = nowY;

                break;
            // 经过一系列的事件处理后,这里的ACTION_UP偶尔不会触发
//            case MotionEvent.ACTION_UP:
//            case MotionEvent.ACTION_CANCEL:
            default:
                break;
        }
    }

    /**
     * 手指触摸屏幕的监听接口
     */
    public interface OnRotateListener {
        /**
         * 回调转动的角度
         *
         * @param degrees,每次触发ACTION_MOVE发生的角度变化量(相当于微分变量)
         */
        void onMoveDegrees(float degrees);
    }

}

1.4 竖直方向滑动事件处理冲突处理。

    
    public class UserLevelScrollView extends ScrollView {

    private final int ox = DisplayUtil.getDisplayWidth(getContext()) / 2;
    private final int oy = DisplayUtil.dip2px(getContext(),225 + 15 - 450);


    public UserLevelScrollView(Context context) {
        this(context, null);
    }

    public UserLevelScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public UserLevelScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    private UserLevelRotateGesture mUserLevelRotateGesture;

    public void setUserLevelRotateGesture(UserLevelRotateGesture userLevelRotateGesture) {
        this.mUserLevelRotateGesture = userLevelRotateGesture;
    }

    private float mDownPosX;
    private float mDownPosY;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mUserLevelRotateGesture != null && isInRadius(ev)) {
            mUserLevelRotateGesture.onTouchEvent(ev);
            Log.e("BetterScrollView", "dispatchTouchEvent,ev.getX == " + ev.getX());
            return true;
        }
        try {
            return super.dispatchTouchEvent(ev);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mDownPosX = ev.getX();
                mDownPosY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                final float deltaX = Math.abs(ev.getX() - mDownPosX);
                final float deltaY = Math.abs(ev.getY() - mDownPosY);
                // 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截
                if (deltaX > deltaY || isInRadius(ev)) {
                    return false;
                }
            default:
                break;
        }

        if (isInRadius(ev)) {
            Log.e("BetterScrollView", "onInterceptTouchEvent,ev.getX == " + ev.getX());
            return false;
        }
        return super.onInterceptTouchEvent(ev);
    }


    private boolean isInRadius(MotionEvent event) {

        float dx = event.getX() - ox;
        // 加上getScrollY动态的控制滑动区域
        float dy = event.getY() - oy + getScrollY();

        return Math.sqrt(dx * dx + dy * dy) < DisplayUtil.dip2px(getContext(),450 + 80)
                && Math.sqrt(dx * dx + dy * dy) > DisplayUtil.dip2px(getContext(),450 - 50);
    }


    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mOnScrollListener != null) {
            mOnScrollListener.onScroll(getScrollY());
        }
    }

    private OnScrollListener mOnScrollListener;

    /**
     * 滑动距离监听器
     */
    public interface OnScrollListener {
        /**
         * 在滑动的时候调用,scrollY为已滑动的距离
         *
         * @param scrollY,表示ScrollView滑动的距离
         */
        void onScroll(int scrollY);
    }

    public void setOnScrollListener(OnScrollListener mOnScrollListener) {
        this.mOnScrollListener = mOnScrollListener;
    }
}
    

2、总结:险夷原不滞胸中,何异浮云过太空

  1. 这自定义控件折磨了我快一个月,头皮油了好几层;
  2. 这里引用王阳明的泛海勉励自己,要心平气和,越挫越勇。