最近项目有新需求,要求一个房间内有最多六个人同时在线,房间人数从 0 到 6 个变化有不同的动画效果,而且自己的视图永远在右上角,效果如下图
刚以看到这个需求动画的时候,觉得很麻烦,没法做呀,当时在想,这个需要知道不同人数所对应的坐标点,在 join 的时候,动态计算一下将要加入的 view 的坐标
当时也确实是这么做的,在 join 的代码写的差不多了,开始写 leave 相关的代码,发现 leave 很麻烦,因为不确定是哪一个位置的 view 要离开,所以目标状态也不确定
于是决定换个思路重新写,之前的方案行不通是因为一切都是动态计算的,在 leave 的时候,要离开的 view 不确定,导致目标状态也不确定,所以导致 leave 的代码没法写,最后想到一个比较好的方案
就是在 RoomLayout 初始化完成后,就确定下来一个布局模型集合,集合里固定了 0 - 6 个 view 所对应的所有坐标,这样在 join 和 leave 的时候,只需要从当前的 view 位置向一个确定的位置变化即可
多说无益,开始撸代码,按照自定义 Layout 的步骤开始写
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
在测量阶段,不需要做什么特殊处理,只需要测量一下子 View 即可
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
halfW = getWidth() / 2;
halfH = getHeight() / 2;
thirdH = getHeight() / 3;
mCompare.set(l, t, r, b);
// 如果本次的 layout 与上一次存储的不一样,那么就重新确定坐标
if (mBounds.isEmpty() || !mBounds.equals(mCompare)) {
mBounds.set(l, t, r, b);
prepareLayoutModels();
}
// 根据当前个数选定 布局模型 并对 INFLATE 布局
selectLayoutModel();
}
在布局这里要确定下来不同 view 个数对应的每个 view 的位置
/**
* 布局模型,用来存储不同子 view 的个数对应的坐标点
*/
private static class LayoutModel {
List<Rect> bounds = new LinkedList<>();
}
/**
* 准备 布局模型
*/
private void prepareLayoutModels() {
// 反向布局,最后一个 view 永远是自己
// 1
LayoutModel model1 = new LayoutModel();
model1.bounds.add(new Rect(0, 0, getWidth(), getHeight()));
// 2
LayoutModel model2 = new LayoutModel();
model2.bounds.add(new Rect(0, 0, getWidth(), getHeight())); // 0
int left = getWidth() / 16 * 9;
int bottom = (getWidth() - left) / 3 * 4;
model2.bounds.add(new Rect(left, 0, getWidth(), bottom)); // 1 mine
// ... 中间还有一些其他 view 个数的初始化
// 6
LayoutModel model6 = new LayoutModel();
model6.bounds.add(new Rect(halfW, thirdH * 2, getWidth(), getHeight())); // 0
model6.bounds.add(new Rect(0, thirdH * 2, halfW, getHeight())); // 1
model6.bounds.add(new Rect(halfW, thirdH, getWidth(), thirdH * 2)); // 2
model6.bounds.add(new Rect(0, thirdH, halfW, thirdH * 2)); // 3
model6.bounds.add(new Rect(0, 0, halfW, thirdH)); // 4
model6.bounds.add(new Rect(halfW, 0, getWidth(), thirdH)); // 5 mine
// 把每个模型存储在 map 中
mLayoutmodels.put(0, model1);
mLayoutmodels.put(1, model2);
mLayoutmodels.put(2, model3);
mLayoutmodels.put(3, model4);
mLayoutmodels.put(4, model5);
mLayoutmodels.put(5, model6);
}
这里规定最后一个 view 是自己的 view,因为在房间内只有两个人的时候,也就是自己和另一个人,自己的 view 在右上角,第二个人的 view 铺满父布局,所以如果不反过来,就是导致自己的 view 被铺满的 view 盖住
初始化完布局模型后,开始布局
// 选定 布局模型
private void selectLayoutModel() {
int N = getChildCount();
if (N == 0 || N > mLayoutmodels.size()) {
return;
}
LayoutModel layoutModel = mLayoutmodels.get(N - 1);
for (int i = 0; i < N; ++i) {
View child = getChildAt(i);
// layoutModel 里面存储的是最终要展示的 view 坐标
Rect end = layoutModel.bounds.get(i);
ViewPropertyHolder holder = getHolder(child);
holder.end.set(end);
// 对 INFLATE 状态的 view 布局,然后设置为 NORMAL 状态
if (holder.state == ViewPropertyHolder.INFLATE) {
holder.state = ViewPropertyHolder.NORMAL;
holder.start.set(end);
child.layout(end.left, end.top, end.right, end.bottom);
} else if (holder.state == ViewPropertyHolder.ADD) {
// 对于 add 进来的 view 它会从不同的地方进来,所以要先布局在预定位置
Rect start = holder.start;
child.layout(start.left, start.top, start.right, start.bottom);
}
}
}
/**
* 获取存储在 View 中的相关属性
*/
private ViewPropertyHolder getHolder(View child) {
// HOLDER 是一个定义在 ids.xml 中的一个 id
ViewPropertyHolder holder = (ViewPropertyHolder) child.getTag(HOLDER);
if (holder == null) {
holder = new ViewPropertyHolder();
child.setTag(HOLDER, holder);
}
return holder;
}
// 存储 view 的属性的类
private static class ViewPropertyHolder {
static final int ADD = 1; // 待添加
static final int REMOVE = 2; // 待移除
static final int NORMAL = 3; // 正常状态
static final int INFLATE = 4; // 新添加并且不执行动画
int state = INFLATE;
// 开始坐标
Rect start = new Rect();
// 结束坐标
Rect end = new Rect();
}
对子 view 布局相关的东西就写完了,接下来是动画部分,动画我使用的是不停的 layout 子 view 来实现的
/**
* 加入一个 view
*
* @param view view
* @param needAnim 是否需要动画
*/
public void join(View view, boolean needAnim) {
ViewPropertyHolder holder = getHolder(view);
if (needAnim && (mIsAnimating || mPendingAnim.size() > 0) && mIsAttached) {
holder.state = ViewPropertyHolder.ADD;
mPendingAnim.add(view);
} else if (needAnim && mIsAttached) {
holder.state = ViewPropertyHolder.ADD;
handleAddAndPrepareAnim(view);
} else {
holder.state = ViewPropertyHolder.INFLATE;
addView(view, 0);
}
}
/**
* 移除 一个 view
*
* @param view view
*/
public void leave(View view) {
ViewPropertyHolder holder = getHolder(view);
if (mIsAnimating || mPendingAnim.size() > 0) {
holder.state = ViewPropertyHolder.REMOVE;
mPendingAnim.add(view);
} else {
holder.state = ViewPropertyHolder.REMOVE;
handleRemoveAndPrepareAnim(view);
}
}
上面的是加入和离开的代码,需要先判断是否正在动画,如果在动画,那么把目标加入一个 list 中,以备后用
private void handleAddAndPrepareAnim(View toAdd) {
prepareViewStart(toAdd);
addView(toAdd, 0);
selectLayoutModel();
startAnimate();
}
private void handleRemoveAndPrepareAnim(View toRemove) {
prepareViewStart(null);
removeView(toRemove);
selectLayoutModel();
startAnimate();
}
/**
* 准备当前 view 的坐标点
*/
private void prepareViewStart(View add) {
int N = getChildCount();
for (int i = 0; i < N; ++i) {
View child = getChildAt(i);
ViewPropertyHolder holder = getHolder(child);
holder.start.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
}
if (add == null) {
return;
}
// 确定 新 add 进来的 view 的位置
ViewPropertyHolder holder = getHolder(add);
switch (N) {
case 1:
holder.start.set(-getWidth(), 0, 0, getHeight());
break;
case 2:
holder.start.set(0, getHeight(), getWidth(), getHeight() + halfH);
break;
case 3:
holder.start.set(getWidth(), halfH, getWidth() + halfW, getHeight());
break;
case 4:
holder.start.set(0, getHeight(), halfW, getHeight() + thirdH);
break;
case 5:
holder.start.set(halfW, getHeight(), getWidth(), getHeight() + thirdH);
break;
}
}
接下来就开始动画了
private void startAnimate() {
ViewCompat.postOnAnimation(this, new Runnable() {
@Override
public void run() {
animatChild();
}
});
}
private void animatChild() {
if (!mIsAttached || mIsAnimating) {
return;
}
int N = getChildCount();
// 动画集合
List<Animator> animators = new ArrayList<>();
for (int i = 0; i < N; ++i) {
View view = getChildAt(i);
ViewPropertyHolder holder = getHolder(view);
// 获取需要更新位置的属性值
PropertyValuesHolder[] childValuesHolder = getChildValuesHolder(view);
if (childValuesHolder != null) {
ViewValueAnimator animator = ViewValueAnimator.ofPropertyValuesHolder(childValuesHolder);
animator.holder = holder;
animator.target = view;
animator.addUpdateListener(new AnimatorUpdateListener());
animator.addListener(new AnimatorAdapter());
animators.add(animator);
} else {
Rect bound = holder.end;
view.layout(bound.left, bound.top, bound.right, bound.bottom);
}
}
if (animators.size() > 0) {
mIsAnimating = true;
mAnimatorSet.playTogether(animators);
mAnimatorSet.setDuration(ANIM_DURATION);
mAnimatorSet.setInterpolator(mInterpolator);
if (mGlobalAnimListener == null) {
mGlobalAnimListener = new GlobalAnimUpdateListener();
}
mAnimatorSet.addListener(mGlobalAnimListener);
mAnimatorSet.start();
}
}
开始动画的代码,要先确定哪些 view 位置需要变化,然后生成一个 ValueAnimator , 然后把所有的 ValueAnimator 一起开始动画
private static final String LEFT = "left";
private static final String TOP = "top";
private static final String RIGHT = "right";
private static final String BOTTOM = "bottom";
private PropertyValuesHolder[] getChildValuesHolder(View child) {
ViewPropertyHolder holder = getHolder(child);
if (holder.start.equals(holder.end)) { // 位置没有变化
return null;
}
PropertyValuesHolder[] holders = new PropertyValuesHolder[4];
holders[0] = PropertyValuesHolder.ofInt(LEFT, holder.start.left, holder.end.left);
holders[1] = PropertyValuesHolder.ofInt(TOP, holder.start.top, holder.end.top);
holders[2] = PropertyValuesHolder.ofInt(RIGHT, holder.start.right, holder.end.right);
holders[3] = PropertyValuesHolder.ofInt(BOTTOM, holder.start.bottom, holder.end.bottom);
return holders;
}
生成一个 PropertyValuesHolder 数组,指定两个坐标的 start 和 end 数值
下面是自定义的 ValueAnimator 和一些 Listeners
private static class AnimatorAdapter extends AnimatorListenerAdapter {
@Override
public void onAnimationEnd(Animator animation) {
animation.removeAllListeners();
ViewValueAnimator anim = (ViewValueAnimator) animation;
anim.removeAllUpdateListeners();
if (anim.holder != null) {
anim.holder.state = ViewPropertyHolder.NORMAL;
}
anim.holder = null;
anim.target = null;
}
@Override
public void onAnimationCancel(android.animation.Animator animation) {
onAnimationEnd(animation);
}
}
private class GlobalAnimUpdateListener extends AnimatorListenerAdapter {
@Override
public void onAnimationStart(Animator animation) {
mIsAnimating = true;
}
@Override
public void onAnimationEnd(Animator animation) {
animation.removeAllListeners();
mIsAnimating = false;
// 判断后续是否有继续开始动画的 view
if (mPendingAnim.size() > 0) {
View view = mPendingAnim.remove(0);
ViewPropertyHolder holder = getHolder(view);
if (holder.state == ViewPropertyHolder.ADD) {
handleAddAndPrepareAnim(view);
} else if (holder.state == ViewPropertyHolder.REMOVE) {
handleRemoveAndPrepareAnim(view);
}
}
}
@Override
public void onAnimationCancel(Animator animation) {
onAnimationEnd(animation);
}
}
private static class AnimatorUpdateListener implements ValueAnimator.AnimatorUpdateListener {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
ViewValueAnimator anim = (ViewValueAnimator) animation;
int l = (int) anim.getAnimatedValue(LEFT);
int t = (int) anim.getAnimatedValue(TOP);
int r = (int) anim.getAnimatedValue(RIGHT);
int b = (int) anim.getAnimatedValue(BOTTOM);
// 不停的布局子 view
anim.target.layout(l, t, r, b);
}
}
/**
* 持有 view 和 holder 的 ValueAnimator
*/
private static class ViewValueAnimator extends ValueAnimator {
View target;
ViewPropertyHolder holder;
public static ViewValueAnimator ofPropertyValuesHolder(PropertyValuesHolder... values) {
ViewValueAnimator anim = new ViewValueAnimator();
anim.setValues(values);
return anim;
}
}
还有一些重写的函数
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mIsAttached = true;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mIsAttached = false;
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
到这里,所有的代码基本都写完了,剩下一些变量声明什么的没有附上来
最后,本人才疏学浅,实现的可能不够完美,有任何意见或建议欢迎交流学习