/**
- 用矩阵的方法,来定义一个点是否位于一个区域内
*/
private boolean isPointInView(View view, float[] points) {
// 像ViewGroup那样,先对齐一下Left和Top
points[0] -= view.getLeft();
points[1] -= view.getTop();
// 获取View所对应的矩阵
Matrix matrix = view.getMatrix();
// 如果矩阵有应用过变换
if (!matrix.isIdentity()) {
// 反转矩阵
matrix.invert(matrix);
// 映射坐标点
matrix.mapPoints(points);
}
//判断坐标点是否在view范围内
return points[0] >= 0 && points[1] >= 0 && points[0] < view.getWidth() && points[1] < view.getHeight();
}
学会了,学会了(抱拳)
关于层级的交换,其实就是把两个View从ViewGroup取出来,然后交换顺序,又放回去。
对应的是detachViewFromParent()
和attachViewToParent()
方法
我们只需要知道,想要交换的两个View在ViewGroup的层级顺序是什么就行了。
其中 indexofChild(View v)
可以知道它所处的层级,它越大,说明它越靠上,最大值就是 childCount-1,这个时候它是在最上层的 。
/**
- 使用attachViewToParent和detachAllViewsFromParent来交换两个Index的层级
*/
public void exchangeOrder(int fromIndex, int toIndex) {
if (fromIndex == toIndex) {
return;
}
View fromChild = getChildAt(fromIndex);
View toChild = getChildAt(toIndex);
//分开这些View
detachViewFromParent(fromChild);
detachViewFromParent(toChild);
//交换两个View,本质就是交换它们的index
attachViewToParent(fromChild, toIndex > getChildCount() ? getChildCount() : toIndex, fromChild.getLayoutParams());
attachViewToParent(toChild, fromIndex > getChildCount() ? getChildCount() : fromIndex, toChild.getLayoutParams());
invalidate();
}
LayoutParams是用来协助Viewgoup的,它可以给子View定义一些属性。而且也可以支持margin。
我们要给子View定义 :
-
从哪里来(from)
-
到哪里去(to)
-
透明度(alpha)
-
缩放值(scale)
所以我们要这样子重写:
/**
- 这里要自己写一个ViewGroup的LayoutParams来记录 scale、alpha、from、to
*/
class RikkaLayoutParams extends MarginLayoutParams {
float scale = 0f;
float alpha = 0f;
int from;
int to;
…(getter and setter)
public RikkaLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public RikkaLayoutParams(int width, int height) {
super(width, height);
}
public RikkaLayoutParams(LayoutParams source) {
super(source);
}
}
/**
- 要支持margin,所以要重写generate方法
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new RikkaLayoutParams(mContext, attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new RikkaLayoutParams§;
}
@Override
p
rotected LayoutParams generateDefaultLayoutParams() {
return new RikkaLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
ViewGroup的宽度要么是写死的值,要么是三个子View之和
高度要么是写死的值,要么是三个View里面,最大的那一个:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//先测量子View
measureChildren(widthMeasureSpec, heightMeasureSpec);
//因为这个时候已经测量完子View了,所以通过子View来计算整个View的宽高
int width = measureWidth(widthMeasureSpec);
int height = measureHeight(heightMeasureSpec);
//根据获取的宽高拿去用
setMeasuredDimension(width, height);
}
//整个View的宽度是三个子View的和
private int measureWidth(int widthMeasureSpec) {
int totalWidth = 0;
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
totalWidth = widthSize;
} else {
for (int i = 0; i < getChildCount(); i++) {
RikkaLayoutParams lp = (RikkaLayoutParams) getChildAt(i).getLayoutParams();
totalWidth += getChildAt(i).getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
}
}
return totalWidth;
}
//整个View的高 取三个View的最大值
private int measureHeight(int heightMeasureSpec) {
int maxHeight = 0;
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.EXACTLY) {
//如果是具体值就取具体值
maxHeight = heightSize;
} else {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
RikkaLayoutParams lp = (RikkaLayoutParams) child.getLayoutParams();
maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
}
}
return maxHeight;
}
测量完后就需要布局了。
我们要根据三条辅助线来确定。最左边的View的辅助线,应该以左边为准,右边的以右边为准,中间的以中间为准。
由于在滑动时,View的位置也是要变的,也要不断的走onLayout
方法,所以辅助线也是跟着变动的,它是跟着滑动百分比
来计算的。
/**
-
根据基准线去布局子View
-
基准线有四条,子View分别在这四条线上
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
int baseLineX = calBaseLine(i);
int baseLineY = getHeight() / 2;
//滑动的过程也是layout的过程,所以在layout的时候也要更改其透明度和缩放度
View child = getChildAt(i);
RikkaLayoutParams lp = (RikkaLayoutParams) child.getLayoutParams();
child.setScaleX(lp.getScale());
child.setScaleY(lp.getScale());
child.setAlpha(lp.getAlpha());
int left = baseLineX - child.getMeasuredWidth() / 2;
int top = baseLineY - child.getMeasuredHeight() / 2;
int right = left + child.getMeasuredWidth();
int bottom = top + child.getMeasuredHeight();
child.layout(left + lp.leftMargin + getPaddingLeft(),
top + lp.topMargin + getPaddingTop(),
right + lp.rightMargin + getPaddingRight(),
bottom + lp.bottomMargin + getPaddingBottom());
}
}
/**
- 根据offsetPercent来计算基线位置,子View是根据基线来布局的
*/
private int calBaseLine(int index) {
float baseline = 0;
//最左边的baseline
float baselineLeft = getWidth() / 4;
//最中间的baseline
float baselineCenter = getWidth() / 2;
//最右边的baseline
float baselineRight = getWidth() - baselineLeft;
RikkaLayoutParams lp = (RikkaLayoutParams) getChildAt(index).getLayoutParams();
//根据lp的from 和 to来确定基线位置
switch (lp.getFrom()) {
case 0:
if (lp.getTo() == 1) {
baseline = baselineLeft + (baselineRight - baselineLeft) * -offsetPercent;
} else if (lp.getTo() == 2) {
baseline = baselineLeft + (baselineCenter - baselineLeft) * offsetPercent;
} else {
baseline = baselineLeft;
}
break;
case 1:
if (lp.getTo() == 0) {
baseline = baselineRight - (baselineRight - baselineLeft) * offsetPercent;
} else if (lp.getTo() == 2) {
baseline = baselineRight + (baselineRight - baselineCenter) * offsetPercent;
} else {
baseline = baselineRight;
}
break;
case 2:
if (lp.getTo() == 1) {
baseline = baselineCenter + (baselineRight - baselineCenter) * offsetPercent;
} else if (lp.getTo() == 0) {
baseline = baselineCenter + (baselineCenter - baselineLeft) * offsetPercent;
} else {
baseline = baselineCenter;
}
break;
}
return (int) baseline;
}
我们需要在onInterceptTouchEvent
里判断一下我们是否需要使用到onTouchEvent
,所以我们需要时时刻刻的去获取点击的位置,并记录偏移量,来判断是否是滑动状态,如果是的话,我们需要处理子View的移动了。
/**
- 如果是滑动,则调用onTouchEvent,如果只是单击,可以切换View
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
isDraged = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mDownX = x;
mDownY = y;
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
//如果滑动超出规定的距离,则可以滑动View
int offsetX = (int) (x - mLastX);
int offsetY = (int) (y - mLastY);
if (Math.abs(offsetX) > MIN_SLOP_DISTANCE && Math.abs(offsetY) > MIN_SLOP_DISTANCE) {
mLastX = x;
mLastY = y;
isDraged = true;
}
case MotionEvent.ACTION_UP:
isDraged = false;
break;
}
return isDraged;
}
/**
- onTouchEvent就是确定是要滑动了,根据滑动距离,做子View的位移动画
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
//通过总位移量除以View长来得到百分比
int offsetX = (int) (x - mLastX);
totalOffsetX += offsetX;
moveItem();
break;
case MotionEvent.ACTION_UP:
isDraged = false;
break;
}
mLastX = x;
mLastY = y;
//能走到onTouchEvent就肯定是返回true的
return true;
}
而子View就是根据 总位移量totalOffsetX来计算百分比的:
/**
- 通过百分比的正负值来确定每个View要去到哪里、设置透明度和缩放、交换View的层级
*/
private void moveItem() {
offsetPercent = totalOffsetX / getWidth();
setViewFromAndTo();
changeViewLevel();
changeAlphaAndScale();
requestLayout();
}
/**
-
根据百分比的正负值,来设置View的from和to
-
如果是负则说明手指正在往左边滑动,则 0->1,1->2,2->0,反之亦然
*/
private void setViewFromAndTo() {
//如果滑动距离超出了屏幕的宽度,则超出的部分要更新
if (Math.abs(offsetPercent) >= 1) {
//在每次完整的滑完一次后,需要重置isReordered,不然当一次滑动很长距离时,会产生问题
isReordered = false;
for (int i = 0; i < getChildCount(); i++) {
RikkaLayoutParams lp = (RikkaLayoutParams) getChildAt(i).getLayoutParams();
lp.setFrom(lp.getTo());
}
totalOffsetX %= getWidth();
/**
- 通过百分比的正负值来确定每个View要去到哪里、设置透明度和缩放、交换View的层级
*/
private void moveItem() {
offsetPercent = totalOffsetX / getWidth();
setViewFromAndTo();
changeViewLevel();
changeAlphaAndScale();
requestLayout();
}
/**
-
根据百分比的正负值,来设置View的from和to
-
如果是负则说明手指正在往左边滑动,则 0->1,1->2,2->0,反之亦然
*/
private void setViewFromAndTo() {
//如果滑动距离超出了屏幕的宽度,则超出的部分要更新
if (Math.abs(offsetPercent) >= 1) {
//在每次完整的滑完一次后,需要重置isReordered,不然当一次滑动很长距离时,会产生问题
isReordered = false;
for (int i = 0; i < getChildCount(); i++) {
RikkaLayoutParams lp = (RikkaLayoutParams) getChildAt(i).getLayoutParams();
lp.setFrom(lp.getTo());
}
totalOffsetX %= getWidth();