前言
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、总结:险夷原不滞胸中,何异浮云过太空
- 这自定义控件折磨了我快一个月,头皮油了好几层;
- 这里引用王阳明的泛海勉励自己,要心平气和,越挫越勇。