This article first shows the effect, followed by the implementation principle

Introduction to the

Tree View; Mind map; Think map; tree map; Tree; Mind mapping;

Github: github.com/guaishouN/a…

At present, I have not found a good Android tree map open source control, so I decided to write an open source control, compared with the market on mind map or tree map display (such as xMind, Mind Master, etc.) app, this open source framework is not inferior. In the process of implementing the tree diagram, the main application of a lot of custom control key knowledge points, such as custom ViewGroup steps, touch event handling, animation use, Scroller and inertial sliding, ViewDragHelper use, and so on. The main realization of the following function points.

  • Follow your fingers silky and shrink, drag, and slide with inertia

  • Automatic animation returns to center of screen

  • Support child node complex layout customization, and node layout click events do not conflict with sliding

  • The connection lines between nodes are customized

  • You can delete dynamic nodes

  • Nodes can be added dynamically

  • You can drag to adjust node relationships

  • Add, delete and move structures to add animation effects

Results show

Basics – wiring, layout, custom node View

add

delete

Drag a node to edit the book tree structure

Drag and drop without affecting the click

Drag and drop to fit the window

Using the step

The Animal class is used as an example bean only

public class Animal {
    public int headId;
    public String name;
}
Copy the code

Follow these four steps to use the open source control

1 Through the inheritance of TreeViewAdapter to achieve node data and node view binding

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 viewView 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; }}Copy the code

2 Configure LayoutManager. Main Settings: Layout style (expand right or vertical down), gap between parent and child, gap between child, line between nodes (already implemented straight, smooth, dotted, root-like, you can also use BaseLine for your own line)

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);
Copy the code

Set Adapter and LayoutManager to your tree

. treeView = findViewById(R.id.tree_view); TreeViewAdapter adapter =newAnimlTreeViewAdapter(); treeView.setAdapter(adapter); treeView.setTreeLayoutManager(treeLayoutManager); .Copy the code

4 Set node data

//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);
Copy the code

Implement the basic layout flow

OnMeasure, onLayout, onDraw, onDispatchDraw, onDispatchDraw And the node of the child View generation and binding to the Adapter processing, in onDispatchDraw draw node line also to the Adapter processing. This makes it much easier for users to customize the line and node View, and even the LayoutManager. Also record the size of the control in onSizeChange.

The flow for these key points is onMeasure->onLayout->onSizeChanged->onDraw or onDispatchDraw

    privateTreeViewHolder<? > createHolder(NodeModel<? > node) {inttype = adapter.getHolderType(node); .// Create a node child View for the adapter
        return adapter.onCreateViewHolder(this, (NodeModel)node);
    }
	/** * Initialize NodeView **/
	private void addNodeViewToGroup(NodeModel
        node) { TreeViewHolder<? > treeViewHolder = createHolder(node);// Node child View binding to adapteradapter.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);
            // Give it to LayoutManager to measure
            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) {
            // Give the layout to LayoutManager
            mTreeLayoutManager.performLayout(this); }}@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // Record the initial size
        viewWidth = w;
        viewHeight = h;
        drawInfo.setWindowWidth(w);
        drawInfo.setWindowHeight(h);
        // Record the scale of the adaptive window
        fixWindow();
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if(mTreeModel ! =null) { drawInfo.setCanvas(canvas); drawTreeLine(mTreeModel.getRootNode()); }}/** * Draw the line of the tree@param root root node
     */
    private void drawTreeLine(NodeModel
        root) { LinkedList<? extends NodeModel<? >> childNodes = root.getChildNodes();for(NodeModel<? > node : childNodes) { ...// Pass the connection to adapter or mTreeLayoutManager
            BaseLine adapterDrawLine = adapter.onDrawLine(drawInfo);
            if(adapterDrawLine! =null){
                adapterDrawLine.draw(drawInfo);
            }else{ mTreeLayoutManager.performDrawLine(drawInfo); } drawTreeLine(node); }}Copy the code

To achieve free and drag

This part is the core point. At first glance, it looks very simple, isn’t it just dispaTouchEvent, onInterceptTouchEvent and onTouchEvent? Yes, it is all processed in these functions, but to know the following several difficulties:

  1. This custom control is to be scaled or moved during the onTouchEventMotionEvent.getX()GetRaw is not supported by all SDK versions, because it does not get stable contact data, so it may cause vibration when it is shrunk
  2. This tree custom control child View is also a ViewGroup, at least drag and shrink does not affect the child View control click event
  3. In addition, it is necessary to consider the regression screen center control, adding and deleting nodes to stabilize the View display of the target node, inverse transformation to obtain the View relative to the screen position, etc., so as to realize the contact following when shrinking and dragging

For problem 1, you can add another layer of ViewGroup (which is actually a GysoTreeView, which is a shell) of the same size to receive touch events, so because the size of the ViewGroup that receives touch events is stable, the intercepted touches need to be stable. The treeViewContainer inside is the real treeViewGroup container.

    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); }Copy the code

TouchEventHandler handles touch events, sort of like the SDK’s ViewDragHelper to determine if you need to intercept touch events, and handles pinch, drag, and inertial sliding. Determine if it slipped a short distance. That’s the intercept

    /**
     * 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;
        }
        // If the slide is greater than mTouchSlop, intercept is triggered
        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.15 f + scaleFactor * 0.85 f;
                    int scaleState = TreeViewControlListener.FREE_SCALE;
                    floatfinalMinScale = isKeepInViewport? minScale:minScale*0.8 f;
                    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;
    }
Copy the code

For problem 2, in order not to affect the click event of the node View, we cannot use Canvas to move or shrink, otherwise the click position will be confused. Also, you can’t use a Sroller to control because the scrollTo control is not recorded in the View transformation Matrix, Instead of using scrollTo for ease of control, use setTranslationY and setScaleY to make it easy to control the entire tree based on the transformation matrix.

For Question 3, control pivotx and reverse pivotx, setPivotX(0) allows you to easily determine the pivotx relationship by using x0*scale+translate = x1

mView.setPivotX(0);
mView.setPivotY(0);
mView.setScaleX(scaleFactor);
mView.setScaleY(scaleFactor);
// Contact follow
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);
Copy the code

To add and delete node animation

The implementation idea is very simple, save the current relative target node location information, after adding or deleting nodes, the new measured layout position as the latest position, position change progress is expressed as the percentage between 0->1

First of all, save the current relative to the target node location information, if is to delete the selected his father node as the target, if is to add the node, then select add child nodes of the parent node as the target node, to record the location of the node relative to the screen, and then zoom ratio, and record all the other nodes of the View relative to the target node location. Use view.setTag to record data while writing code

    /**
     * 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(booleanisRemove, 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 nodeMap<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 Location record relative to the window
                ViewBox targetBoxOnViewport = targetBox.convert(getMatrix());

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

                // Record The relative locations of other nodesMap<NodeModel<? >,ViewBox> relativeLocationMap =newHashMap<>(); mTreeModel.doTraversalNodes(node->{ TreeViewHolder<? > oneHolder = getTreeViewHolder(node); ViewBox relativeBox = oneHolder! =null?
                            ViewBox.getViewBox(oneHolder.getView()).subtract(targetBox):
                            newViewBox(); relativeLocationMap.put(node,relativeBox); }); setTag(R.id.relative_locations,relativeLocationMap); }}}Copy the code

Then the normal process triggers the remeasurement and layout. But at this time do not rush to draw to the screen, first according to the original location of the target node in the screen, and the size of the shrinkage, the inverse transformation of the target node will not produce the feeling of jumping.

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

                    // Fix pre size and location to reset the target node based on the location of the screen in the phone
                    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; }...Copy the code

Finally, in Animate start, restore the previous position according to the relative position. 0->1 transforms to the final latest position

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

                @Override
                public void finish(a) {
                    // After the layout position is determined, start moving from the relative position to the final position by animationlayoutAnimate(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 instanceofMap) { 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 -> {
              	// First draw the original position according to the relative position
                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 locationViewBox 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 preLocationmTreeModel.doTraversalNodes(node -> { TreeViewHolder<? > treeViewHolder = treeViewContainer.getTreeViewHolder(node);if(treeViewHolder ! =null) {
                            View view = treeViewHolder.getView();
                            ViewBox relativeLocation = relativeLocationMap.get(treeViewHolder.getNode());

                            // Calculate location info
                            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);

                            // Update the layout on preLocationview.layout(preLocation.left, preLocation.top, preLocation.left+view.getMeasuredWidth(), preLocation.top+view.getMeasuredHeight()); }}); }@Override
                public void onAnimationEnd(Animator animation, boolean isReverse) {...// Layout on finalLocationmTreeModel.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(); }}Copy the code

Implement the regression adaptation screen of the tree

This function point is relatively simple, provided that the TreeViewContainer is centered on (0,0) and that the TreeViewContainer is not shrunk using Canas or srollTo. We’ll just record the scale that fits the screen.

/** * Record */
@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(a) {
        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.01 f) {//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();
    }

	/** * Restore */
   public void focusMidLocation(a) {
        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(); }}Copy the code

Drag to edit the tree structure

To drag and edit the tree structure, take the following steps:

  1. Ask the parent View not to intercept touch events

  2. Use the ViewDragHelper in the TreeViewContainer to capture the View and record the original location for all nodes of the target Node

  3. Drag the target View group

  4. During the movement, calculate whether or not you hit a node View, and if so, record the node that you hit

  5. When releasing, if there are collision nodes, then go through the process of adding and deleting nodes

  6. On release, if there is no point of collision, the Scroller is used to roll back to the original position

Request the parent View don’t intercept touch events, this don’t mixed up, is the parent requestDisallowInterceptTouchEvent (isEditMode); Instead of directly requestDisallowInterceptTouchEvent

    protected void requestMoveNodeByDragging(boolean isEditMode) {
        this.isDraggingNodeMode = isEditMode;
        ViewParent parent = getParent();
        if (parent instanceofView) { parent.requestDisallowInterceptTouchEvent(isEditMode); }}Copy the code

ViewDragHelper is a useful utility class for customizing viewgroups. It provides a series of useful actions and state tracking that allow users to drag or change the position of child views in a parent class. Note that, limited to dragging and changing position, there is nothing to do with scaling, but just dragging to edit a node does not use scaling. And what it does is it intercepts touch events by saying, is it sliding a certain distance, or is it reaching an edge.

/ / 1 initialization
dragHelper = ViewDragHelper.create(this, dragCallback);
//2 Determine to intercept and process 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 implementation Callback
private final ViewDragHelper.Callback dragCallback = new ViewDragHelper.Callback(){
    @Override
    public boolean tryCaptureView(@NonNull View child, int pointerId) {
        // Whether to capture the dragged View
        return false;
    }

    @Override
    public int getViewHorizontalDragRange(@NonNull  View child) {
        // When judging whether to intercept, judge whether it is beyond the range of horizontal movement
        return Integer.MAX_VALUE;
    }

    @Override
    public int getViewVerticalDragRange(@NonNull  View child) {
        // When determining whether to intercept, determine whether to exceed the vertical movement range
        return Integer.MAX_VALUE;
    }

    @Override
    public int clampViewPositionHorizontal(@NonNull  View child, int left, int dx) {
        // Move the position horizontally and return to the desired position
        // Note that in the intercept phase, return left as before, indicating that the boundary has been reached and no interception is made
        return left;
    }

    @Override
    public int clampViewPositionVertical(@NonNull  View child, int top, int dy) {
        // Return the desired position by moving the position vertically
        // Note that in the intercept phase, return left as before, indicating that the boundary has been reached and no interception is made
        return top;
    }

    @Override
    public void onViewReleased(@NonNull  View releasedChild, float xvel, float yvel) {
        // Release the captured View}};Copy the code

So when you capture, you start recording the location

        @Override
        public boolean tryCaptureView(@NonNull View child, int pointerId) {
            // If it is a drag edit function, then use the record to move the block
            if(isDraggingNodeMode && dragBlock.load(child)){
                child.setTag(R.id.edit_and_dragging,IS_EDIT_DRAGGING);
                child.setElevation(Z_SELECT);
                return true;
            }
            return false;
        }
Copy the code

When you drag a group of views, because the relative position of the group of views is the same, you can use the same dx, dy both vertically and horizontally

    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 changes the layout, not the transformation matrix. Dragging here does not affect the Container's Matrixview.offsetLeftAndRight(dx); view.offsetTopAndBottom(dy); }}Copy the code

As I drag, I’m going to calculate whether I’m hitting another View

@Override
public int clampViewPositionHorizontal(@NonNull  View child, int left, int dx) {
    // Return left before interception to indicate that there is no boundary to intercept. Return left after interception to indicate that there is no boundary to intercept
    if(dragHelper.getViewDragState()==ViewDragHelper.STATE_DRAGGING){
        final int oldLeft = child.getLeft();
        dragBlock.drag(dx,0);
        // Drag to determine whether the collision
        estimateToHitTarget(child);
        invalidate();
        return oldLeft;
    }else{
        returnleft; }}@Override
public int clampViewPositionVertical(@NonNull  View child, int top, int dy) {
    // The same as above. }// If a collision occurs, then invalidate and draw a warning
private void drawDragBackGround(View view){
    Object fTag = view.getTag(R.id.the_hit_target);
    booleangetHit = 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); }}Copy the code

Release, if there is a target, then delete and add, go delete and add process; If not, use a Scroller to assist in the rollback

/ / release
@Override
public void onViewReleased(@NonNull  View releasedChild, float xvel, float yvel) {
    TreeViewLog.d(TAG, "onViewReleased: ");
    Object fTag = releasedChild.getTag(R.id.the_hit_target);
    booleangetHit = fTag ! =null;
    // If the impact point is recorded, delete it and then add it
    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 if not, then use Scroller to assist the rollback
        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();
}

// Note that the container computeScroll is overwritten to implement the update
@Override
public void computeScroll(a) {
    if(dragBlock.computeScroll()){ invalidate(); }}Copy the code

Write in the last

To this end, the whole tree node diagram drag and shrink, add and delete nodes, drag and edit these several functions of the implementation principle, of course, there are a lot of implementation details. You can use this article as a guide to the source code, but there are still a lot of details to work out. Later this open source should continue to update, we can also discuss together, fork out to change together. Give me a star if you think it’s good.

This project will continue to be updated if anyone uses it. Like it, thank you.