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:
- This custom control is to be scaled or moved during the onTouchEvent
MotionEvent.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 - This tree custom control child View is also a ViewGroup, at least drag and shrink does not affect the child View control click event
- 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:
-
Ask the parent View not to intercept touch events
-
Use the ViewDragHelper in the TreeViewContainer to capture the View and record the original location for all nodes of the target Node
-
Drag the target View group
-
During the movement, calculate whether or not you hit a node View, and if so, record the node that you hit
-
When releasing, if there are collision nodes, then go through the process of adding and deleting nodes
-
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.