自定义一个 6 人的房间布局

作者: 为何是Hex的昵称 | 来源:发表于2017-10-13 14:48 被阅读109次

    最近项目有新需求,要求一个房间内有最多六个人同时在线,房间人数从 0 到 6 个变化有不同的动画效果,而且自己的视图永远在右上角,效果如下图


    roomroom

    刚以看到这个需求动画的时候,觉得很麻烦,没法做呀,当时在想,这个需要知道不同人数所对应的坐标点,在 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);
    }
    

    到这里,所有的代码基本都写完了,剩下一些变量声明什么的没有附上来
    最后,本人才疏学浅,实现的可能不够完美,有任何意见或建议欢迎交流学习

    相关文章

      网友评论

        本文标题:自定义一个 6 人的房间布局

        本文链接:https://www.haomeiwen.com/subject/mqdzyxtx.html