自定义ViewGroup排版家谱树成员

  1. 完整代码
/**
 * Created by wanggang on 2017/4/20.
 * 家族树
 */

public class FamilyTreeView extends ViewGroup {

    /**
     * 界面可滚动的左边界
     */
    private int leftBorder;

    /**
     * 界面可滚动的上边界
     */
    private int topBorder;

    /**
     * 界面可滚动的右边界
     */
    private int rightBorder;

    /**
     * 界面可滚动的下边界
     */
    private int bottomBorder;

    FamilyTreeAdapter familyTreeAdapter;
    private Paint mPaint;

    int colSpace;
    int lineSpace;
    int radius; // 弧形的半径
    int itemWidth;
    int itemHeight;

    List<Ponit> ponits; // 记录所有有轨迹的点
    ValueAnimator mAnimator;
    float mPercent;

    Stack<PersonView> personViews; // 保存PersonView

    boolean canLayout; // 是否正在添加View
    boolean canMeasure; // 是否正在添加View

    int layoutIndex;

    public FamilyTreeAdapter getFamilyTreeAdapter() {
        return familyTreeAdapter;
    }

    public void setFamilyTreeAdapter(FamilyTreeAdapter familyTreeAdapter) {
        this.familyTreeAdapter = familyTreeAdapter;
    }

    public FamilyTreeView(Context context) {
        super(context);
        init();
    }

    public FamilyTreeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public FamilyTreeView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {

        personViews = new Stack<>();

        colSpace = MeasureUtils.dip2px(getContext(), 24);
        lineSpace = MeasureUtils.dip2px(getContext(), 32);
        radius = MeasureUtils.dip2px(getContext(), 6);
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(MeasureUtils.dip2px(getContext(), 1));
        mPaint.setColor(Color.parseColor("#999999"));
        mPaint.setAntiAlias(true);
        setWillNotDraw(false);

        mAnimator = ValueAnimator.ofFloat(0, 1);
        mAnimator.setDuration(1000);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mPercent = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (canMeasure) {
            canMeasure = false;
            for (int i = 0; i < getChildCount(); i++) {
                PersonView childView = (PersonView) getChildAt(i);
                measureChild(childView, widthMeasureSpec, heightMeasureSpec);
                measureView(childView);
            }
//            setMeasuredDimension(rightBorder - leftBorder, bottomBorder - topBorder);
        }
    }



    private void measureView(PersonView personView) {
        PersonEntity personEntity = personView.getPersonEntity();
        PersonData personData = personEntity;
        itemWidth = personView.getMeasuredWidth();
        itemHeight = personView.getMeasuredHeight();
        changeMeasurePoint(personData.getCenterPoint());
        int centerX = personData.getCenterPoint().x;
        int centerY = personData.getCenterPoint().y;
        if (centerX - itemWidth < leftBorder) {
            leftBorder = centerX - itemWidth;
        }
        if (centerX + itemWidth > rightBorder) {
            rightBorder = centerX + itemWidth;
        }
        if (centerY - itemHeight < topBorder) {
            topBorder = centerY - itemHeight;
        }
        if (centerY + itemHeight > bottomBorder) {
            bottomBorder = centerY + itemHeight;
        }
    }

    public void changeMeasurePoint(Ponit ponit) {
        ponit.x = ponit.coordinateX * (itemWidth + colSpace);
        ponit.y = - ponit.coordinateY * (itemHeight + lineSpace);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (canLayout) {
            canLayout = false;

            for (int i = 0; i < getChildCount(); i++) {
                PersonView personView = (PersonView) getChildAt(i);
                layoutView(personView);

                if (i == 0) {
                    personView.setSelected(true);
                    personView.setTitleColor(R.color.white);
                } else {
                    personView.setSelected(false);
                    personView.setTitleColor(R.color.black);
                }
            }
            mAnimator.start();
        }
    }

    /**
     * 通过中心点位置放置view
     */
    private void layoutView(PersonView personView) {
        PersonEntity personEntity = personView.getPersonEntity();
        PersonData personData = personEntity;
        itemWidth = personView.getMeasuredWidth();
        itemHeight = personView.getMeasuredHeight();
        changePoint(personData.getCenterPoint());
        addDrawPoint(personData.getCenterPoint());
        int centerX = personData.getCenterPoint().x;
        int centerY = personData.getCenterPoint().y;
        personView.layout(centerX - itemWidth / 2, centerY - itemHeight / 2,
                centerX + itemWidth / 2, centerY + itemHeight / 2);
        personView.setImage(personData.gender, personData.avatar);
        personView.setTitle(personData.name);
    }

    // 坐标系坐标转换成屏幕坐标
    public void changePoint(Ponit ponit) {
        ponit.x = ponit.x - leftBorder;
        ponit.y = ponit.y - topBorder;
    }

    private void addDrawPoint(Ponit ponit) {
        if (ponits == null) {
            ponits = new LinkedList<>();
        }
        ponits.add(ponit);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (ponits != null) {
            for (int i = 0; i < ponits.size(); i++) {
                Ponit ponit = ponits.get(i);
                if (ponit.connecPonit != null) {
                    ponit.drawConnection(itemWidth, itemHeight, lineSpace, colSpace, radius);
                    canvas.drawPath(ponit.getSegment(mPercent), mPaint);
                }
            }
        }
    }

    public void refreshUI() {
        for (int i = 0; i < getChildCount(); i++) {
            if (getChildAt(i) instanceof PersonView)
                personViews.add((PersonView) getChildAt(i));
        }
        removeAllViews();
        if (ponits != null) {
            ponits.clear();
        }
        int childSize = familyTreeAdapter.dataList.size();

        for (int i = 0; i < childSize; i++) {
            PersonView personView = familyTreeAdapter.getPersonView(getPersonView(), this, familyTreeAdapter.dataList.get(i));
//            addViewInLayout(personView, i, new LayoutParams(DensityUtil.dip2px(getContext(), 60), DensityUtil.dip2px(getContext(), 80)));
            addView(personView);
        }
        canLayout = true;
        canMeasure = true;
        requestLayout();
    }

    private PersonView getPersonView() {
        if (personViews != null && personViews.size() > 0)
            return personViews.pop();
        return null;
    }

    @Override
    public void addView(View child) {
        super.addView(child);
    }

    @Override
    protected boolean addViewInLayout(View child, int index, LayoutParams params, boolean preventRequestLayout) {
        return super.addViewInLayout(child, index, params, preventRequestLayout);
    }

    public int getLeftBorder() {
        return leftBorder;
    }

    public int getTopBorder() {
        return topBorder;
    }

    public int getRightBorder() {
        return rightBorder;
    }

    public int getBottomBorder() {
        return bottomBorder;
    }
}
  1. 添加成员view
public void refreshUI() {
        for (int i = 0; i < getChildCount(); i++) {
            if (getChildAt(i) instanceof PersonView)
                personViews.add((PersonView) getChildAt(i));
        }
        removeAllViews();
        if (ponits != null) {
            ponits.clear();
        }
        int childSize = familyTreeAdapter.dataList.size();

        for (int i = 0; i < childSize; i++) {
            PersonView personView = familyTreeAdapter.getPersonView(getPersonView(), this, familyTreeAdapter.dataList.get(i));
//            addViewInLayout(personView, i, new LayoutParams(DensityUtil.dip2px(getContext(), 60), DensityUtil.dip2px(getContext(), 80)));
            addView(personView);
        }
        canLayout = true;
        canMeasure = true;
        requestLayout();
    }

这段代码主要工作就是通过adapter中列表的数据,往viewgroup中添加成员view。这里使用了canLayout和canMeasure来控制viewgroup的测量和layout次数。之前使用了addViewInLayout来添加view,但是当viewgroup被缩放的时候,所有添加的子view都不见了,所以使用addView加控制变量来操作。

  1. 测量以及坐标转换
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    if (canMeasure) {
        canMeasure = false;
        for (int i = 0; i < getChildCount(); i++) {
            PersonView childView = (PersonView) getChildAt(i);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
            measureView(childView);
        }
    }
}

private void measureView(PersonView personView) {
    PersonEntity personEntity = personView.getPersonEntity();
    PersonData personData = personEntity;
    itemWidth = personView.getMeasuredWidth();
    itemHeight = personView.getMeasuredHeight();
    changeMeasurePoint(personData.getCenterPoint());
    int centerX = personData.getCenterPoint().x;
    int centerY = personData.getCenterPoint().y;
    if (centerX - itemWidth < leftBorder) {
        leftBorder = centerX - itemWidth;
    }
    if (centerX + itemWidth > rightBorder) {
        rightBorder = centerX + itemWidth;
    }
    if (centerY - itemHeight < topBorder) {
        topBorder = centerY - itemHeight;
    }
    if (centerY + itemHeight > bottomBorder) {
        bottomBorder = centerY + itemHeight;
    }
}

public void changeMeasurePoint(Ponit ponit) {
    ponit.x = ponit.coordinateX * (itemWidth + colSpace);
    ponit.y = - ponit.coordinateY * (itemHeight + lineSpace);
}

这里的主要功能是将每个成员节点的坐标系坐标转换成屏幕的物理坐标,其转换规则就是changeMeasurePoint方法。转换成功之后再计算当前viewgroup的边界,也就是leftBorder、rightBorder、topBorder、bottomBorder的重新赋值。

  1. 成员view的排版布局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (canLayout) {
        canLayout = false;
        for (int i = 0; i < getChildCount(); i++) {
            PersonView personView = (PersonView) getChildAt(i);
            layoutView(personView);
        }
    }
}

/**
 * 通过中心点位置放置view
 */
private void layoutView(PersonView personView) {
    PersonEntity personEntity = personView.getPersonEntity();
    PersonData personData = personEntity;
    itemWidth = personView.getMeasuredWidth();
    itemHeight = personView.getMeasuredHeight();
    changePoint(personData.getCenterPoint());
    addDrawPoint(personData.getCenterPoint());
    int centerX = personData.getCenterPoint().x;
    int centerY = personData.getCenterPoint().y;
    personView.layout(centerX - itemWidth / 2, centerY - itemHeight / 2,
            centerX + itemWidth / 2, centerY + itemHeight / 2);
    personView.setImage(personData.gender, personData.avatar);
    personView.setTitle(personData.name);
}

经过测量过程的坐标计算之后,onLayout就简单很多了,根据算好的坐标,将对应的成员view布局到viewgroup中。

  1. 最后还有绘制连接线的过程,有了坐标,这块就是根据canvas的api画线就行,不多做解释了
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (ponits != null) {
        for (int i = 0; i < ponits.size(); i++) {
            Ponit ponit = ponits.get(i);
            if (ponit.connecPonit != null) {
                ponit.drawConnection(itemWidth, itemHeight, lineSpace, colSpace, radius);
                canvas.drawPath(ponit.getSegment(mPercent), mPaint);
            }
        }
    }
}

至此,家族树就布局完成了,最后看一下最终效果


SVID_familytree.gif
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容