我站在巨人的肩膀上
关于layout只是水平或者垂直摆放子控件的话好像根本没什么特别的东西,于是思考摆出一个圆形菜单,研究几天还是不太满意自己的结果,于是查阅了前人的写法果然受益匪浅。首先贴上鸿洋的博客:http://blog.csdn.net/lmj623565791/article/details/43131133。
正文
刚开始设计的时候想的是如何做个圆形的菜单,继承ViewGroup 重写layout方法,按照位置来摆放就ok,计算一下弧度角度,问题应该都不大,初步实现的时候确实问题不是特别的大。
- 保证是一个圆形的View()
- 计算多少个Item之间的弧度差。
- 获取到子View的中心点,通过中心点以及宽高来判断自己的layout 位置。
onLayout()代码:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int parentHeight = getMeasuredHeight();
int parentWidth = getMeasuredWidth();
int count = getChildCount();
for (int i = 0; i < count; i++) {
View childView = getChildAt(i);
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
int length = Math.round(parentWidth / 2 - childWidth / 2);
int left = parentWidth / 2 + (int) Math.round(length * Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f
* childWidth);
int top = parentWidth
/ 2
+ (int) Math.round(length
* Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f
* childWidth);
childView.layout(left, top, left + childWidth, top + childHeight);
mStartAngle += mpadding;
}
}
在onMeasure()中进行了圆形的设置简单的写法就是:
measureChildren(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
width = height = Math.min(width, height);
setMeasuredDimension(width, height);
讲道理这里应该没有任何问题,根据前面的onlayout()第一篇,肯定难道不大,但是,我在使用的时候翻车了,当view写在ConstraintLayout中的时候,我只有一个View就是自定义的View,宽高写match_parent,然后通过onMeasure()来设置宽高相等的时候发现就怎么搞都不好使,还是占满了全屏。查阅资料之后,根布局换成LinearLayout(逃避的办法),又有说用使用0dp 表示 match_constraint 即可,换上之后 然而并没有什么用。此处埋点有坑。换成LinearLayout继续写。下面开始数学计算,讲解一下这个圆形的layout具体的摆放过程。首先圆点的问题这个很容易算,View的一半,第二算一下每个子view摆放的位置。一般来进行摆放我们可以搞两个同心圆,外面的圆就是这个View的大小,里面的圆的线路就是子View摆放的位置的中心。
已知A,B 坐标,加AB半径,再通过每个之间的间距的弧度,直接可以推算出第二个点,第三个点......以及后面点的位置.好吧其实画这个图不是来算每个子项的位置的,而是为了后面滑动准备的。代码
int left = parentWidth / 2 + (int) Math.round(length * Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f
* childWidth);
这个其实是算距离子View 左边的位置,之前说过位置的摆放是左上右下的来的,这里先通过子View的圆心距离父View的圆心减去子View的半径,就可以算出来子View距离圆心的左边,以此推算出上面(Top),然后调用child.layout()方法进行摆放。这也就完成了初步的计算与布局。(这里后来又想了一下继承View 直接通过onDraw()来实现子菜单好像也是可以,这个只要算出子View的圆心一会也能直接画出来,但是从可塑性上来看,还是继承ViewGroup来比较好)。
下面开始搞滑动。还是上面那个图,B点是我们按下的点,然后滑动到了C点,刚开始我计算的时候走进的误区就是通过B,C两点来计算偏移量(角度),那个复杂的,越算越怀疑人生,涉及到角度,涉及到象限,什么时候加,什么时候减,麻烦的一腿,后来看鸿洋的博客,直接顺了两个方法:
private float getAngle(float xTouch, float yTouch)
{
double x = xTouch - (getMeasuredWidth() / 2d);
double y = yTouch - (getMeasuredWidth() / 2d);
return (float) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);
}
方法1,计算角度,所有的角度分为两块,都是跟圆心A点之间的角度来算角度差。
方法2,算象限:
private int getQuadrant(float x, float y)
{
int tmpX = (int) (x - getMeasuredWidth() / 2);
int tmpY = (int) (y - getMeasuredWidth() / 2);
if (tmpX >= 0)
{
return tmpY >= 0 ? 4 : 1;
} else
{
return tmpY >= 0 ? 3 : 2;
}
}
两个方法之后就是onMove()中的代码:
float start = getAngle(lastX, lastY);
/**
* 获得当前的角度
*/
float end = getAngle(x, y);
// Log.e("TAG", "start = " + start + " , end =" + end);
// 如果是一、四象限,则直接end-start,角度值都是正值
if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4)
{
mStartAngle += end - start;
} else
// 二、三象限,色角度值是付值
{
mStartAngle += start - end;
}
// 重新布局
requestLayout();
lastX = x;
lastY = y;
break;
正常情况之前的View进行状态改变我们调用invalidate()方法,但是现在是 ViewGroup 咋可以换一个方法来整。当然后期还有其他的方法来做。原理其实很简单,我们不需要去确切的计算每一个子View的距离,只要保证我们的起点第一个子View的位置,然后通过View之间的角度来动态算出后面子View的位置。这样就能整出可以跟着手指滑动的菜单View了。
到这里onLayout()就算是告一段落了,但是呢鸿洋大神还在这个View中加了惯性滑动,他的计算方法就是记录按下的时间与抬起的时间,计算出这个时间段走过的多少角度,然后给自己设定的一个值进行比较,再来算出后续惯性滑动的距离是多少。
再次给链接:http://blog.csdn.net/lmj623565791/article/details/43131133。思路就是那个思路。至于后续添加子View,按键事件,都跟onLayout()一中一样。
但是我这用的另外一个东西来进行处理。
VelocityTracker
这个是个速度检测器,使用起来也很简单。首先在down中进行注册,move的时候进行事件监听,up的时候看一下滑动了偏移量,这个是靠系统来计算的,然后自己定义一个标准让他来继续惯性就完事。
使用代码Down:
case MotionEvent.ACTION_DOWN:
//初始化速度追踪
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
} else {
velocityTracker.clear();
}
Move ---> velocityTracker.addMovement(event);
Up
velocityTracker.computeCurrentVelocity(1000); 此处单位为1S =1000ms
velocity = velocityTracker.getYVelocity();//获取的是Y轴的单位偏移量,同样有获取X的方法
float minVelocity = ViewConfiguration.get(mContext).getScaledMinimumFlingVelocity();//可以帮他当成一个对比的量
if (Math.abs(velocity) > minVelocity) {//因为滑动有上下,即出现正负的情况。
continueScroll = true;
continueScroll();
} else {
velocityTracker.recycle();
velocityTracker = null;
}
最后继续惯性滑动的代码:
private void continueScroll() {
new Thread(new Runnable() {
@Override
public void run() {
float velocityAbs = 0;//速度绝对值
if (velocity > 0 && continueScroll) {
velocity -= 300;
mStartAngle += 20;
velocityAbs = velocity;
} else if (velocity < 0 && continueScroll) {
velocity += 300;
mStartAngle -= 20;
velocityAbs = velocity;
}
handler.sendEmptyMessage(0);
if (continueScroll && Math.abs(velocityAbs) > 300) {
post(this);
} else {
continueScroll = false;
}
}
}).start();
这里给出的300 是自己实际测试出来的单位数据,不是特别的严谨。但是可以完成比较合适的惯性效果,当然要是实现更复杂的惯性,比如越来越慢,最后来个类似弹性的东西,可以在这计算的时候花点时间。最后记得在主线程中调用requestLayout()。
最后未改善的代码:
public class CircleView extends ViewGroup {
String TAG = "CircleView";
Context mContext;
private double mStartAngle = 0;
float lastX = 0;
float lastY = 0 ;
private double offsetAngle;
int[] items;
public CircleView(Context context) {
this(context, null);
}
public CircleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
Log.e(TAG, "onMeasure: "+width+"-->"+height );
int size = Math.min(width, height);
Log.e(TAG, "onMeasure: "+size);
setMeasuredDimension(size, size);
measureChildren(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "onMeasure:-- " + getMeasuredHeight() + "---->height" + getMinimumWidth());
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.d(TAG, "onLayout: "+mStartAngle);
int parentHeight = getMeasuredHeight();
int parentWidth = getMeasuredWidth();
int count = getChildCount();
for (int i = 0; i < count; i++) {
View childView = getChildAt(i);
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
int length = Math.round(parentWidth / 2 - childWidth / 2);
int left = parentWidth / 2 + (int) Math.round(length * Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f
* childWidth);
int top = parentWidth
/ 2
+ (int) Math.round(length
* Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f
* childWidth);
childView.layout(left, top, left + childWidth, top + childHeight);
mStartAngle += offsetAngle;
}
}
public void bindView(int[] items) {
this.items = items;
offsetAngle = mStartAngle = 360 / items.length;
int count = items.length;
for (int i = 0; i < count; i++) {
ImageView imageView = new ImageView(mContext);
imageView.setImageResource(items[i]);
this.addView(imageView);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//初始化速度追踪
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
} else {
velocityTracker.clear();
}
continueScroll = false;
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
velocityTracker.addMovement(event);
Log.d(TAG, "onTouchEvent: "+"MOVE");
/**
* 获得开始的角度
*/
float start = getAngle(lastX, lastY);
/**
* 获得当前的角度
*/
float end = getAngle(x, y);
// Log.e("TAG", "start = " + start + " , end =" + end);
// 如果是一、四象限,则直接end-start,角度值都是正值
if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4)
{
mStartAngle += end - start;
// mTmpAngle += end - start;
} else
// 二、三象限,色角度值是付值
{
mStartAngle += start - end;
// mTmpAngle += start - end;
}
// 重新布局
requestLayout();
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_UP:
velocityTracker.computeCurrentVelocity(2000);
velocity = velocityTracker.getYVelocity();
float minVelocity = ViewConfiguration.get(mContext).getScaledMinimumFlingVelocity();
if (Math.abs(velocity) > minVelocity) {
continueScroll = true;
continueScroll();
} else {
velocityTracker.recycle();
velocityTracker = null;
}
break;
}
return true;
}
private int getQuadrant(float x, float y)
{
int tmpX = (int) (x - getMeasuredWidth() / 2);
int tmpY = (int) (y - getMeasuredWidth() / 2);
if (tmpX >= 0)
{
return tmpY >= 0 ? 4 : 1;
} else
{
return tmpY >= 0 ? 3 : 2;
}
}
/**
* 根据触摸的位置,计算角度
*
* @param xTouch
* @param yTouch
* @return
*/
private float getAngle(float xTouch, float yTouch)
{
double x = xTouch - (getMeasuredWidth() / 2d);
double y = yTouch - (getMeasuredWidth() / 2d);
return (float) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);
}
private VelocityTracker velocityTracker;//速度监测
private float velocity;//当前滑动速度
private float a = 1000000;//加速度
boolean continueScroll = false;
Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
requestLayout();
return false;
}
});
private void continueScroll() {
new Thread(new Runnable() {
@Override
public void run() {
float velocityAbs = 0;//速度绝对值
if (velocity > 0 && continueScroll) {
velocity -= 300;
mStartAngle += 20;
velocityAbs = velocity;
} else if (velocity < 0 && continueScroll) {
velocity += 300;
mStartAngle -= 20;
velocityAbs = velocity;
}
handler.sendEmptyMessage(0);
if (continueScroll && Math.abs(velocityAbs) > 300) {
post(this);
} else {
continueScroll = false;
}
}
}).start();
}
}
网友评论