Android 自定义viewAndroid开发经验谈Android开发

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

2017-10-13  本文已影响109人  为何是Hex的昵称

最近项目有新需求,要求一个房间内有最多六个人同时在线,房间人数从 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);
}

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

上一篇下一篇

猜你喜欢

热点阅读