Android自定義樹狀圖控件 GysoTreeView組織結(jié)構(gòu)樹形圖

Tree View; Mind map; Think map; tree map; 樹狀圖武鲁;思維導(dǎo)圖睁本;組織結(jié)構(gòu)圖掐禁;層次圖

文章目錄

[1 簡(jiǎn)介](# 簡(jiǎn)介)

[2 效果展示](# 效果展示)

[3 使用步驟](# 使用步驟)

[4 實(shí)現(xiàn)基本布局流程](# 實(shí)現(xiàn)基本布局流程)

[5 實(shí)現(xiàn)自由放縮及拖動(dòng)](# 實(shí)現(xiàn)自由放縮及拖動(dòng))

[6 實(shí)現(xiàn)添加刪除及節(jié)點(diǎn)動(dòng)畫](# 實(shí)現(xiàn)添加刪除及節(jié)點(diǎn)動(dòng)畫)

[7 實(shí)現(xiàn)樹狀圖的回歸適應(yīng)屏幕](# 實(shí)現(xiàn)樹狀圖的回歸適應(yīng)屏幕)

[8 實(shí)現(xiàn)拖到編輯樹狀圖結(jié)構(gòu)](# 實(shí)現(xiàn)拖到編輯樹狀圖結(jié)構(gòu))

[9 寫在最后](# 寫在最后)

簡(jiǎn)介

github連接: https://github.com/guaishouN/android-tree-view.git

目前沒發(fā)現(xiàn)比較好的Android樹狀圖開源控件脂倦,于是決定自己寫一個(gè)開源控件厂汗,對(duì)比了一下市面上關(guān)于思維導(dǎo)圖或者樹狀圖顯示(如xMind业崖,mind master等)的app野芒,本文開源框架并不遜色。實(shí)現(xiàn)這個(gè)樹狀圖過程中主要綜合應(yīng)用了很多自定義控件關(guān)鍵知識(shí)點(diǎn)双炕,比如自定義ViewGroup的步驟狞悲、觸摸事件的處理、動(dòng)畫使用妇斤、Scroller及慣性滑動(dòng)摇锋、ViewDragHelper的使用等等。主要實(shí)現(xiàn)了下面幾個(gè)功能點(diǎn)站超。

  • 絲滑的跟隨手指放縮荸恕,拖動(dòng),及慣性滑動(dòng)

  • 自動(dòng)動(dòng)畫回歸屏幕中心

  • 支持子節(jié)點(diǎn)復(fù)雜布局自定義死相,并且節(jié)點(diǎn)布局點(diǎn)擊事件與滑動(dòng)不沖突

  • 節(jié)點(diǎn)間的連接線自定義

  • 可刪除動(dòng)態(tài)節(jié)點(diǎn)

  • 可動(dòng)態(tài)添加節(jié)點(diǎn)

  • 支持拖動(dòng)調(diào)整節(jié)點(diǎn)關(guān)系

  • 增刪融求、移動(dòng)結(jié)構(gòu)添加動(dòng)畫效果

效果展示

基礎(chǔ)--連接線, 布局, 自定義節(jié)點(diǎn)View

new.jpg
fs.png

添加

add.gif

刪除

add.gif

拖動(dòng)節(jié)點(diǎn)編輯書樹狀圖結(jié)構(gòu)

add.gif

放縮拖動(dòng)不影響點(diǎn)擊

add.gif

放縮拖動(dòng)及適應(yīng)窗口

add.gif

使用步驟

下面說明中Animal類是僅僅用于舉例的bean

public class Animal {
    public int headId;
    public String name;
}

按照以下四個(gè)步驟使用該開源控件

1 通過繼承 TreeViewAdapter實(shí)現(xiàn)節(jié)點(diǎn)數(shù)據(jù)與節(jié)點(diǎn)視圖的綁定

public class AnimalTreeViewAdapter extends TreeViewAdapter<Animal> {
    private DashLine dashLine =  new DashLine(Color.parseColor("#F06292"),6);
    @Override
    public TreeViewHolder<Animal> onCreateViewHolder(@NonNull ViewGroup viewGroup, NodeModel<Animal> node) {
        //TODO in inflate item view
        NodeBaseLayoutBinding nodeBinding = NodeBaseLayoutBinding.inflate(LayoutInflater.from(viewGroup.getContext()),viewGroup,false);
        return new TreeViewHolder<>(nodeBinding.getRoot(),node);
    }

    @Override
    public void onBindViewHolder(@NonNull TreeViewHolder<Animal> holder) {
        //TODO get view and node from holder, and then control your item view
        View itemView = holder.getView();
        NodeModel<Animal> node = holder.getNode();
        ...
    }

    @Override
    public Baseline onDrawLine(DrawInfo drawInfo) {
        // TODO If you return an BaseLine, line will be draw by the return one instead of TreeViewLayoutManager's
        // if(...){
        //   ...
        //   return dashLine;
        // }
        return null;
    }
}

2 配置LayoutManager。主要設(shè)置布局風(fēng)格(向右展開或垂直向下展開)算撮、父節(jié)點(diǎn)與子節(jié)點(diǎn)的間隙生宛、子節(jié)點(diǎn)間的間隙、節(jié)點(diǎn)間的連線(已經(jīng)實(shí)現(xiàn)了直線肮柜、光滑曲線、虛線审洞、根狀線,也可通過BaseLine實(shí)現(xiàn)你自己的連線)

int space_50dp = 50;
int space_20dp = 20;
//choose a demo line or a customs line. StraightLine, PointedLine, DashLine, SmoothLine are available.
Baseline line =  new DashLine(Color.parseColor("#4DB6AC"),8);
//choose layoout manager. VerticalTreeLayoutManager,RightTreeLayoutManager are available.
TreeLayoutManager treeLayoutManager = new RightTreeLayoutManager(this,space_50dp,space_20dp,line);

3 把Adapter和LayoutManager設(shè)置到你的樹狀圖

...
treeView = findViewById(R.id.tree_view);   
TreeViewAdapter adapter = new AnimlTreeViewAdapter();
treeView.setAdapter(adapter);
treeView.setTreeLayoutManager(treeLayoutManager);
...

4 設(shè)置節(jié)點(diǎn)數(shù)據(jù)

//Create a TreeModel by using a root node.
NodeModel<Animal> node0 = new NodeModel<>(new Animal(R.drawable.ic_01,"root"));
TreeModel<Animal> treeModel = new TreeModel<>(root);

//Other nodes.
NodeModel<Animal> node1 = new NodeModel<>(new Animal(R.drawable.ic_02,"sub0"));
NodeModel<Animal> node2 = new NodeModel<>(new Animal(R.drawable.ic_03,"sub1"));
NodeModel<Animal> node3 = new NodeModel<>(new Animal(R.drawable.ic_04,"sub2"));
NodeModel<Animal> node4 = new NodeModel<>(new Animal(R.drawable.ic_05,"sub3"));
NodeModel<Animal> node5 = new NodeModel<>(new Animal(R.drawable.ic_06,"sub4"));


//Build the relationship between parent node and childs,like:
//treeModel.add(parent, child1, child2, ...., childN);
treeModel.add(node0, node1, node2);
treeModel.add(node1, node3, node4);
treeModel.add(node2, node5);

//finally set this treeModel to the adapter
adapter.setTreeModel(treeModel);

實(shí)現(xiàn)基本的布局流程

這里涉及View自定義的基本三部曲onMeasure耙箍、onLayoutonDrawonDispatchDraw, 其中我把onMeasureonLayout布局的交給了一個(gè)特定的類LayoutManager處理辩昆,并且把節(jié)點(diǎn)的子View生成及綁定交給Adapter處理旨袒,在onDispatchDraw中畫節(jié)點(diǎn)的連線也交給Adapter處理。這樣可以極大地方便使用者自定義連線及節(jié)點(diǎn)View砚尽,甚至是自定義LayoutManager施无。另外在onSizeChange中記錄控件的大小。

這幾個(gè)關(guān)鍵點(diǎn)的流程是onMeasure->onLayout->onSizeChanged->onDrawonDispatchDraw

    private TreeViewHolder<?> createHolder(NodeModel<?> node) {
        int type = adapter.getHolderType(node);
        ...
        //node 子View創(chuàng)建交給adapter
        return adapter.onCreateViewHolder(this, (NodeModel)node);
    }
    /**
    * 初始化添加NodeView
    **/
    private void addNodeViewToGroup(NodeModel<?> node) {
        TreeViewHolder<?> treeViewHolder = createHolder(node);
        //node 子View綁定交給adapter
        adapter.onBindViewHolder((TreeViewHolder)treeViewHolder);
        ...
    }
    ...
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        TreeViewLog.e(TAG,"onMeasure");
        final int size = getChildCount();
        for (int i = 0; i < size; i++) {
            measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
        }
        if(MeasureSpec.getSize(widthMeasureSpec)>0 && MeasureSpec.getSize(heightMeasureSpec)>0){
            winWidth  = MeasureSpec.getSize(widthMeasureSpec);
            winHeight = MeasureSpec.getSize(heightMeasureSpec);
        }
        if (mTreeLayoutManager != null && mTreeModel != null) {
            mTreeLayoutManager.setViewport(winHeight,winWidth);
            //交給LayoutManager測(cè)量
            mTreeLayoutManager.performMeasure(this);
            ViewBox viewBox = mTreeLayoutManager.getTreeLayoutBox();
            drawInfo.setSpace(mTreeLayoutManager.getSpacePeerToPeer(),mTreeLayoutManager.getSpaceParentToChild());
            int specWidth = MeasureSpec.makeMeasureSpec(Math.max(winWidth, viewBox.getWidth()), MeasureSpec.EXACTLY);
            int specHeight = MeasureSpec.makeMeasureSpec(Math.max(winHeight,viewBox.getHeight()),MeasureSpec.EXACTLY);
            setMeasuredDimension(specWidth,specHeight);
        }else{
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        TreeViewLog.e(TAG,"onLayout");
        if (mTreeLayoutManager != null && mTreeModel != null) {
            //交給LayoutManager布局
            mTreeLayoutManager.performLayout(this);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //記錄初始大小
        viewWidth = w;
        viewHeight = h;
        drawInfo.setWindowWidth(w);
        drawInfo.setWindowHeight(h);
        //記錄適應(yīng)窗口的scale
        fixWindow();
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mTreeModel != null) {
            drawInfo.setCanvas(canvas);
            drawTreeLine(mTreeModel.getRootNode());
        }
    }
    /**
     * 繪制樹形的連線
     * @param root root node
     */
    private void drawTreeLine(NodeModel<?> root) {
        LinkedList<? extends NodeModel<?>> childNodes = root.getChildNodes();
        for (NodeModel<?> node : childNodes) {
            ...
            //畫連線交給adapter或mTreeLayoutManager處理
            BaseLine adapterDrawLine = adapter.onDrawLine(drawInfo);
            if(adapterDrawLine!=null){
                adapterDrawLine.draw(drawInfo);
            }else{
                mTreeLayoutManager.performDrawLine(drawInfo);
            }
            drawTreeLine(node);
        }
    }

實(shí)現(xiàn)自由放縮及拖動(dòng)

這部分是核心點(diǎn)必孤,乍一看很簡(jiǎn)單猾骡,不就是處理下dispaTouchEventonInterceptTouchEventonTouchEvent就可以了嗎敷搪?沒錯(cuò)是都是在這幾個(gè)函數(shù)中處理兴想,但是要知道以下這幾個(gè)難點(diǎn):

  1. 這個(gè)自定義控件要放縮或移動(dòng)過程中,通過onTouchEvent中MotionEvent.getX()拿到的觸摸事件也是放縮后觸點(diǎn)相對(duì)父View的位置赡勘,而getRaw又不是所有SDK版本都支持的嫂便,因?yàn)椴荒塬@取穩(wěn)定的觸點(diǎn)數(shù)據(jù),所以可能放縮會(huì)出現(xiàn)震動(dòng)的現(xiàn)象
  2. 這個(gè)樹狀圖自定義控件子節(jié)點(diǎn)View也是ViewGroup闸与,至少拖動(dòng)放縮不能影響子節(jié)點(diǎn)View里的控件點(diǎn)擊事件
  3. 另外還要考慮毙替,回歸屏幕中心控制、增刪節(jié)點(diǎn)要穩(wěn)定目標(biāo)節(jié)點(diǎn)View顯示践樱、反變換獲取View相對(duì)屏幕位置等, 實(shí)現(xiàn)放縮及拖動(dòng)時(shí)的觸點(diǎn)跟隨

對(duì)于問題1厂画,可以再加一層一樣大小的ViewGroup(其實(shí)就是GysoTreeView,它是一個(gè)殼)用來接收觸摸事件拷邢,這樣因?yàn)檫@個(gè)接收觸摸事件的ViewGroup是大小是穩(wěn)定的袱院,所以攔截的觸摸要是穩(wěn)定的。里面的treeViewContainer是真正的樹狀圖ViewGroup容器解孙。

    public GysoTreeView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
        setClipChildren(false);
        setClipToPadding(false);
        treeViewContainer = new TreeViewContainer(getContext());
        treeViewContainer.setLayoutParams(layoutParams);
        addView(treeViewContainer);
        treeViewGestureHandler = new TouchEventHandler(getContext(), treeViewContainer);
        treeViewGestureHandler.setKeepInViewport(false);

        //set animate default
        treeViewContainer.setAnimateAdd(true);
        treeViewContainer.setAnimateRemove(true);
        treeViewContainer.setAnimateMove(true);
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        super.requestDisallowInterceptTouchEvent(disallowIntercept);
        this.disallowIntercept = disallowIntercept;
        TreeViewLog.e(TAG, "requestDisallowInterceptTouchEvent:"+disallowIntercept);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        TreeViewLog.e(TAG, "onInterceptTouchEvent: "+MotionEvent.actionToString(event.getAction()));
        return (!disallowIntercept && treeViewGestureHandler.detectInterceptTouchEvent(event)) || super.onInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        TreeViewLog.e(TAG, "onTouchEvent: "+MotionEvent.actionToString(event.getAction()));
        return !disallowIntercept && treeViewGestureHandler.onTouchEvent(event);
    }

TouchEventHandler用來處理觸摸事件抛人,有點(diǎn)像SDK提供的ViewDragHelper判斷是否需要攔截觸摸事件,并處理放縮廷臼、拖動(dòng)及慣性滑動(dòng)。判斷是不是滑動(dòng)了一小段距離寂恬,是那么攔截

    /**
     * to detect whether should intercept the touch event
     * @param event event
     * @return true for intercept
     */
    public boolean detectInterceptTouchEvent(MotionEvent event){
        final int action = event.getAction() & MotionEvent.ACTION_MASK;
        onTouchEvent(event);
        if (action == MotionEvent.ACTION_DOWN){
            preInterceptTouchEvent = MotionEvent.obtain(event);
            mIsMoving = false;
        }
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            mIsMoving = false;
        }
        //如果滑動(dòng)大于mTouchSlop初肉,則觸發(fā)攔截
        if(action == MotionEvent.ACTION_MOVE && mTouchSlop < calculateMoveDistance(event, preInterceptTouchEvent)){
            mIsMoving = true;
        }
        return mIsMoving;
    }

    /**
     * handler the touch event, drag and scale
     * @param event touch event
     * @return true for has consume
     */
    public boolean onTouchEvent(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        //Log.e(TAG, "onTouchEvent:"+event);
        int action =  event.getAction() & MotionEvent.ACTION_MASK;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mode = TOUCH_MODE_SINGLE;
                preMovingTouchEvent = MotionEvent.obtain(event);
                if(mView instanceof TreeViewContainer){
                    minScale = ((TreeViewContainer)mView).getMinScale();
                }
                if(flingX!=null){
                    flingX.cancel();
                }
                if(flingY!=null){
                    flingY.cancel();
                }
                break;
            case MotionEvent.ACTION_UP:
                mode = TOUCH_MODE_RELEASE;
                break;
            case MotionEvent.ACTION_POINTER_UP:
            case MotionEvent.ACTION_CANCEL:
                mode = TOUCH_MODE_UNSET;
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                mode++;
                if (mode >= TOUCH_MODE_DOUBLE){
                    scaleFactor = preScaleFactor = mView.getScaleX();
                    preTranslate.set( mView.getTranslationX(),mView.getTranslationY());
                    scaleBaseR = (float) distanceBetweenFingers(event);
                    centerPointBetweenFingers(event,preFocusCenter);
                    centerPointBetweenFingers(event,postFocusCenter);
                }
                break;

            case MotionEvent.ACTION_MOVE:
                if (mode >= TOUCH_MODE_DOUBLE) {
                    float scaleNewR = (float) distanceBetweenFingers(event);
                    centerPointBetweenFingers(event,postFocusCenter);
                    if (scaleBaseR <= 0){
                        break;
                    }
                    scaleFactor = (scaleNewR / scaleBaseR) * preScaleFactor * 0.15f + scaleFactor * 0.85f;
                    int scaleState = TreeViewControlListener.FREE_SCALE;
                    float finalMinScale = isKeepInViewport?minScale:minScale*0.8f;
                    if (scaleFactor >= MAX_SCALE) {
                        scaleFactor = MAX_SCALE;
                        scaleState = TreeViewControlListener.MAX_SCALE;
                    }else if (scaleFactor <= finalMinScale) {
                        scaleFactor = finalMinScale;
                        scaleState = TreeViewControlListener.MIN_SCALE;
                    }
                    if(controlListener!=null){
                        int current = (int)(scaleFactor*100);
                        //just make it no so frequently callback
                        if(scalePercentOnlyForControlListener!=current){
                            scalePercentOnlyForControlListener = current;
                            controlListener.onScaling(scaleState,scalePercentOnlyForControlListener);
                        }
                    }
                    mView.setPivotX(0);
                    mView.setPivotY(0);
                    mView.setScaleX(scaleFactor);
                    mView.setScaleY(scaleFactor);
                    float tx = postFocusCenter.x-(preFocusCenter.x-preTranslate.x)*scaleFactor / preScaleFactor;
                    float ty = postFocusCenter.y-(preFocusCenter.y-preTranslate.y)*scaleFactor / preScaleFactor;
                    mView.setTranslationX(tx);
                    mView.setTranslationY(ty);
                    keepWithinBoundaries();
                } else if (mode == TOUCH_MODE_SINGLE) {
                    float deltaX = event.getRawX() - preMovingTouchEvent.getRawX();
                    float deltaY = event.getRawY() - preMovingTouchEvent.getRawY();
                    onSinglePointMoving(deltaX, deltaY);
                }
                break;
            case MotionEvent.ACTION_OUTSIDE:
                TreeViewLog.e(TAG, "onTouchEvent: touch out side" );
                break;
        }
        preMovingTouchEvent = MotionEvent.obtain(event);
        return true;
    }

對(duì)于問題2,為了不影響節(jié)點(diǎn)View的點(diǎn)擊事件妄壶,我們不能使用Canvas去移送或放縮寄狼,否則點(diǎn)擊位置會(huì)錯(cuò)亂泊愧。另外,也不能使用Sroller去控制奢浑,因?yàn)?code>scrollTo滾動(dòng)控制不會(huì)記錄在View變換Matrix中雀彼,為了方便控制不使用scrollTo, 而是使用setTranslationYsetScaleY, 這樣可以很方便根據(jù)變換矩陣來控制整個(gè)樹狀圖即寡。

對(duì)于問題3,控制變換及反變換, setPivotX(0)這樣你可以很方便的通過x0*scale+translate = x1確定變換關(guān)系

mView.setPivotX(0);
mView.setPivotY(0);
mView.setScaleX(scaleFactor);
mView.setScaleY(scaleFactor);
//觸點(diǎn)跟隨
float tx = postFocusCenter.x-(preFocusCenter.x-preTranslate.x)*scaleFactor / preScaleFactor;
float ty = postFocusCenter.y-(preFocusCenter.y-preTranslate.y)*scaleFactor / preScaleFactor;
mView.setTranslationX(tx);
mView.setTranslationY(ty);

實(shí)現(xiàn)添加刪除節(jié)點(diǎn)動(dòng)畫

實(shí)現(xiàn)思路很簡(jiǎn)單莺丑,保存當(dāng)前相對(duì)目標(biāo)節(jié)點(diǎn)位置信息梢莽,增刪節(jié)點(diǎn)后昏名,把重新測(cè)量布局的位置作為最新位置阵面,位置變化進(jìn)度用0->1間的百分比表示

首先洪鸭,保存當(dāng)前相對(duì)目標(biāo)節(jié)點(diǎn)位置信息览爵,如果是刪除則選其父節(jié)點(diǎn)作為目標(biāo)節(jié)點(diǎn)蜓竹,如果是添加節(jié)點(diǎn)储藐,那么選添加子節(jié)點(diǎn)的父節(jié)點(diǎn)作為目標(biāo)節(jié)點(diǎn)邑茄,記錄這個(gè)節(jié)點(diǎn)相對(duì)屏幕的位置,及這時(shí)的放縮比例左医,并且記錄所有其他節(jié)點(diǎn)View相對(duì)這個(gè)目標(biāo)節(jié)點(diǎn)的位置浮梢。寫代碼過程中彤路,使用View.setTag記錄數(shù)據(jù)

    /**
     * Prepare moving, adding or removing nodes, record the last one node as an anchor node on view port, so that make it looks smooth change
     * Note:The last one will been choose as target node.
     *  @param nodeModels nodes[nodes.length-1] as the target one
     */
    private void recordAnchorLocationOnViewPort(boolean isRemove, NodeModel<?>... nodeModels) {
        if(nodeModels==null || nodeModels.length==0){
            return;
        }
        NodeModel<?> targetNode = nodeModels[nodeModels.length-1];
        if(targetNode!=null && isRemove){
            //if remove, parent will be the target node
            Map<NodeModel<?>,View> removeNodeMap = new HashMap<>();
            targetNode.selfTraverse(node -> {
                removeNodeMap.put(node,getTreeViewHolder(node).getView());
            });
            setTag(R.id.mark_remove_views,removeNodeMap);
            targetNode = targetNode.getParentNode();
        }
        if(targetNode!=null){
            TreeViewHolder<?> targetHolder = getTreeViewHolder(targetNode);
            if(targetHolder!=null){
                View targetHolderView = targetHolder.getView();
                targetHolderView.setElevation(Z_SELECT);
                ViewBox targetBox = ViewBox.getViewBox(targetHolderView);
                //get target location on view port 相對(duì)窗口的位置記錄
                ViewBox targetBoxOnViewport = targetBox.convert(getMatrix());

                setTag(R.id.target_node,targetNode);
                setTag(R.id.target_location_on_viewport,targetBoxOnViewport);

                //The relative locations of other nodes 相對(duì)位置記錄
                Map<NodeModel<?>,ViewBox> relativeLocationMap = new HashMap<>();
                mTreeModel.doTraversalNodes(node->{
                    TreeViewHolder<?> oneHolder = getTreeViewHolder(node);
                    ViewBox relativeBox =
                            oneHolder!=null?
                            ViewBox.getViewBox(oneHolder.getView()).subtract(targetBox):
                            new ViewBox();
                    relativeLocationMap.put(node,relativeBox);
                });
                setTag(R.id.relative_locations,relativeLocationMap);
            }
        }
    }

然后按正常流程觸發(fā)重新測(cè)量远豺、布局坞嘀。但是這時(shí)不要急著畫到屏幕,先根據(jù)目標(biāo)節(jié)點(diǎn)原來在屏幕的位置棺滞,及放縮大小继准,反變換使目標(biāo)節(jié)點(diǎn)不至于產(chǎn)生跳動(dòng)的感覺矮男。

                ...
                if(targetLocationOnViewPortTag instanceof ViewBox){
                    ViewBox targetLocationOnViewPort=(ViewBox)targetLocationOnViewPortTag;

                    //fix pre size and location 根據(jù)目標(biāo)節(jié)點(diǎn)在手機(jī)中屏幕的位置重新移動(dòng)昂灵,避免跳動(dòng)
                    float scale = targetLocationOnViewPort.getWidth() * 1f / finalLocation.getWidth();
                    treeViewContainer.setPivotX(0);
                    treeViewContainer.setPivotY(0);
                    treeViewContainer.setScaleX(scale);
                    treeViewContainer.setScaleY(scale);
                    float dx = targetLocationOnViewPort.left-finalLocation.left*scale;
                    float dy = targetLocationOnViewPort.top-finalLocation.top*scale;
                    treeViewContainer.setTranslationX(dx);
                    treeViewContainer.setTranslationY(dy);
                    return true;
                }
                ...

最后在Animate的start中根據(jù)相對(duì)位置還原添加刪除前的位置舞萄,0->1變換到最終最新位置

    @Override
    public void performLayout(final TreeViewContainer treeViewContainer) {
        final TreeModel<?> mTreeModel = treeViewContainer.getTreeModel();
        if (mTreeModel != null) {
            mTreeModel.doTraversalNodes(new ITraversal<NodeModel<?>>() {
                @Override
                public void next(NodeModel<?> next) {
                    layoutNodes(next, treeViewContainer);
                }

                @Override
                public void finish() {
                    //布局位置確定完后,開始通過動(dòng)畫從相對(duì)位置移動(dòng)到最終位置
                    layoutAnimate(treeViewContainer);
                }
            });
        }
    }

    /**
     * For layout animator
     * @param treeViewContainer container
     */
    protected void layoutAnimate(TreeViewContainer treeViewContainer) {
        TreeModel<?> mTreeModel = treeViewContainer.getTreeModel();
        //means that smooth move from preLocation to curLocation
        Object nodeTag = treeViewContainer.getTag(R.id.target_node);
        Object targetNodeLocationTag = treeViewContainer.getTag(R.id.target_node_final_location);
        Object relativeLocationMapTag = treeViewContainer.getTag(R.id.relative_locations);
        Object animatorTag = treeViewContainer.getTag(R.id.node_trans_animator);
        if(animatorTag instanceof ValueAnimator){
            ((ValueAnimator)animatorTag).end();
        }
        if (nodeTag instanceof NodeModel
                && targetNodeLocationTag instanceof ViewBox
                && relativeLocationMapTag instanceof Map) {
            ViewBox targetNodeLocation = (ViewBox) targetNodeLocationTag;
            Map<NodeModel<?>,ViewBox> relativeLocationMap = (Map<NodeModel<?>,ViewBox>)relativeLocationMapTag;

            AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
            ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f);
            valueAnimator.setDuration(TreeViewContainer.DEFAULT_FOCUS_DURATION);
            valueAnimator.setInterpolator(interpolator);
            valueAnimator.addUpdateListener(value -> {
                //先根據(jù)相對(duì)位置畫出原來的位置
                float ratio = (float) value.getAnimatedValue();
                TreeViewLog.e(TAG, "valueAnimator update ratio[" + ratio + "]");
                mTreeModel.doTraversalNodes(node -> {
                    TreeViewHolder<?> treeViewHolder = treeViewContainer.getTreeViewHolder(node);
                    if (treeViewHolder != null) {
                        View view = treeViewHolder.getView();
                        ViewBox preLocation = (ViewBox) view.getTag(R.id.node_pre_location);
                        ViewBox deltaLocation = (ViewBox) view.getTag(R.id.node_delta_location);
                        if(preLocation !=null && deltaLocation!=null){
                            //calculate current location 計(jì)算漸變位置 并 布局
                            ViewBox currentLocation = preLocation.add(deltaLocation.multiply(ratio));
                            view.layout(currentLocation.left,
                                    currentLocation.top,
                                    currentLocation.left+view.getMeasuredWidth(),
                                    currentLocation.top+view.getMeasuredHeight());
                        }
                    }
                });
            });

            valueAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationStart(Animator animation, boolean isReverse) {
                    TreeViewLog.e(TAG, "onAnimationStart ");
                    //calculate and layout on preLocation  位置變換過程
                    mTreeModel.doTraversalNodes(node -> {
                        TreeViewHolder<?> treeViewHolder = treeViewContainer.getTreeViewHolder(node);
                        if (treeViewHolder != null) {
                            View view = treeViewHolder.getView();
                            ViewBox relativeLocation = relativeLocationMap.get(treeViewHolder.getNode());

                            //calculate location info 計(jì)算位置
                            ViewBox preLocation = targetNodeLocation.add(relativeLocation);
                            ViewBox finalLocation = (ViewBox) view.getTag(R.id.node_final_location);
                            if(preLocation==null || finalLocation==null){
                                return;
                            }

                            ViewBox deltaLocation = finalLocation.subtract(preLocation);

                            //save as tag
                            view.setTag(R.id.node_pre_location, preLocation);
                            view.setTag(R.id.node_delta_location, deltaLocation);

                            //layout on preLocation 更新布局
                            view.layout(preLocation.left, preLocation.top, preLocation.left+view.getMeasuredWidth(), preLocation.top+view.getMeasuredHeight());
                        }
                    });

                }

                @Override
                public void onAnimationEnd(Animator animation, boolean isReverse) {
                    ...
                    //layout on finalLocation 在布局最終位置
                    mTreeModel.doTraversalNodes(node -> {
                        TreeViewHolder<?> treeViewHolder = treeViewContainer.getTreeViewHolder(node);
                        if (treeViewHolder != null) {
                            View view = treeViewHolder.getView();
                            ViewBox finalLocation = (ViewBox) view.getTag(R.id.node_final_location);
                            if(finalLocation!=null){
                                view.layout(finalLocation.left, finalLocation.top, finalLocation.right, finalLocation.bottom);
                            }
                            view.setTag(R.id.node_pre_location,null);
                            view.setTag(R.id.node_delta_location,null);
                            view.setTag(R.id.node_final_location, null);
                            view.setElevation(TreeViewContainer.Z_NOR);
                        }
                    });
                }
            });
            treeViewContainer.setTag(R.id.node_trans_animator,valueAnimator);
            valueAnimator.start();
        }
    }

實(shí)現(xiàn)樹狀圖的回歸適應(yīng)屏幕

這個(gè)功能點(diǎn)相對(duì)簡(jiǎn)單甘晤,前提是TreeViewContainer放縮一定要以(0,0)為中心點(diǎn),并且TreeViewContainer的移動(dòng)放縮不是使用Canas或srollTo操作遏弱,這樣在onSizeChange中塞弊,我們記錄適配屏幕的scale就行了。

/**
*記錄
*/
@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        TreeViewLog.e(TAG,"onSizeChanged w["+w+"]h["+h+"]oldw["+oldw+"]oldh["+oldh+"]");
        viewWidth = w;
        viewHeight = h;
        drawInfo.setWindowWidth(w);
        drawInfo.setWindowHeight(h);
        fixWindow();
    }
    /**
     * fix view tree
     */
    private void fixWindow() {
        float scale;
        float hr = 1f*viewHeight/winHeight;
        float wr = 1f*viewWidth/winWidth;
        scale = Math.max(hr, wr);
        minScale = 1f/scale;
        if(Math.abs(scale-1)>0.01f){
            //setPivotX((winWidth*scale-viewWidth)/(2*(scale-1)));
            //setPivotY((winHeight*scale-viewHeight)/(2*(scale-1)));
            setPivotX(0);
            setPivotY(0);
            setScaleX(1f/scale);
            setScaleY(1f/scale);
        }
        //when first init
        if(centerMatrix==null){
            centerMatrix = new Matrix();
        }
        centerMatrix.set(getMatrix());
        float[] values = new float[9];
        centerMatrix.getValues(values);
        values[Matrix.MTRANS_X]=0f;
        values[Matrix.MTRANS_Y]=0f;
        centerMatrix.setValues(values);
        setTouchDelegate();
    }

    /**
    *恢復(fù)
    */
   public void focusMidLocation() {
        TreeViewLog.e(TAG, "focusMidLocation: "+getMatrix());
        float[] centerM = new float[9];
        if(centerMatrix==null){
            TreeViewLog.e(TAG, "no centerMatrix!!!");
            return;
        }
        centerMatrix.getValues(centerM);
        float[] now = new float[9];
        getMatrix().getValues(now);
        if(now[Matrix.MSCALE_X]>0&&now[Matrix.MSCALE_Y]>0){
            animate().scaleX(centerM[Matrix.MSCALE_X])
                    .translationX(centerM[Matrix.MTRANS_X])
                    .scaleY(centerM[Matrix.MSCALE_Y])
                    .translationY(centerM[Matrix.MTRANS_Y])
                    .setDuration(DEFAULT_FOCUS_DURATION)
                    .start();
        }
    }

拖動(dòng)編輯樹狀圖結(jié)構(gòu)

想要拖動(dòng)編輯樹狀圖結(jié)構(gòu)要有如下幾個(gè)步驟:

  1. 請(qǐng)求父View不要攔截觸摸事件

  2. 在TreeViewContainer中使用ViewDragHelper實(shí)現(xiàn)捕獲View,以目標(biāo)Node的所有Node一并記錄原始位置

  3. 拖動(dòng)目標(biāo)View組

  4. 在移動(dòng)過程中诀黍,計(jì)算跟是不是碰撞到某個(gè)節(jié)點(diǎn)View了,如果是那么記錄碰撞的節(jié)點(diǎn)

  5. 在釋放時(shí)枣宫,如果有碰撞節(jié)點(diǎn)吃环,那么走添加刪除節(jié)點(diǎn)流程即可

  6. 在釋放時(shí),如果沒有碰撞點(diǎn)歇拆,則使用Scroller回滾到初始位置

請(qǐng)求父View不要攔截觸摸事件, 這個(gè)不要搞混了故觅,是parent.requestDisallowInterceptTouchEvent(isEditMode);而不是直接requestDisallowInterceptTouchEvent

    protected void requestMoveNodeByDragging(boolean isEditMode) {
        this.isDraggingNodeMode = isEditMode;
        ViewParent parent = getParent();
        if (parent instanceof View) {
            parent.requestDisallowInterceptTouchEvent(isEditMode);
        }
    }

這里簡(jiǎn)單說一下ViewDragHelper的使用, 官方說ViewDragHelper是在自定義ViewGroup時(shí)非常有用的工具類渠啊。它提供了一系列有用的操作及狀態(tài)跟蹤使用戶可以在父類的中拖動(dòng)或改變子View的位置。注重, 限于拖動(dòng)及改變位置贯溅,對(duì)于放縮那就無能為力了, 不過剛好拖動(dòng)編輯節(jié)點(diǎn)這個(gè)功能不使用放縮。它的原理也是译柏,判斷有沒滑動(dòng)一定距離姐霍,或者是否到達(dá)了邊界來攔截觸摸事件。

//1 初始化
dragHelper = ViewDragHelper.create(this, dragCallback);
//2 判斷攔截及處理onTouchEvent
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercept = dragHelper.shouldInterceptTouchEvent(event);
    TreeViewLog.e(TAG, "onInterceptTouchEvent: "+MotionEvent.actionToString(event.getAction())+" intercept:"+intercept);
    return isDraggingNodeMode && intercept;
}

@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
    TreeViewLog.e(TAG, "onTouchEvent: "+MotionEvent.actionToString(event.getAction()));
    if(isDraggingNodeMode) {
        dragHelper.processTouchEvent(event);
    }
    return isDraggingNodeMode;
}
//3 實(shí)現(xiàn)Callback
private final ViewDragHelper.Callback dragCallback = new ViewDragHelper.Callback(){
    @Override
    public boolean tryCaptureView(@NonNull View child, int pointerId) {
        //是否捕獲拖動(dòng)的View
        return false;
    }

    @Override
    public int getViewHorizontalDragRange(@NonNull  View child) {
        //在判斷是否攔截時(shí)胯府,判斷是否超出水平移動(dòng)范圍
        return Integer.MAX_VALUE;
    }

    @Override
    public int getViewVerticalDragRange(@NonNull  View child) {
        //在判斷是否攔截時(shí)骂因,判斷是否超出垂直移動(dòng)范圍
        return Integer.MAX_VALUE;
    }

    @Override
    public int clampViewPositionHorizontal(@NonNull  View child, int left, int dx) {
        //水平移動(dòng)位置差赃泡,返回希望移動(dòng)后的位置
        //特別注意在攔截階段 返回left與原來一樣,說明到達(dá)邊界影所,不攔截
        return left;
    }

    @Override
    public int clampViewPositionVertical(@NonNull  View child, int top, int dy) {
        //垂直移動(dòng)位置差猴娩,返回希望移動(dòng)后的位置
        //特別注意在攔截階段 返回left與原來一樣勺阐,說明到達(dá)邊界,不攔截
        return top;
    }

    @Override
    public void onViewReleased(@NonNull  View releasedChild, float xvel, float yvel) {
        //釋放捕獲的View
    }
};

那么捕獲時(shí)蟆豫,開始記錄位置

        @Override
        public boolean tryCaptureView(@NonNull View child, int pointerId) {
            //如果是拖動(dòng)編輯功能懒闷,那么使用記錄要移動(dòng)的塊
            if(isDraggingNodeMode && dragBlock.load(child)){
                child.setTag(R.id.edit_and_dragging,IS_EDIT_DRAGGING);
                child.setElevation(Z_SELECT);
                return true;
            }
            return false;
        }

拖動(dòng)一組View時(shí),因?yàn)檫@組View的相對(duì)位置是不變的帮辟,所以可以都是無論是垂直方向還是水平方向都使用同一個(gè)dx玩焰,dy

    public void drag(int dx, int dy){
        if(!mScroller.isFinished()){
            return;
        }
        this.isDragging = true;
        for (int i = 0; i < tmp.size(); i++) {
            View view = tmp.get(i);
            //offset變化的是布局昔园,不是變換矩陣并炮。而這里拖動(dòng)沒有影響container的Matrix
            view.offsetLeftAndRight(dx);
            view.offsetTopAndBottom(dy);
        }
    }

拖動(dòng)過程中逃魄,要計(jì)算是否碰撞到其他View

@Override
public int clampViewPositionHorizontal(@NonNull  View child, int left, int dx) {
    //攔截前返回left說明沒有到邊界可以攔截壹若, 攔截后返回原來位置皂冰,說明不用dragHelper來幫忙移動(dòng),我們自己來一共目標(biāo)View
    if(dragHelper.getViewDragState()==ViewDragHelper.STATE_DRAGGING){
        final int oldLeft = child.getLeft();
        dragBlock.drag(dx,0);
        //拖動(dòng)過程中不斷判斷是否碰撞
        estimateToHitTarget(child);
        invalidate();
        return oldLeft;
    }else{
        return left;
    }
}

@Override
public int clampViewPositionVertical(@NonNull  View child, int top, int dy) {
    //與上面代碼一致
    ...
}

//如果撞擊了赂蕴,那么invalidate概说,畫撞擊提醒
private void drawDragBackGround(View view){
    Object fTag = view.getTag(R.id.the_hit_target);
    boolean getHit = fTag != null;
    if(getHit){
        //draw
        .....
        mPaint.reset();
        mPaint.setColor(Color.parseColor("#4FF1286C"));
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        PointF centerPoint = getCenterPoint(view);
        drawInfo.getCanvas().drawCircle(centerPoint.x,centerPoint.y,(float)fR,mPaint);
        PointPool.free(centerPoint);
    }
}

釋放時(shí)嚣伐,如果有目標(biāo)那么刪除再添加,走刪除添加流程放典;如果沒有基茵,那么使用Scroller協(xié)助回滾

//釋放
@Override
public void onViewReleased(@NonNull  View releasedChild, float xvel, float yvel) {
    TreeViewLog.d(TAG, "onViewReleased: ");
    Object fTag = releasedChild.getTag(R.id.the_hit_target);
    boolean getHit = fTag != null;
    //如果及記錄了撞擊點(diǎn),刪除再添加弥臼,走刪除添加流程
    if(getHit){
        TreeViewHolder<?> targetHolder = getTreeViewHolder((NodeModel)fTag);
        NodeModel<?> targetHolderNode = targetHolder.getNode();

        TreeViewHolder<?> releasedChildHolder = (TreeViewHolder<?>)releasedChild.getTag(R.id.item_holder);
        NodeModel<?> releasedChildHolderNode = releasedChildHolder.getNode();
        if(releasedChildHolderNode.getParentNode()!=null){
            mTreeModel.removeNode(releasedChildHolderNode.getParentNode(),releasedChildHolderNode);
        }
        mTreeModel.addNode(targetHolderNode,releasedChildHolderNode);
        mTreeModel.calculateTreeNodesDeep();
        if(isAnimateMove()){
            recordAnchorLocationOnViewPort(false,targetHolderNode);
        }
        requestLayout();
    }else{
        //recover 如果沒有径缅,那么使用Scroller協(xié)助回滾
        dragBlock.smoothRecover(releasedChild);
    }
    dragBlock.setDragging(false);
    releasedChild.setElevation(Z_NOR);
    releasedChild.setTag(R.id.edit_and_dragging,null);
    releasedChild.setTag(R.id.the_hit_target, null);
    invalidate();
}

//注意重寫container的computeScroll烙肺,實(shí)現(xiàn)更新
@Override
public void computeScroll() {
    if(dragBlock.computeScroll()){
        invalidate();
    }
}

寫在最后

到到這里就介紹完,整個(gè)樹狀節(jié)點(diǎn)圖的拖動(dòng)放縮兆旬,添加刪除節(jié)點(diǎn)丽猬,拖動(dòng)編輯等這幾個(gè)功能的實(shí)現(xiàn)原理了,當(dāng)然里面還有很多實(shí)現(xiàn)細(xì)節(jié)脚祟。你可以把這篇文章作為源碼查看的引導(dǎo),細(xì)節(jié)方面也還有很多待完善的地方为黎。后面這個(gè)開源應(yīng)該會(huì)繼續(xù)更新行您,大家也可以一起探討,fork出來一起改炕檩。如果覺得不錯(cuò)請(qǐng)給個(gè)星呢捌斧。

這個(gè)項(xiàng)目如果有人用就會(huì)持續(xù)更新下去。喜歡點(diǎn)個(gè)贊妇押,謝謝姓迅。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市色冀,隨后出現(xiàn)的幾起案子柱嫌,更是在濱河造成了極大的恐慌,老刑警劉巖与学,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件索守,死亡現(xiàn)場(chǎng)離奇詭異抑片,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)截汪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來阳柔,“玉大人蚓峦,你說我怎么就攤上這事』糇” “怎么了干茉?”我有些...
    開封第一講書人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵角虫,是天一觀的道長(zhǎng)委造。 經(jīng)常有香客問我,道長(zhǎng)枫虏,這世上最難降的妖魔是什么爬虱? 我笑而不...
    開封第一講書人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任跑筝,我火速辦了婚禮,結(jié)果婚禮上曲梗,老公的妹妹穿的比我還像新娘。我一直安慰自己愧旦,他們只是感情好定罢,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著境蜕,像睡著了一般凌停。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上台诗,一...
    開封第一講書人閱讀 52,785評(píng)論 1 314
  • 那天赐俗,我揣著相機(jī)與錄音,去河邊找鬼粱快。 笑死叔扼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鳍咱。 我是一名探鬼主播与柑,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼丑念!你這毒婦竟也來了结蟋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤挠将,失蹤者是張志新(化名)和其女友劉穎舔稀,沒想到半個(gè)月后掌测,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體产园,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡什燕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年竞端,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了屎即。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片技俐。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡雕擂,死狀恐怖贱勃,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情贵扰,我是刑警寧澤,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布仪缸,位于F島的核電站,受9級(jí)特大地震影響宾茂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜跨晴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一端盆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蒋伦,春花似錦、人聲如沸痕届。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽渊啰。三九已至,卻和暖如春虽抄,著一層夾襖步出監(jiān)牢的瞬間独柑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工车酣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留索绪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓娘摔,卻偏偏與公主長(zhǎng)得像唤反,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子肠缨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

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