github连接: https://links.jianshu.com/go?to=https://github.com/guaishouN/android-tree-view.git
目前没发现比较好的Android树状图开源控件,于是决定自己写一个开源控件,对比了一下市面上关于思维导图或者树状图显示(如xMind,mind master等)的app,本文开源框架并不逊色。实现这个树状图过程中主要综合应用了很多自定义控件关键知识点,比如自定义ViewGroup
的步骤、触摸事件的处理、动画使用、Scroller
及惯性滑动、ViewDragHelper
的使用等等。主要实现了下面几个功能点。
基础--连接线, 布局, 自定义节点View
添加
删除
拖动节点编辑书树状图结构
放缩拖动不影响点击
放缩拖动及适应窗口
下面说明中Animal
类是仅仅用于举例的bean
public class Animal {
public int headId;
public String name;
}
按照以下四个步骤使用该开源控件
1、 通过继承 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;
}
}
2、 配置LayoutManager
。主要设置布局风格(向右展开或垂直向下展开)、父节点与子节点的间隙、子节点间的间隙、节点间的连线(已经实现了直线、光滑曲线、虚线、根状线,也可通过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);
3、 把Adapter
和LayoutManager
设置到你的树状图
. . .
treeView = findViewById(R.id.tree_view);
TreeViewAdapter adapter = new AnimlTreeViewAdapter();
treeView.setAdapter(adapter);
treeView.setTreeLayoutManager(treeLayoutManager);
. . .
4 、设置节点数据
//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);
这里涉及View自定义的基本三部曲onMeasure
、onLayout
、onDraw
或onDispatchDraw
, 其中我把onMeasure
和onLayout
布局的交给了一个特定的类LayoutManager
处理,并且把节点的子View生成及绑定交给Adapter处理,在onDispatchDraw
中画节点的连线也交给Adapter处理。这样可以极大地方便使用者自定义连线及节点View,甚至是自定义LayoutManager
。另外在onSizeChange
中记录控件的大小。
这几个关键点的流程是onMeasure->onLayout->onSizeChanged->onDraw
或onDispatchDraw
private TreeViewHolder<?> createHolder(NodeModel<?> node) {
int type = adapter.getHolderType(node);
...
//node 子View创建交给adapter
return adapter.onCreateViewHolder(this, (NodeModel)node);
}
/**
* 初始化添加NodeView
**/
private void addNodeViewToGroup(NodeModel<?> node) {
TreeViewHolder<?> treeViewHolder = createHolder(node);
//node 子View绑定交给adapter
adapter.onBindViewHolder((TreeViewHolder)treeViewHolder);
...
}
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
TreeViewLog.e(TAG,"onMeasure");
final int size = getChildCount();
for (int i = 0; i < size; i++) {
measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
}
if(MeasureSpec.getSize(widthMeasureSpec)>0 && MeasureSpec.getSize(heightMeasureSpec)>0){
winWidth = MeasureSpec.getSize(widthMeasureSpec);
winHeight = MeasureSpec.getSize(heightMeasureSpec);
}
if (mTreeLayoutManager != null && mTreeModel != null) {
mTreeLayoutManager.setViewport(winHeight,winWidth);
//交给LayoutManager测量
mTreeLayoutManager.performMeasure(this);
ViewBox viewBox = mTreeLayoutManager.getTreeLayoutBox();
drawInfo.setSpace(mTreeLayoutManager.getSpacePeerToPeer(),mTreeLayoutManager.getSpaceParentToChild());
int specWidth = MeasureSpec.makeMeasureSpec(Math.max(winWidth, viewBox.getWidth()), MeasureSpec.EXACTLY);
int specHeight = MeasureSpec.makeMeasureSpec(Math.max(winHeight,viewBox.getHeight()),MeasureSpec.EXACTLY);
setMeasuredDimension(specWidth,specHeight);
}else{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TreeViewLog.e(TAG,"onLayout");
if (mTreeLayoutManager != null && mTreeModel != null) {
//交给LayoutManager布局
mTreeLayoutManager.performLayout(this);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//记录初始大小
viewWidth = w;
viewHeight = h;
drawInfo.setWindowWidth(w);
drawInfo.setWindowHeight(h);
//记录适应窗口的scale
fixWindow();
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mTreeModel != null) {
drawInfo.setCanvas(canvas);
drawTreeLine(mTreeModel.getRootNode());
}
}
/**
* 绘制树形的连线
* @param root root node
*/
private void drawTreeLine(NodeModel<?> root) {
LinkedList<? extends NodeModel<?>> childNodes = root.getChildNodes();
for (NodeModel<?> node : childNodes) {
...
//画连线交给adapter或mTreeLayoutManager处理
BaseLine adapterDrawLine = adapter.onDrawLine(drawInfo);
if(adapterDrawLine!=null){
adapterDrawLine.draw(drawInfo);
}else{
mTreeLayoutManager.performDrawLine(drawInfo);
}
drawTreeLine(node);
}
}
这部分是核心点,乍一看很简单,不就是处理下dispaTouchEvent
、onInterceptTouchEvent
及onTouchEvent
就可以了吗?没错是都是在这几个函数中处理,但是要知道以下这几个难点:
MotionEvent.getX()
拿到的触摸事件也是放缩后触点相对父View的位置,而getRaw
又不是所有SDK版本都支持的,因为不能获取稳定的触点数据,所以可能放缩会出现震动的现象对于问题1,可以再加一层一样大小的ViewGroup
(其实就是GysoTreeView
,它是一个壳)用来接收触摸事件,这样因为这个接收触摸事件的ViewGroup
是大小是稳定的,所以拦截的触摸要是稳定的。里面的treeViewContainer
是真正的树状图ViewGroup容器。
public GysoTreeView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
setClipChildren(false);
setClipToPadding(false);
treeViewContainer = new TreeViewContainer(getContext());
treeViewContainer.setLayoutParams(layoutParams);
addView(treeViewContainer);
treeViewGestureHandler = new TouchEventHandler(getContext(), treeViewContainer);
treeViewGestureHandler.setKeepInViewport(false);
//set animate default
treeViewContainer.setAnimateAdd(true);
treeViewContainer.setAnimateRemove(true);
treeViewContainer.setAnimateMove(true);
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
super.requestDisallowInterceptTouchEvent(disallowIntercept);
this.disallowIntercept = disallowIntercept;
TreeViewLog.e(TAG, "requestDisallowInterceptTouchEvent:"+disallowIntercept);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
TreeViewLog.e(TAG, "onInterceptTouchEvent: "+MotionEvent.actionToString(event.getAction()));
return (!disallowIntercept && treeViewGestureHandler.detectInterceptTouchEvent(event)) || super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
TreeViewLog.e(TAG, "onTouchEvent: "+MotionEvent.actionToString(event.getAction()));
return !disallowIntercept && treeViewGestureHandler.onTouchEvent(event);
}
TouchEventHandler
用来处理触摸事件,有点像SDK提供的ViewDragHelper
判断是否需要拦截触摸事件,并处理放缩、拖动及惯性滑动。判断是不是滑动了一小段距离,是那么拦截
/**
* 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;
}
//如果滑动大于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.8f;
if (scaleFactor >= MAX_SCALE) {
scaleFactor = MAX_SCALE;
scaleState = TreeViewControlListener.MAX_SCALE;
}else if (scaleFactor <= finalMinScale) {
scaleFactor = finalMinScale;
scaleState = TreeViewControlListener.MIN_SCALE;
}
if(controlListener!=null){
int current = (int)(scaleFactor*100);
//just make it no so frequently callback
if(scalePercentOnlyForControlListener!=current){
scalePercentOnlyForControlListener = current;
controlListener.onScaling(scaleState,scalePercentOnlyForControlListener);
}
}
mView.setPivotX(0);
mView.setPivotY(0);
mView.setScaleX(scaleFactor);
mView.setScaleY(scaleFactor);
float tx = postFocusCenter.x-(preFocusCenter.x-preTranslate.x)*scaleFactor / preScaleFactor;
float ty = postFocusCenter.y-(preFocusCenter.y-preTranslate.y)*scaleFactor / preScaleFactor;
mView.setTranslationX(tx);
mView.setTranslationY(ty);
keepWithinBoundaries();
} else if (mode == TOUCH_MODE_SINGLE) {
float deltaX = event.getRawX() - preMovingTouchEvent.getRawX();
float deltaY = event.getRawY() - preMovingTouchEvent.getRawY();
onSinglePointMoving(deltaX, deltaY);
}
break;
case MotionEvent.ACTION_OUTSIDE:
TreeViewLog.e(TAG, "onTouchEvent: touch out side" );
break;
}
preMovingTouchEvent = MotionEvent.obtain(event);
return true;
}
对于问题2,为了不影响节点View的点击事件,我们不能使用Canvas去移送或放缩,否则点击位置会错乱。另外,也不能使用Sroller
去控制,因为scrollTo
滚动控制不会记录在View变换Matrix中,为了方便控制不使用scrollTo
, 而是使用setTranslationY
及setScaleY
, 这样可以很方便根据变换矩阵来控制整个树状图。
对于问题3,控制变换及反变换, setPivotX(0)
这样你可以很方便的通过x0*scale+translate = x1
确定变换关系
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);
实现思路很简单,保存当前相对目标节点位置信息,增删节点后,把重新测量布局的位置作为最新位置,位置变化进度用0->1间的百分比表示
首先,保存当前相对目标节点位置信息,如果是删除则选其父节点作为目标节点,如果是添加节点,那么选添加子节点的父节点作为目标节点,记录这个节点相对屏幕的位置,及这时的放缩比例,并且记录所有其他节点View相对这个目标节点的位置。写代码过程中,使用View.setTag
记录数据
/**
* Prepare moving, adding or removing nodes, record the last one node as an anchor node on view port, so that make it looks smooth change
* Note:The last one will been choose as target node.
* @param nodeModels nodes[nodes.length-1] as the target one
*/
private void recordAnchorLocationOnViewPort(boolean isRemove, NodeModel<?>... nodeModels) {
if(nodeModels==null || nodeModels.length==0){
return;
}
NodeModel<?> targetNode = nodeModels[nodeModels.length-1];
if(targetNode!=null && isRemove){
//if remove, parent will be the target node
Map<NodeModel<?>,View> removeNodeMap = new HashMap<>();
targetNode.selfTraverse(node -> {
removeNodeMap.put(node,getTreeViewHolder(node).getView());
});
setTag(R.id.mark_remove_views,removeNodeMap);
targetNode = targetNode.getParentNode();
}
if(targetNode!=null){
TreeViewHolder<?> targetHolder = getTreeViewHolder(targetNode);
if(targetHolder!=null){
View targetHolderView = targetHolder.getView();
targetHolderView.setElevation(Z_SELECT);
ViewBox targetBox = ViewBox.getViewBox(targetHolderView);
//get target location on view port 相对窗口的位置记录
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);
}
}
}
然后按正常流程触发重新测量、布局。但是这时不要急着画到屏幕,先根据目标节点原来在屏幕的位置,及放缩大小,反变换使目标节点不至于产生跳动的感觉。
...
if(targetLocationOnViewPortTag instanceof ViewBox){
ViewBox targetLocationOnViewPort=(ViewBox)targetLocationOnViewPortTag;
//fix pre size and location 根据目标节点在手机中屏幕的位置重新移动,避免跳动
float scale = targetLocationOnViewPort.getWidth() * 1f / finalLocation.getWidth();
treeViewContainer.setPivotX(0);
treeViewContainer.setPivotY(0);
treeViewContainer.setScaleX(scale);
treeViewContainer.setScaleY(scale);
float dx = targetLocationOnViewPort.left-finalLocation.left*scale;
float dy = targetLocationOnViewPort.top-finalLocation.top*scale;
treeViewContainer.setTranslationX(dx);
treeViewContainer.setTranslationY(dy);
return true;
}
...
最后在Animate的start
中根据相对位置还原添加删除前的位置,0->1变换到最终最新位置
@Override
public void performLayout(final TreeViewContainer treeViewContainer) {
final TreeModel<?> mTreeModel = treeViewContainer.getTreeModel();
if (mTreeModel != null) {
mTreeModel.doTraversalNodes(new ITraversal<NodeModel<?>>() {
@Override
public void next(NodeModel<?> next) {
layoutNodes(next, treeViewContainer);
}
@Override
public void finish() {
//布局位置确定完后,开始通过动画从相对位置移动到最终位置
layoutAnimate(treeViewContainer);
}
});
}
}
/**
* For layout animator
* @param treeViewContainer container
*/
protected void layoutAnimate(TreeViewContainer treeViewContainer) {
TreeModel<?> mTreeModel = treeViewContainer.getTreeModel();
//means that smooth move from preLocation to curLocation
Object nodeTag = treeViewContainer.getTag(R.id.target_node);
Object targetNodeLocationTag = treeViewContainer.getTag(R.id.target_node_final_location);
Object relativeLocationMapTag = treeViewContainer.getTag(R.id.relative_locations);
Object animatorTag = treeViewContainer.getTag(R.id.node_trans_animator);
if(animatorTag instanceof ValueAnimator){
((ValueAnimator)animatorTag).end();
}
if (nodeTag instanceof NodeModel
&& targetNodeLocationTag instanceof ViewBox
&& relativeLocationMapTag instanceof Map) {
ViewBox targetNodeLocation = (ViewBox) targetNodeLocationTag;
Map<NodeModel<?>,ViewBox> relativeLocationMap = (Map<NodeModel<?>,ViewBox>)relativeLocationMapTag;
AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f);
valueAnimator.setDuration(TreeViewContainer.DEFAULT_FOCUS_DURATION);
valueAnimator.setInterpolator(interpolator);
valueAnimator.addUpdateListener(value -> {
//先根据相对位置画出原来的位置
float ratio = (float) value.getAnimatedValue();
TreeViewLog.e(TAG, "valueAnimator update ratio[" + ratio + "]");
mTreeModel.doTraversalNodes(node -> {
TreeViewHolder<?> treeViewHolder = treeViewContainer.getTreeViewHolder(node);
if (treeViewHolder != null) {
View view = treeViewHolder.getView();
ViewBox preLocation = (ViewBox) view.getTag(R.id.node_pre_location);
ViewBox deltaLocation = (ViewBox) view.getTag(R.id.node_delta_location);
if(preLocation !=null && deltaLocation!=null){
//calculate current location 计算渐变位置 并 布局
ViewBox currentLocation = preLocation.add(deltaLocation.multiply(ratio));
view.layout(currentLocation.left,
currentLocation.top,
currentLocation.left+view.getMeasuredWidth(),
currentLocation.top+view.getMeasuredHeight());
}
}
});
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation, boolean isReverse) {
TreeViewLog.e(TAG, "onAnimationStart ");
//calculate and layout on preLocation 位置变换过程
mTreeModel.doTraversalNodes(node -> {
TreeViewHolder<?> treeViewHolder = treeViewContainer.getTreeViewHolder(node);
if (treeViewHolder != null) {
View view = treeViewHolder.getView();
ViewBox relativeLocation = relativeLocationMap.get(treeViewHolder.getNode());
//calculate location info 计算位置
ViewBox preLocation = targetNodeLocation.add(relativeLocation);
ViewBox finalLocation = (ViewBox) view.getTag(R.id.node_final_location);
if(preLocation==null || finalLocation==null){
return;
}
ViewBox deltaLocation = finalLocation.subtract(preLocation);
//save as tag
view.setTag(R.id.node_pre_location, preLocation);
view.setTag(R.id.node_delta_location, deltaLocation);
//layout on preLocation 更新布局
view.layout(preLocation.left, preLocation.top, preLocation.left+view.getMeasuredWidth(), preLocation.top+view.getMeasuredHeight());
}
});
}
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
...
//layout on finalLocation 在布局最终位置
mTreeModel.doTraversalNodes(node -> {
TreeViewHolder<?> treeViewHolder = treeViewContainer.getTreeViewHolder(node);
if (treeViewHolder != null) {
View view = treeViewHolder.getView();
ViewBox finalLocation = (ViewBox) view.getTag(R.id.node_final_location);
if(finalLocation!=null){
view.layout(finalLocation.left, finalLocation.top, finalLocation.right, finalLocation.bottom);
}
view.setTag(R.id.node_pre_location,null);
view.setTag(R.id.node_delta_location,null);
view.setTag(R.id.node_final_location, null);
view.setElevation(TreeViewContainer.Z_NOR);
}
});
}
});
treeViewContainer.setTag(R.id.node_trans_animator,valueAnimator);
valueAnimator.start();
}
}
这个功能点相对简单,前提是TreeViewContainer
放缩一定要以(0,0)
为中心点,并且TreeViewContainer
的移动放缩不是使用Canas或srollTo
操作,这样在onSizeChange
中,我们记录适配屏幕的scale就行了。
/**
*记录
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
TreeViewLog.e(TAG,"onSizeChanged w["+w+"]h["+h+"]oldw["+oldw+"]oldh["+oldh+"]");
viewWidth = w;
viewHeight = h;
drawInfo.setWindowWidth(w);
drawInfo.setWindowHeight(h);
fixWindow();
}
/**
* fix view tree
*/
private void fixWindow() {
float scale;
float hr = 1f*viewHeight/winHeight;
float wr = 1f*viewWidth/winWidth;
scale = Math.max(hr, wr);
minScale = 1f/scale;
if(Math.abs(scale-1)>0.01f){
//setPivotX((winWidth*scale-viewWidth)/(2*(scale-1)));
//setPivotY((winHeight*scale-viewHeight)/(2*(scale-1)));
setPivotX(0);
setPivotY(0);
setScaleX(1f/scale);
setScaleY(1f/scale);
}
//when first init
if(centerMatrix==null){
centerMatrix = new Matrix();
}
centerMatrix.set(getMatrix());
float[] values = new float[9];
centerMatrix.getValues(values);
values[Matrix.MTRANS_X]=0f;
values[Matrix.MTRANS_Y]=0f;
centerMatrix.setValues(values);
setTouchDelegate();
}
/**
*恢复
*/
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();
}
}
想要拖动编辑树状图结构要有如下几个步骤:
TreeViewContainer
中使用ViewDragHelper
实现捕获View,以目标Node的所有Node一并记录原始位置请求父View不要拦截触摸事件, 这个不要搞混了,是parent.requestDisallowInterceptTouchEvent(isEditMode);
而不是直接requestDisallowInterceptTouchEvent
protected void requestMoveNodeByDragging(boolean isEditMode) {
this.isDraggingNodeMode = isEditMode;
ViewParent parent = getParent();
if (parent instanceof View) {
parent.requestDisallowInterceptTouchEvent(isEditMode);
}
}
这里简单说一下ViewDragHelper
的使用, 官方说ViewDragHelper
是在自定义ViewGroup时非常有用的工具类。它提供了一系列有用的操作及状态跟踪使用户可以在父类的中拖动或改变子View的位置。注重, 限于拖动及改变位置,对于放缩那就无能为力了, 不过刚好拖动编辑节点这个功能不使用放缩。它的原理也是,判断有没滑动一定距离,或者是否到达了边界来拦截触摸事件。
//1 初始化
dragHelper = ViewDragHelper.create(this, dragCallback);
//2 判断拦截及处理onTouchEvent
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercept = dragHelper.shouldInterceptTouchEvent(event);
TreeViewLog.e(TAG, "onInterceptTouchEvent: "+MotionEvent.actionToString(event.getAction())+" intercept:"+intercept);
return isDraggingNodeMode && intercept;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
TreeViewLog.e(TAG, "onTouchEvent: "+MotionEvent.actionToString(event.getAction()));
if(isDraggingNodeMode) {
dragHelper.processTouchEvent(event);
}
return isDraggingNodeMode;
}
//3 实现Callback
private final ViewDragHelper.Callback dragCallback = new ViewDragHelper.Callback(){
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
//是否捕获拖动的View
return false;
}
@Override
public int getViewHorizontalDragRange(@NonNull View child) {
//在判断是否拦截时,判断是否超出水平移动范围
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) {
//水平移动位置差,返回希望移动后的位置
//特别注意在拦截阶段 返回left与原来一样,说明到达边界,不拦截
return left;
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
//垂直移动位置差,返回希望移动后的位置
//特别注意在拦截阶段 返回left与原来一样,说明到达边界,不拦截
return top;
}
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
//释放捕获的View
}
};
那么捕获时,开始记录位置
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
//如果是拖动编辑功能,那么使用记录要移动的块
if(isDraggingNodeMode && dragBlock.load(child)){
child.setTag(R.id.edit_and_dragging,IS_EDIT_DRAGGING);
child.setElevation(Z_SELECT);
return true;
}
return false;
}
拖动一组View时,因为这组View的相对位置是不变的,所以可以都是无论是垂直方向还是水平方向都使用同一个dx
,dy
public void drag(int dx, int dy){
if(!mScroller.isFinished()){
return;
}
this.isDragging = true;
for (int i = 0; i < tmp.size(); i++) {
View view = tmp.get(i);
//offset变化的是布局,不是变换矩阵。而这里拖动没有影响container的Matrix
view.offsetLeftAndRight(dx);
view.offsetTopAndBottom(dy);
}
}
拖动过程中,要计算是否碰撞到其他View
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
//拦截前返回left说明没有到边界可以拦截, 拦截后返回原来位置,说明不用dragHelper来帮忙移动,我们自己来一共目标View
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) {
//与上面代码一致
...
}
//如果撞击了,那么invalidate,画撞击提醒
private void drawDragBackGround(View view){
Object fTag = view.getTag(R.id.the_hit_target);
boolean getHit = fTag != null;
if(getHit){
//draw
.....
mPaint.reset();
mPaint.setColor(Color.parseColor("#4FF1286C"));
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
PointF centerPoint = getCenterPoint(view);
drawInfo.getCanvas().drawCircle(centerPoint.x,centerPoint.y,(float)fR,mPaint);
PointPool.free(centerPoint);
}
}
释放时,如果有目标那么删除再添加,走删除添加流程;如果没有,那么使用Scroller
协助回滚
//释放
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
TreeViewLog.d(TAG, "onViewReleased: ");
Object fTag = releasedChild.getTag(R.id.the_hit_target);
boolean getHit = fTag != null;
//如果及记录了撞击点,删除再添加,走删除添加流程
if(getHit){
TreeViewHolder<?> targetHolder = getTreeViewHolder((NodeModel)fTag);
NodeModel<?> targetHolderNode = targetHolder.getNode();
TreeViewHolder<?> releasedChildHolder = (TreeViewHolder<?>)releasedChild.getTag(R.id.item_holder);
NodeModel<?> releasedChildHolderNode = releasedChildHolder.getNode();
if(releasedChildHolderNode.getParentNode()!=null){
mTreeModel.removeNode(releasedChildHolderNode.getParentNode(),releasedChildHolderNode);
}
mTreeModel.addNode(targetHolderNode,releasedChildHolderNode);
mTreeModel.calculateTreeNodesDeep();
if(isAnimateMove()){
recordAnchorLocationOnViewPort(false,targetHolderNode);
}
requestLayout();
}else{
//recover 如果没有,那么使用Scroller协助回滚
dragBlock.smoothRecover(releasedChild);
}
dragBlock.setDragging(false);
releasedChild.setElevation(Z_NOR);
releasedChild.setTag(R.id.edit_and_dragging,null);
releasedChild.setTag(R.id.the_hit_target, null);
invalidate();
}
//注意重写container的computeScroll,实现更新
@Override
public void computeScroll() {
if(dragBlock.computeScroll()){
invalidate();
}
}
到到这里就介绍完,整个树状节点图的拖动放缩,添加删除节点,拖动编辑等这几个功能的实现原理了,当然里面还有很多实现细节。你可以把这篇文章作为源码查看的引导,细节方面也还有很多待完善的地方。后面这个开源应该会继续更新,大家也可以一起探讨,fork出来一起改。如果觉得不错请给个星呢。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/GkcowlG5q0_IyxEQoDyQSg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。