最近項(xiàng)目有新需求初厚,要求一個(gè)房間內(nèi)有最多六個(gè)人同時(shí)在線养筒,房間人數(shù)從 0 到 6 個(gè)變化有不同的動(dòng)畫效果,而且自己的視圖永遠(yuǎn)在右上角猪勇,效果如下圖
![room](http://omu5krm39.bkt.clouddn.com/17-10-13/22681671.jpg?imageView2/0/q/75%7Cwatermark/2/text/d2FuZ2t1bmxpbi5kYXRl/font/Y29uc29sYXM=/fontsize/480/fill/IzIzNzhFMg==/dissolve/71/gravity/SouthEast/dx/10/dy/10%7Cimageslim)
剛以看到這個(gè)需求動(dòng)畫的時(shí)候,覺(jué)得很麻煩颠蕴,沒(méi)法做呀泣刹,當(dāng)時(shí)在想,這個(gè)需要知道不同人數(shù)所對(duì)應(yīng)的坐標(biāo)點(diǎn)犀被,在 join 的時(shí)候椅您,動(dòng)態(tài)計(jì)算一下將要加入的 view 的坐標(biāo)
當(dāng)時(shí)也確實(shí)是這么做的,在 join 的代碼寫的差不多了寡键,開始寫 leave 相關(guān)的代碼掀泳,發(fā)現(xiàn) leave 很麻煩,因?yàn)椴淮_定是哪一個(gè)位置的 view 要離開,所以目標(biāo)狀態(tài)也不確定
于是決定換個(gè)思路重新寫开伏,之前的方案行不通是因?yàn)橐磺卸际莿?dòng)態(tài)計(jì)算的膀跌,在 leave 的時(shí)候,要離開的 view 不確定固灵,導(dǎo)致目標(biāo)狀態(tài)也不確定捅伤,所以導(dǎo)致 leave 的代碼沒(méi)法寫,最后想到一個(gè)比較好的方案
就是在 RoomLayout 初始化完成后巫玻,就確定下來(lái)一個(gè)布局模型集合丛忆,集合里固定了 0 - 6 個(gè) view 所對(duì)應(yīng)的所有坐標(biāo),這樣在 join 和 leave 的時(shí)候仍秤,只需要從當(dāng)前的 view 位置向一個(gè)確定的位置變化即可
多說(shuō)無(wú)益熄诡,開始擼代碼,按照自定義 Layout 的步驟開始寫
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
在測(cè)量階段诗力,不需要做什么特殊處理凰浮,只需要測(cè)量一下子 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 與上一次存儲(chǔ)的不一樣,那么就重新確定坐標(biāo)
if (mBounds.isEmpty() || !mBounds.equals(mCompare)) {
mBounds.set(l, t, r, b);
prepareLayoutModels();
}
// 根據(jù)當(dāng)前個(gè)數(shù)選定 布局模型 并對(duì) INFLATE 布局
selectLayoutModel();
}
在布局這里要確定下來(lái)不同 view 個(gè)數(shù)對(duì)應(yīng)的每個(gè) view 的位置
/**
* 布局模型苇本,用來(lái)存儲(chǔ)不同子 view 的個(gè)數(shù)對(duì)應(yīng)的坐標(biāo)點(diǎn)
*/
private static class LayoutModel {
List<Rect> bounds = new LinkedList<>();
}
/**
* 準(zhǔn)備 布局模型
*/
private void prepareLayoutModels() {
// 反向布局袜茧,最后一個(gè) view 永遠(yuǎn)是自己
// 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 個(gè)數(shù)的初始化
// 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
// 把每個(gè)模型存儲(chǔ)在 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);
}
這里規(guī)定最后一個(gè) view 是自己的 view,因?yàn)樵诜块g內(nèi)只有兩個(gè)人的時(shí)候瓣窄,也就是自己和另一個(gè)人笛厦,自己的 view 在右上角,第二個(gè)人的 view 鋪滿父布局俺夕,所以如果不反過(guò)來(lái)裳凸,就是導(dǎo)致自己的 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 里面存儲(chǔ)的是最終要展示的 view 坐標(biāo)
Rect end = layoutModel.bounds.get(i);
ViewPropertyHolder holder = getHolder(child);
holder.end.set(end);
// 對(duì) INFLATE 狀態(tài)的 view 布局劝贸,然后設(shè)置為 NORMAL 狀態(tài)
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) {
// 對(duì)于 add 進(jìn)來(lái)的 view 它會(huì)從不同的地方進(jìn)來(lái)姨谷,所以要先布局在預(yù)定位置
Rect start = holder.start;
child.layout(start.left, start.top, start.right, start.bottom);
}
}
}
/**
* 獲取存儲(chǔ)在 View 中的相關(guān)屬性
*/
private ViewPropertyHolder getHolder(View child) {
// HOLDER 是一個(gè)定義在 ids.xml 中的一個(gè) id
ViewPropertyHolder holder = (ViewPropertyHolder) child.getTag(HOLDER);
if (holder == null) {
holder = new ViewPropertyHolder();
child.setTag(HOLDER, holder);
}
return holder;
}
// 存儲(chǔ) view 的屬性的類
private static class ViewPropertyHolder {
static final int ADD = 1; // 待添加
static final int REMOVE = 2; // 待移除
static final int NORMAL = 3; // 正常狀態(tài)
static final int INFLATE = 4; // 新添加并且不執(zhí)行動(dòng)畫
int state = INFLATE;
// 開始坐標(biāo)
Rect start = new Rect();
// 結(jié)束坐標(biāo)
Rect end = new Rect();
}
對(duì)子 view 布局相關(guān)的東西就寫完了,接下來(lái)是動(dòng)畫部分映九,動(dòng)畫我使用的是不停的 layout 子 view 來(lái)實(shí)現(xiàn)的
/**
* 加入一個(gè) view
*
* @param view view
* @param needAnim 是否需要?jiǎng)赢? */
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);
}
}
/**
* 移除 一個(gè) 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);
}
}
上面的是加入和離開的代碼菠秒,需要先判斷是否正在動(dòng)畫,如果在動(dòng)畫氯迂,那么把目標(biāo)加入一個(gè) list 中,以備后用
private void handleAddAndPrepareAnim(View toAdd) {
prepareViewStart(toAdd);
addView(toAdd, 0);
selectLayoutModel();
startAnimate();
}
private void handleRemoveAndPrepareAnim(View toRemove) {
prepareViewStart(null);
removeView(toRemove);
selectLayoutModel();
startAnimate();
}
/**
* 準(zhǔn)備當(dāng)前 view 的坐標(biāo)點(diǎn)
*/
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 進(jìn)來(lái)的 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;
}
}
接下來(lái)就開始動(dòng)畫了
private void startAnimate() {
ViewCompat.postOnAnimation(this, new Runnable() {
@Override
public void run() {
animatChild();
}
});
}
private void animatChild() {
if (!mIsAttached || mIsAnimating) {
return;
}
int N = getChildCount();
// 動(dòng)畫集合
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();
}
}
開始動(dòng)畫的代碼言缤,要先確定哪些 view 位置需要變化嚼蚀,然后生成一個(gè) ValueAnimator , 然后把所有的 ValueAnimator 一起開始動(dòng)畫
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)) { // 位置沒(méi)有變化
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;
}
生成一個(gè) PropertyValuesHolder 數(shù)組,指定兩個(gè)坐標(biāo)的 start 和 end 數(shù)值
下面是自定義的 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;
// 判斷后續(xù)是否有繼續(xù)開始動(dòng)畫的 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;
}
}
還有一些重寫的函數(shù)
@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);
}
到這里管挟,所有的代碼基本都寫完了轿曙,剩下一些變量聲明什么的沒(méi)有附上來(lái)
最后,本人才疏學(xué)淺,實(shí)現(xiàn)的可能不夠完美导帝,有任何意見或建議歡迎交流學(xué)習(xí)