Tree View; Mind map; Think map; tree map; Tree; Mind mapping; Organization chart; Hierarchy chart

The article directories

[1 Introduction](# Introduction)

[2 effect display](# effect display)

[3 use steps](# Use steps)

[4 implement the basic layout process](# Implement the basic layout process)

[5 implement free scale and drag](# Implement free scale and drag)

[6 implement add delete and node animation](# implement add delete and node animation)

[7 implement regression adaptation screen of tree graph](# Implement regression adaptation screen of tree graph)

[8 implement drag to edit tree](# implement drag to edit tree)

[9 write last](# write last)

Introduction to the

Github Connection: github.com/guaishouN/a…

At present, I have not found a good Android tree graph open source control, so I decided to write an open source control, compared with the app about mind mapping or tree graph display (such as xMind, Mind Master, etc.) in the market, the open source framework in this paper is not inferior. In the process of realizing this tree, many key knowledge points of custom control are mainly applied comprehensively, such as the steps of custom ViewGroup, the processing of touch events, the use of animation, Scroller and inertial sliding, the use of ViewDragHelper and so on. The main implementation of the following function points.

  • Silky follow finger retraction, drag, and inertial slide

  • Automatic animation returns to the center of the screen

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

  • The connection line between nodes is customized

  • Dynamic nodes can be deleted

  • Nodes can be dynamically added

  • You can drag to adjust node relationships

  • Add, delete, move structures add animation effects

Results show

Basics – Wiring, layout, custom node View

fs.png

add

add.gif

delete

add.gif

Drag nodes to edit the book tree structure

add.gif

Drag and drop does not affect clicking

add.gif

Drag and expand to fit Windows

add.gif

Using the step

The Animal class in the following illustration is a bean for example only

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

Copy the code

Follow these four steps to use the open source control

1 Bind node data to node view by inheriting TreeViewAdapter

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

2 Configure LayoutManager. Set layout style (expand right or vertically down), gap between parent and child, gap between children, line between nodes (already implemented straight line, smooth curve, dotted line, root line, you can also implement your own line via BaseLine)

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

3 set Adapter and LayoutManager to your tree

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

4 Configure 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 process

This involves the View customization of the basic trilogy onMeasure, onLayout, onDraw or onDispatchDraw, where I assign the onMeasure and onLayout layout to a specific class LayoutManager. In addition, the generation and binding of the child View of the node are handed over to Adapter, and the wiring of the node in onDispatchDraw is also handed over to Adapter. This makes it much easier for users to customize connections, node views, and even layoutManagers. Also record the size of the control in onSizeChange.

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

private TreeViewHolder<? > createHolder(NodeModel<? > node) { int type = adapter.getHolderType(node); . / / the node child View created to adapter return adapter. OnCreateViewHolder (this, (NodeModel) node); /** * private void addNodeViewToGroup(NodeModel<? > node) { TreeViewHolder<? > treeViewHolder = createHolder(node); 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); / / to LayoutManager measuring 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) {/ / to the LayoutManager layout 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 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) { ... BaseLine adapterDrawLine = adapter.onDrawLine(drawInfo); if(adapterDrawLine! =null){ adapterDrawLine.draw(drawInfo); }else{ mTreeLayoutManager.performDrawLine(drawInfo); } drawTreeLine(node); }}Copy the code

Achieve free shrink and drag

This part is the core point. At first glance, it seems very simple. It just needs to handle dispaTouchEvent, onInterceptTouchEvent and onTouchEvent. Yes, it’s all handled in these functions, but here are some tricky things to know:

  1. This custom control is to be scaled or moved during onTouchEventMotionEvent.getX()The touch event is also the position of the contact relative to the parent View after scaling, and getRaw is not supported by all SDK versions because it can’t get stable contact data, so it may vibrate when scaling
  2. This tree custom control child node View is also a ViewGroup, at least drag and drop does not affect the child node View control click events
  3. In addition, it should be considered that the regression screen center control, adding and deleting nodes should stabilize the View display of the target node, inverse transformation to obtain the position of View relative to the screen, etc., so as to achieve the touch point following when scaling and dragging

For problem 1, you can add another layer of ViewGroup (GysoTreeView, which is a shell) of the same size to receive touch events, so that since the ViewGroup receiving touch events is stable in size, the intercepted touches should be stable. The treeViewContainer inside is a 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 is used to handle touch events, much like the ViewDragHelper provided by the SDK that determines whether touch events need to be intercepted and handles drop, drag, and inertial sliding. Check if it slipped a little distance. It was that 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, 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.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 node View, we can not use Canvas to move or expand, otherwise the click position will be confused. In addition, Sroller can not be used to control, because scrollTo control will not be recorded in the View transformation Matrix. To facilitate control, instead of using scrollTo, use setTranslationY and setScaleY, which makes it easy to control the whole tree according to the transformation matrix.

For question 3, control pivotX and reverse PivotX, setPivotX(0) so you can easily determine the transformation relationship with x0*scale+translate = x1

mView.setPivotX(0); mView.setPivotY(0); mView.setScaleX(scaleFactor); mView.setScaleY(scaleFactor); 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

Realize add delete node animation

The realization idea is very simple, save the current relative location information of the target node, add and delete nodes, re-measure the location of the layout as the latest location, location change progress with 0->1 percentage

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. While writing code, use view.settag to record data

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

Remeasurement and layout are then triggered according to the normal process. However, do not rush to the screen at this time, according to the original location of the target node on the screen, and the size of the scaling, inverse transformation of the target node will not have a beating feeling.

. if(targetLocationOnViewPortTag instanceof ViewBox){ ViewBox targetLocationOnViewPort=(ViewBox)targetLocationOnViewPortTag; //fix pre size and location Avoid beating float scale = targetLocationOnViewPort. GetWidth () * 1 f/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

In Animate start, you need to restore the position before adding and removing the animation

@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() {// Start animate from relative position to final position; }}); } } /** * 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 - > {/ / according to the relative position first draw the location of the original float thewire = (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 currentLocation and layout 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 the and layout on preLocation position transformation process mTreeModel. DoTraversalNodes (node - > {TreeViewHolder <? > treeViewHolder = treeViewContainer.getTreeViewHolder(node); if (treeViewHolder ! = null) { View view = treeViewHolder.getView(); ViewBox relativeLocation = relativeLocationMap.get(treeViewHolder.getNode()); // Calculate location info Calculate location 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); 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 in final location layout 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(); }}Copy the code

Realization of tree graph regression adaptation screen

This function point is relatively simple, provided that the TreeViewContainer must be centered on (0,0), and the TreeViewContainer cannot be moved or expanded using Canas or srollTo. We just record the scale that fits the screen.

/ / @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.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(); } /** * 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(); }}Copy the code

Drag to edit the tree structure

To drag and edit the tree structure, there are several steps:

  1. Request the parent View not to intercept touch events

  2. The ViewDragHelper implementation captures the View in the TreeViewContainer, recording the original location with all the nodes of the target Node

  3. Drag the target View group

  4. In the process of moving, calculate whether or not a node View was hit, and if so, record the node that was hit

  5. In the release, if there are collision nodes, then go through the process of adding and removing nodes

  6. On release, if there is no collision point, use Scroller to roll back to the initial 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 instanceof View) { parent.requestDisallowInterceptTouchEvent(isEditMode); }}Copy the code

ViewDragHelper is a useful tool class for customizing viewgroups. It provides a useful set of actions and state tracking that allows the user to drag or change the position of the child View within the parent class. Notice, be limited to drag and change position, to put shrink then powerless, just drag edit node this function does not use put shrink nevertheless. And the idea is, again, to determine whether or not you’ve slipped a certain distance, or whether or not you’ve reached a boundary to intercept touch events.

//1 initialize dragHelper = viewDragHelper. create(this, dragCallback); 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; Callback private final viewDragHelper.callback dragCallback = new viewDragHelper.callback (){@override public Boolean tryCaptureView(@nonnull View child, int pointerId) {// Whether to capture dragged View return false; } @ Override public int getViewHorizontalDragRange (@ NonNull View child) {/ / in judging whether the intercept, Return integer. MAX_VALUE; } @override public int getViewVerticalDragRange(@nonnull View child) {return integer.max_value;  } @ Override public int clampViewPositionHorizontal (@ NonNull View child, int left, int dx) {/ / level of mobile location is poor, Return left (left); return left (left); } @ Override public int clampViewPositionVertical (@ NonNull View child, int top, int dy) {/ / vertical mobile location is poor, Return top; return top; return top; } @override public void onViewReleased(@nonnull View releasedChild, float xvel, float yvel) {Copy the code

So when you capture, start recording the location

@override public Boolean tryCaptureView(@nonnull View child, int pointerId) { So use the record to move the block if(isDraggingNodeMode && dragblock.load (child)){child.settag (R.IDit_and_dragging,IS_EDIT_DRAGGING); child.setElevation(Z_SELECT); return true; } return false; }Copy the code

When you drag a bunch of views, because the relative positions of the views are 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. The drag here does not affect the Matrix view.offsetLeftandRight (dx) of the container; view.offsetTopAndBottom(dy); }}Copy the code

As you drag, you need to calculate whether you hit another View

@ Override public int clampViewPositionHorizontal (@ NonNull View child, int left, int dx) {/ / interception return before left instructions not to the boundary can be intercepted, Intercept and return to the original position, indicating that dragHelper is not used to help move, Ourselves to a total target View the if (dragHelper getViewDragState () = = ViewDragHelper. STATE_DRAGGING) {final int oldLeft = child. GetLeft (); dragBlock.drag(dx,0); EstimateToHitTarget (child); invalidate(); return oldLeft; }else{ return left; }} @ Override public int clampViewPositionVertical (@ NonNull View child, int top, int dy) {/ / in accordance with the above code... } private void drawDragBackGround(View View){Object fTag = view.getTag(r.i.d.he_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); }}Copy the code

Release, if there is a target then delete and add, go through the process of delete add; If not, use Scroller assistance for rollback

@override public void onViewReleased(@nonnull View releasedChild, float xvel, float yvel) {treeViewlog. d(TAG, float yvel) "onViewReleased: "); Object fTag = releasedChild.getTag(R.id.the_hit_target); boolean getHit = fTag ! = null; If (getHit){TreeViewHolder<? 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 assist to roll back 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 overridden, @override public void computeScroll() {if(dragBlock.computeScroll()){invalidate(); }}Copy the code

Write in the last

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