自定義一個(gè) 6 人的房間布局

最近項(xiàng)目有新需求初厚,要求一個(gè)房間內(nèi)有最多六個(gè)人同時(shí)在線养筒,房間人數(shù)從 0 到 6 個(gè)變化有不同的動(dòng)畫效果,而且自己的視圖永遠(yuǎn)在右上角猪勇,效果如下圖


room
room

剛以看到這個(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í)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末守谓,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子您单,更是在濱河造成了極大的恐慌斋荞,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件虐秦,死亡現(xiàn)場(chǎng)離奇詭異平酿,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)悦陋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門蜈彼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人俺驶,你說(shuō)我怎么就攤上這事幸逆。” “怎么了暮现?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵还绘,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我送矩,道長(zhǎng)蚕甥,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任栋荸,我火速辦了婚禮菇怀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘晌块。我一直安慰自己爱沟,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布匆背。 她就那樣靜靜地躺著呼伸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪钝尸。 梳的紋絲不亂的頭發(fā)上括享,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音珍促,去河邊找鬼铃辖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛猪叙,可吹牛的內(nèi)容都是我干的娇斩。 我是一名探鬼主播仁卷,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼犬第!你這毒婦竟也來(lái)了锦积?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤歉嗓,失蹤者是張志新(化名)和其女友劉穎丰介,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體遥椿,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡基矮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了冠场。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片家浇。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖碴裙,靈堂內(nèi)的尸體忽然破棺而出钢悲,到底是詐尸還是另有隱情,我是刑警寧澤舔株,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布莺琳,位于F島的核電站,受9級(jí)特大地震影響载慈,放射性物質(zhì)發(fā)生泄漏惭等。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一办铡、第九天 我趴在偏房一處隱蔽的房頂上張望辞做。 院中可真熱鬧,春花似錦寡具、人聲如沸秤茅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)框喳。三九已至,卻和暖如春厦坛,著一層夾襖步出監(jiān)牢的瞬間五垮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工杜秸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拼余,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓亩歹,卻偏偏與公主長(zhǎng)得像匙监,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子小作,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容