Tree View; Mind map; Think map; tree map; 樹狀圖武鲁;思維導(dǎo)圖睁本;組織結(jié)構(gòu)圖掐禁;層次圖
文章目錄
[1 簡(jiǎn)介](# 簡(jiǎn)介)
[2 效果展示](# 效果展示)
[3 使用步驟](# 使用步驟)
[4 實(shí)現(xiàn)基本布局流程](# 實(shí)現(xiàn)基本布局流程)
[5 實(shí)現(xiàn)自由放縮及拖動(dòng)](# 實(shí)現(xiàn)自由放縮及拖動(dòng))
[6 實(shí)現(xiàn)添加刪除及節(jié)點(diǎn)動(dòng)畫](# 實(shí)現(xiàn)添加刪除及節(jié)點(diǎn)動(dòng)畫)
[7 實(shí)現(xiàn)樹狀圖的回歸適應(yīng)屏幕](# 實(shí)現(xiàn)樹狀圖的回歸適應(yīng)屏幕)
[8 實(shí)現(xiàn)拖到編輯樹狀圖結(jié)構(gòu)](# 實(shí)現(xiàn)拖到編輯樹狀圖結(jié)構(gòu))
[9 寫在最后](# 寫在最后)
簡(jiǎn)介
github連接: https://github.com/guaishouN/android-tree-view.git
目前沒發(fā)現(xiàn)比較好的Android樹狀圖開源控件脂倦,于是決定自己寫一個(gè)開源控件厂汗,對(duì)比了一下市面上關(guān)于思維導(dǎo)圖或者樹狀圖顯示(如xMind业崖,mind master等)的app野芒,本文開源框架并不遜色。實(shí)現(xiàn)這個(gè)樹狀圖過程中主要綜合應(yīng)用了很多自定義控件關(guān)鍵知識(shí)點(diǎn)双炕,比如自定義ViewGroup的步驟狞悲、觸摸事件的處理、動(dòng)畫使用妇斤、Scroller及慣性滑動(dòng)摇锋、ViewDragHelper的使用等等。主要實(shí)現(xiàn)了下面幾個(gè)功能點(diǎn)站超。
絲滑的跟隨手指放縮荸恕,拖動(dòng),及慣性滑動(dòng)
自動(dòng)動(dòng)畫回歸屏幕中心
支持子節(jié)點(diǎn)復(fù)雜布局自定義死相,并且節(jié)點(diǎn)布局點(diǎn)擊事件與滑動(dòng)不沖突
節(jié)點(diǎn)間的連接線自定義
可刪除動(dòng)態(tài)節(jié)點(diǎn)
可動(dòng)態(tài)添加節(jié)點(diǎn)
支持拖動(dòng)調(diào)整節(jié)點(diǎn)關(guān)系
增刪融求、移動(dòng)結(jié)構(gòu)添加動(dòng)畫效果
效果展示
基礎(chǔ)--連接線, 布局, 自定義節(jié)點(diǎn)View
添加
刪除
拖動(dòng)節(jié)點(diǎn)編輯書樹狀圖結(jié)構(gòu)
放縮拖動(dòng)不影響點(diǎn)擊
放縮拖動(dòng)及適應(yīng)窗口
使用步驟
下面說明中Animal類是僅僅用于舉例的bean
public class Animal {
public int headId;
public String name;
}
按照以下四個(gè)步驟使用該開源控件
1 通過繼承 TreeViewAdapter實(shí)現(xiàn)節(jié)點(diǎn)數(shù)據(jù)與節(jié)點(diǎn)視圖的綁定
public class AnimalTreeViewAdapter extends TreeViewAdapter<Animal> {
private DashLine dashLine = new DashLine(Color.parseColor("#F06292"),6);
@Override
public TreeViewHolder<Animal> onCreateViewHolder(@NonNull ViewGroup viewGroup, NodeModel<Animal> node) {
//TODO in inflate item view
NodeBaseLayoutBinding nodeBinding = NodeBaseLayoutBinding.inflate(LayoutInflater.from(viewGroup.getContext()),viewGroup,false);
return new TreeViewHolder<>(nodeBinding.getRoot(),node);
}
@Override
public void onBindViewHolder(@NonNull TreeViewHolder<Animal> holder) {
//TODO get view and node from holder, and then control your item view
View itemView = holder.getView();
NodeModel<Animal> node = holder.getNode();
...
}
@Override
public Baseline onDrawLine(DrawInfo drawInfo) {
// TODO If you return an BaseLine, line will be draw by the return one instead of TreeViewLayoutManager's
// if(...){
// ...
// return dashLine;
// }
return null;
}
}
2 配置LayoutManager。主要設(shè)置布局風(fēng)格(向右展開或垂直向下展開)算撮、父節(jié)點(diǎn)與子節(jié)點(diǎn)的間隙生宛、子節(jié)點(diǎn)間的間隙、節(jié)點(diǎn)間的連線(已經(jīng)實(shí)現(xiàn)了直線肮柜、光滑曲線、虛線审洞、根狀線,也可通過BaseLine實(shí)現(xiàn)你自己的連線)
int space_50dp = 50;
int space_20dp = 20;
//choose a demo line or a customs line. StraightLine, PointedLine, DashLine, SmoothLine are available.
Baseline line = new DashLine(Color.parseColor("#4DB6AC"),8);
//choose layoout manager. VerticalTreeLayoutManager,RightTreeLayoutManager are available.
TreeLayoutManager treeLayoutManager = new RightTreeLayoutManager(this,space_50dp,space_20dp,line);
3 把Adapter和LayoutManager設(shè)置到你的樹狀圖
...
treeView = findViewById(R.id.tree_view);
TreeViewAdapter adapter = new AnimlTreeViewAdapter();
treeView.setAdapter(adapter);
treeView.setTreeLayoutManager(treeLayoutManager);
...
4 設(shè)置節(jié)點(diǎn)數(shù)據(jù)
//Create a TreeModel by using a root node.
NodeModel<Animal> node0 = new NodeModel<>(new Animal(R.drawable.ic_01,"root"));
TreeModel<Animal> treeModel = new TreeModel<>(root);
//Other nodes.
NodeModel<Animal> node1 = new NodeModel<>(new Animal(R.drawable.ic_02,"sub0"));
NodeModel<Animal> node2 = new NodeModel<>(new Animal(R.drawable.ic_03,"sub1"));
NodeModel<Animal> node3 = new NodeModel<>(new Animal(R.drawable.ic_04,"sub2"));
NodeModel<Animal> node4 = new NodeModel<>(new Animal(R.drawable.ic_05,"sub3"));
NodeModel<Animal> node5 = new NodeModel<>(new Animal(R.drawable.ic_06,"sub4"));
//Build the relationship between parent node and childs,like:
//treeModel.add(parent, child1, child2, ...., childN);
treeModel.add(node0, node1, node2);
treeModel.add(node1, node3, node4);
treeModel.add(node2, node5);
//finally set this treeModel to the adapter
adapter.setTreeModel(treeModel);
實(shí)現(xiàn)基本的布局流程
這里涉及View自定義的基本三部曲onMeasure
耙箍、onLayout
、onDraw
或onDispatchDraw
, 其中我把onMeasure
和onLayout
布局的交給了一個(gè)特定的類LayoutManager處理辩昆,并且把節(jié)點(diǎn)的子View生成及綁定交給Adapter處理旨袒,在onDispatchDraw
中畫節(jié)點(diǎn)的連線也交給Adapter處理。這樣可以極大地方便使用者自定義連線及節(jié)點(diǎn)View砚尽,甚至是自定義LayoutManager施无。另外在onSizeChange
中記錄控件的大小。
這幾個(gè)關(guān)鍵點(diǎn)的流程是onMeasure
->onLayout
->onSizeChanged
->onDraw
或onDispatchDraw
private TreeViewHolder<?> createHolder(NodeModel<?> node) {
int type = adapter.getHolderType(node);
...
//node 子View創(chuàng)建交給adapter
return adapter.onCreateViewHolder(this, (NodeModel)node);
}
/**
* 初始化添加NodeView
**/
private void addNodeViewToGroup(NodeModel<?> node) {
TreeViewHolder<?> treeViewHolder = createHolder(node);
//node 子View綁定交給adapter
adapter.onBindViewHolder((TreeViewHolder)treeViewHolder);
...
}
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
TreeViewLog.e(TAG,"onMeasure");
final int size = getChildCount();
for (int i = 0; i < size; i++) {
measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
}
if(MeasureSpec.getSize(widthMeasureSpec)>0 && MeasureSpec.getSize(heightMeasureSpec)>0){
winWidth = MeasureSpec.getSize(widthMeasureSpec);
winHeight = MeasureSpec.getSize(heightMeasureSpec);
}
if (mTreeLayoutManager != null && mTreeModel != null) {
mTreeLayoutManager.setViewport(winHeight,winWidth);
//交給LayoutManager測(cè)量
mTreeLayoutManager.performMeasure(this);
ViewBox viewBox = mTreeLayoutManager.getTreeLayoutBox();
drawInfo.setSpace(mTreeLayoutManager.getSpacePeerToPeer(),mTreeLayoutManager.getSpaceParentToChild());
int specWidth = MeasureSpec.makeMeasureSpec(Math.max(winWidth, viewBox.getWidth()), MeasureSpec.EXACTLY);
int specHeight = MeasureSpec.makeMeasureSpec(Math.max(winHeight,viewBox.getHeight()),MeasureSpec.EXACTLY);
setMeasuredDimension(specWidth,specHeight);
}else{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TreeViewLog.e(TAG,"onLayout");
if (mTreeLayoutManager != null && mTreeModel != null) {
//交給LayoutManager布局
mTreeLayoutManager.performLayout(this);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//記錄初始大小
viewWidth = w;
viewHeight = h;
drawInfo.setWindowWidth(w);
drawInfo.setWindowHeight(h);
//記錄適應(yīng)窗口的scale
fixWindow();
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mTreeModel != null) {
drawInfo.setCanvas(canvas);
drawTreeLine(mTreeModel.getRootNode());
}
}
/**
* 繪制樹形的連線
* @param root root node
*/
private void drawTreeLine(NodeModel<?> root) {
LinkedList<? extends NodeModel<?>> childNodes = root.getChildNodes();
for (NodeModel<?> node : childNodes) {
...
//畫連線交給adapter或mTreeLayoutManager處理
BaseLine adapterDrawLine = adapter.onDrawLine(drawInfo);
if(adapterDrawLine!=null){
adapterDrawLine.draw(drawInfo);
}else{
mTreeLayoutManager.performDrawLine(drawInfo);
}
drawTreeLine(node);
}
}
實(shí)現(xiàn)自由放縮及拖動(dòng)
這部分是核心點(diǎn)必孤,乍一看很簡(jiǎn)單猾骡,不就是處理下dispaTouchEvent
、onInterceptTouchEvent
及onTouchEvent
就可以了嗎敷搪?沒錯(cuò)是都是在這幾個(gè)函數(shù)中處理兴想,但是要知道以下這幾個(gè)難點(diǎn):
- 這個(gè)自定義控件要放縮或移動(dòng)過程中,通過onTouchEvent中
MotionEvent.getX()
拿到的觸摸事件也是放縮后觸點(diǎn)相對(duì)父View的位置赡勘,而getRaw又不是所有SDK版本都支持的嫂便,因?yàn)椴荒塬@取穩(wěn)定的觸點(diǎn)數(shù)據(jù),所以可能放縮會(huì)出現(xiàn)震動(dòng)的現(xiàn)象 - 這個(gè)樹狀圖自定義控件子節(jié)點(diǎn)View也是ViewGroup闸与,至少拖動(dòng)放縮不能影響子節(jié)點(diǎn)View里的控件點(diǎn)擊事件
- 另外還要考慮毙替,回歸屏幕中心控制、增刪節(jié)點(diǎn)要穩(wěn)定目標(biāo)節(jié)點(diǎn)View顯示践樱、反變換獲取View相對(duì)屏幕位置等, 實(shí)現(xiàn)放縮及拖動(dòng)時(shí)的觸點(diǎn)跟隨
對(duì)于問題1厂画,可以再加一層一樣大小的ViewGroup(其實(shí)就是GysoTreeView,它是一個(gè)殼)用來接收觸摸事件拷邢,這樣因?yàn)檫@個(gè)接收觸摸事件的ViewGroup是大小是穩(wěn)定的袱院,所以攔截的觸摸要是穩(wěn)定的。里面的treeViewContainer是真正的樹狀圖ViewGroup容器解孙。
public GysoTreeView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
setClipChildren(false);
setClipToPadding(false);
treeViewContainer = new TreeViewContainer(getContext());
treeViewContainer.setLayoutParams(layoutParams);
addView(treeViewContainer);
treeViewGestureHandler = new TouchEventHandler(getContext(), treeViewContainer);
treeViewGestureHandler.setKeepInViewport(false);
//set animate default
treeViewContainer.setAnimateAdd(true);
treeViewContainer.setAnimateRemove(true);
treeViewContainer.setAnimateMove(true);
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
super.requestDisallowInterceptTouchEvent(disallowIntercept);
this.disallowIntercept = disallowIntercept;
TreeViewLog.e(TAG, "requestDisallowInterceptTouchEvent:"+disallowIntercept);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
TreeViewLog.e(TAG, "onInterceptTouchEvent: "+MotionEvent.actionToString(event.getAction()));
return (!disallowIntercept && treeViewGestureHandler.detectInterceptTouchEvent(event)) || super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
TreeViewLog.e(TAG, "onTouchEvent: "+MotionEvent.actionToString(event.getAction()));
return !disallowIntercept && treeViewGestureHandler.onTouchEvent(event);
}
TouchEventHandler
用來處理觸摸事件抛人,有點(diǎn)像SDK提供的ViewDragHelper
判斷是否需要攔截觸摸事件,并處理放縮廷臼、拖動(dòng)及慣性滑動(dòng)。判斷是不是滑動(dòng)了一小段距離寂恬,是那么攔截
/**
* to detect whether should intercept the touch event
* @param event event
* @return true for intercept
*/
public boolean detectInterceptTouchEvent(MotionEvent event){
final int action = event.getAction() & MotionEvent.ACTION_MASK;
onTouchEvent(event);
if (action == MotionEvent.ACTION_DOWN){
preInterceptTouchEvent = MotionEvent.obtain(event);
mIsMoving = false;
}
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mIsMoving = false;
}
//如果滑動(dòng)大于mTouchSlop初肉,則觸發(fā)攔截
if(action == MotionEvent.ACTION_MOVE && mTouchSlop < calculateMoveDistance(event, preInterceptTouchEvent)){
mIsMoving = true;
}
return mIsMoving;
}
/**
* handler the touch event, drag and scale
* @param event touch event
* @return true for has consume
*/
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
//Log.e(TAG, "onTouchEvent:"+event);
int action = event.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN:
mode = TOUCH_MODE_SINGLE;
preMovingTouchEvent = MotionEvent.obtain(event);
if(mView instanceof TreeViewContainer){
minScale = ((TreeViewContainer)mView).getMinScale();
}
if(flingX!=null){
flingX.cancel();
}
if(flingY!=null){
flingY.cancel();
}
break;
case MotionEvent.ACTION_UP:
mode = TOUCH_MODE_RELEASE;
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_CANCEL:
mode = TOUCH_MODE_UNSET;
break;
case MotionEvent.ACTION_POINTER_DOWN:
mode++;
if (mode >= TOUCH_MODE_DOUBLE){
scaleFactor = preScaleFactor = mView.getScaleX();
preTranslate.set( mView.getTranslationX(),mView.getTranslationY());
scaleBaseR = (float) distanceBetweenFingers(event);
centerPointBetweenFingers(event,preFocusCenter);
centerPointBetweenFingers(event,postFocusCenter);
}
break;
case MotionEvent.ACTION_MOVE:
if (mode >= TOUCH_MODE_DOUBLE) {
float scaleNewR = (float) distanceBetweenFingers(event);
centerPointBetweenFingers(event,postFocusCenter);
if (scaleBaseR <= 0){
break;
}
scaleFactor = (scaleNewR / scaleBaseR) * preScaleFactor * 0.15f + scaleFactor * 0.85f;
int scaleState = TreeViewControlListener.FREE_SCALE;
float finalMinScale = isKeepInViewport?minScale:minScale*0.8f;
if (scaleFactor >= MAX_SCALE) {
scaleFactor = MAX_SCALE;
scaleState = TreeViewControlListener.MAX_SCALE;
}else if (scaleFactor <= finalMinScale) {
scaleFactor = finalMinScale;
scaleState = TreeViewControlListener.MIN_SCALE;
}
if(controlListener!=null){
int current = (int)(scaleFactor*100);
//just make it no so frequently callback
if(scalePercentOnlyForControlListener!=current){
scalePercentOnlyForControlListener = current;
controlListener.onScaling(scaleState,scalePercentOnlyForControlListener);
}
}
mView.setPivotX(0);
mView.setPivotY(0);
mView.setScaleX(scaleFactor);
mView.setScaleY(scaleFactor);
float tx = postFocusCenter.x-(preFocusCenter.x-preTranslate.x)*scaleFactor / preScaleFactor;
float ty = postFocusCenter.y-(preFocusCenter.y-preTranslate.y)*scaleFactor / preScaleFactor;
mView.setTranslationX(tx);
mView.setTranslationY(ty);
keepWithinBoundaries();
} else if (mode == TOUCH_MODE_SINGLE) {
float deltaX = event.getRawX() - preMovingTouchEvent.getRawX();
float deltaY = event.getRawY() - preMovingTouchEvent.getRawY();
onSinglePointMoving(deltaX, deltaY);
}
break;
case MotionEvent.ACTION_OUTSIDE:
TreeViewLog.e(TAG, "onTouchEvent: touch out side" );
break;
}
preMovingTouchEvent = MotionEvent.obtain(event);
return true;
}
對(duì)于問題2,為了不影響節(jié)點(diǎn)View的點(diǎn)擊事件妄壶,我們不能使用Canvas去移送或放縮寄狼,否則點(diǎn)擊位置會(huì)錯(cuò)亂泊愧。另外,也不能使用Sroller去控制奢浑,因?yàn)?code>scrollTo滾動(dòng)控制不會(huì)記錄在View變換Matrix中雀彼,為了方便控制不使用scrollTo
, 而是使用setTranslationY
及setScaleY
, 這樣可以很方便根據(jù)變換矩陣來控制整個(gè)樹狀圖即寡。
對(duì)于問題3,控制變換及反變換, setPivotX(0)
這樣你可以很方便的通過x0*scale+translate = x1確定變換關(guān)系
mView.setPivotX(0);
mView.setPivotY(0);
mView.setScaleX(scaleFactor);
mView.setScaleY(scaleFactor);
//觸點(diǎn)跟隨
float tx = postFocusCenter.x-(preFocusCenter.x-preTranslate.x)*scaleFactor / preScaleFactor;
float ty = postFocusCenter.y-(preFocusCenter.y-preTranslate.y)*scaleFactor / preScaleFactor;
mView.setTranslationX(tx);
mView.setTranslationY(ty);
實(shí)現(xiàn)添加刪除節(jié)點(diǎn)動(dòng)畫
實(shí)現(xiàn)思路很簡(jiǎn)單莺丑,保存當(dāng)前相對(duì)目標(biāo)節(jié)點(diǎn)位置信息梢莽,增刪節(jié)點(diǎn)后昏名,把重新測(cè)量布局的位置作為最新位置阵面,位置變化進(jìn)度用0->1間的百分比表示
首先洪鸭,保存當(dāng)前相對(duì)目標(biāo)節(jié)點(diǎn)位置信息览爵,如果是刪除則選其父節(jié)點(diǎn)作為目標(biāo)節(jié)點(diǎn)蜓竹,如果是添加節(jié)點(diǎn)储藐,那么選添加子節(jié)點(diǎn)的父節(jié)點(diǎn)作為目標(biāo)節(jié)點(diǎn)邑茄,記錄這個(gè)節(jié)點(diǎn)相對(duì)屏幕的位置,及這時(shí)的放縮比例左医,并且記錄所有其他節(jié)點(diǎn)View相對(duì)這個(gè)目標(biāo)節(jié)點(diǎn)的位置浮梢。寫代碼過程中彤路,使用View.setTag記錄數(shù)據(jù)
/**
* Prepare moving, adding or removing nodes, record the last one node as an anchor node on view port, so that make it looks smooth change
* Note:The last one will been choose as target node.
* @param nodeModels nodes[nodes.length-1] as the target one
*/
private void recordAnchorLocationOnViewPort(boolean isRemove, NodeModel<?>... nodeModels) {
if(nodeModels==null || nodeModels.length==0){
return;
}
NodeModel<?> targetNode = nodeModels[nodeModels.length-1];
if(targetNode!=null && isRemove){
//if remove, parent will be the target node
Map<NodeModel<?>,View> removeNodeMap = new HashMap<>();
targetNode.selfTraverse(node -> {
removeNodeMap.put(node,getTreeViewHolder(node).getView());
});
setTag(R.id.mark_remove_views,removeNodeMap);
targetNode = targetNode.getParentNode();
}
if(targetNode!=null){
TreeViewHolder<?> targetHolder = getTreeViewHolder(targetNode);
if(targetHolder!=null){
View targetHolderView = targetHolder.getView();
targetHolderView.setElevation(Z_SELECT);
ViewBox targetBox = ViewBox.getViewBox(targetHolderView);
//get target location on view port 相對(duì)窗口的位置記錄
ViewBox targetBoxOnViewport = targetBox.convert(getMatrix());
setTag(R.id.target_node,targetNode);
setTag(R.id.target_location_on_viewport,targetBoxOnViewport);
//The relative locations of other nodes 相對(duì)位置記錄
Map<NodeModel<?>,ViewBox> relativeLocationMap = new HashMap<>();
mTreeModel.doTraversalNodes(node->{
TreeViewHolder<?> oneHolder = getTreeViewHolder(node);
ViewBox relativeBox =
oneHolder!=null?
ViewBox.getViewBox(oneHolder.getView()).subtract(targetBox):
new ViewBox();
relativeLocationMap.put(node,relativeBox);
});
setTag(R.id.relative_locations,relativeLocationMap);
}
}
}
然后按正常流程觸發(fā)重新測(cè)量远豺、布局坞嘀。但是這時(shí)不要急著畫到屏幕,先根據(jù)目標(biāo)節(jié)點(diǎn)原來在屏幕的位置棺滞,及放縮大小继准,反變換使目標(biāo)節(jié)點(diǎn)不至于產(chǎn)生跳動(dòng)的感覺矮男。
...
if(targetLocationOnViewPortTag instanceof ViewBox){
ViewBox targetLocationOnViewPort=(ViewBox)targetLocationOnViewPortTag;
//fix pre size and location 根據(jù)目標(biāo)節(jié)點(diǎn)在手機(jī)中屏幕的位置重新移動(dòng)昂灵,避免跳動(dòng)
float scale = targetLocationOnViewPort.getWidth() * 1f / finalLocation.getWidth();
treeViewContainer.setPivotX(0);
treeViewContainer.setPivotY(0);
treeViewContainer.setScaleX(scale);
treeViewContainer.setScaleY(scale);
float dx = targetLocationOnViewPort.left-finalLocation.left*scale;
float dy = targetLocationOnViewPort.top-finalLocation.top*scale;
treeViewContainer.setTranslationX(dx);
treeViewContainer.setTranslationY(dy);
return true;
}
...
最后在Animate的start中根據(jù)相對(duì)位置還原添加刪除前的位置舞萄,0->1變換到最終最新位置
@Override
public void performLayout(final TreeViewContainer treeViewContainer) {
final TreeModel<?> mTreeModel = treeViewContainer.getTreeModel();
if (mTreeModel != null) {
mTreeModel.doTraversalNodes(new ITraversal<NodeModel<?>>() {
@Override
public void next(NodeModel<?> next) {
layoutNodes(next, treeViewContainer);
}
@Override
public void finish() {
//布局位置確定完后,開始通過動(dòng)畫從相對(duì)位置移動(dòng)到最終位置
layoutAnimate(treeViewContainer);
}
});
}
}
/**
* For layout animator
* @param treeViewContainer container
*/
protected void layoutAnimate(TreeViewContainer treeViewContainer) {
TreeModel<?> mTreeModel = treeViewContainer.getTreeModel();
//means that smooth move from preLocation to curLocation
Object nodeTag = treeViewContainer.getTag(R.id.target_node);
Object targetNodeLocationTag = treeViewContainer.getTag(R.id.target_node_final_location);
Object relativeLocationMapTag = treeViewContainer.getTag(R.id.relative_locations);
Object animatorTag = treeViewContainer.getTag(R.id.node_trans_animator);
if(animatorTag instanceof ValueAnimator){
((ValueAnimator)animatorTag).end();
}
if (nodeTag instanceof NodeModel
&& targetNodeLocationTag instanceof ViewBox
&& relativeLocationMapTag instanceof Map) {
ViewBox targetNodeLocation = (ViewBox) targetNodeLocationTag;
Map<NodeModel<?>,ViewBox> relativeLocationMap = (Map<NodeModel<?>,ViewBox>)relativeLocationMapTag;
AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f);
valueAnimator.setDuration(TreeViewContainer.DEFAULT_FOCUS_DURATION);
valueAnimator.setInterpolator(interpolator);
valueAnimator.addUpdateListener(value -> {
//先根據(jù)相對(duì)位置畫出原來的位置
float ratio = (float) value.getAnimatedValue();
TreeViewLog.e(TAG, "valueAnimator update ratio[" + ratio + "]");
mTreeModel.doTraversalNodes(node -> {
TreeViewHolder<?> treeViewHolder = treeViewContainer.getTreeViewHolder(node);
if (treeViewHolder != null) {
View view = treeViewHolder.getView();
ViewBox preLocation = (ViewBox) view.getTag(R.id.node_pre_location);
ViewBox deltaLocation = (ViewBox) view.getTag(R.id.node_delta_location);
if(preLocation !=null && deltaLocation!=null){
//calculate current location 計(jì)算漸變位置 并 布局
ViewBox currentLocation = preLocation.add(deltaLocation.multiply(ratio));
view.layout(currentLocation.left,
currentLocation.top,
currentLocation.left+view.getMeasuredWidth(),
currentLocation.top+view.getMeasuredHeight());
}
}
});
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation, boolean isReverse) {
TreeViewLog.e(TAG, "onAnimationStart ");
//calculate and layout on preLocation 位置變換過程
mTreeModel.doTraversalNodes(node -> {
TreeViewHolder<?> treeViewHolder = treeViewContainer.getTreeViewHolder(node);
if (treeViewHolder != null) {
View view = treeViewHolder.getView();
ViewBox relativeLocation = relativeLocationMap.get(treeViewHolder.getNode());
//calculate location info 計(jì)算位置
ViewBox preLocation = targetNodeLocation.add(relativeLocation);
ViewBox finalLocation = (ViewBox) view.getTag(R.id.node_final_location);
if(preLocation==null || finalLocation==null){
return;
}
ViewBox deltaLocation = finalLocation.subtract(preLocation);
//save as tag
view.setTag(R.id.node_pre_location, preLocation);
view.setTag(R.id.node_delta_location, deltaLocation);
//layout on preLocation 更新布局
view.layout(preLocation.left, preLocation.top, preLocation.left+view.getMeasuredWidth(), preLocation.top+view.getMeasuredHeight());
}
});
}
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
...
//layout on finalLocation 在布局最終位置
mTreeModel.doTraversalNodes(node -> {
TreeViewHolder<?> treeViewHolder = treeViewContainer.getTreeViewHolder(node);
if (treeViewHolder != null) {
View view = treeViewHolder.getView();
ViewBox finalLocation = (ViewBox) view.getTag(R.id.node_final_location);
if(finalLocation!=null){
view.layout(finalLocation.left, finalLocation.top, finalLocation.right, finalLocation.bottom);
}
view.setTag(R.id.node_pre_location,null);
view.setTag(R.id.node_delta_location,null);
view.setTag(R.id.node_final_location, null);
view.setElevation(TreeViewContainer.Z_NOR);
}
});
}
});
treeViewContainer.setTag(R.id.node_trans_animator,valueAnimator);
valueAnimator.start();
}
}
實(shí)現(xiàn)樹狀圖的回歸適應(yīng)屏幕
這個(gè)功能點(diǎn)相對(duì)簡(jiǎn)單甘晤,前提是TreeViewContainer放縮一定要以(0,0)為中心點(diǎn),并且TreeViewContainer的移動(dòng)放縮不是使用Canas或srollTo操作遏弱,這樣在onSizeChange中塞弊,我們記錄適配屏幕的scale就行了。
/**
*記錄
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
TreeViewLog.e(TAG,"onSizeChanged w["+w+"]h["+h+"]oldw["+oldw+"]oldh["+oldh+"]");
viewWidth = w;
viewHeight = h;
drawInfo.setWindowWidth(w);
drawInfo.setWindowHeight(h);
fixWindow();
}
/**
* fix view tree
*/
private void fixWindow() {
float scale;
float hr = 1f*viewHeight/winHeight;
float wr = 1f*viewWidth/winWidth;
scale = Math.max(hr, wr);
minScale = 1f/scale;
if(Math.abs(scale-1)>0.01f){
//setPivotX((winWidth*scale-viewWidth)/(2*(scale-1)));
//setPivotY((winHeight*scale-viewHeight)/(2*(scale-1)));
setPivotX(0);
setPivotY(0);
setScaleX(1f/scale);
setScaleY(1f/scale);
}
//when first init
if(centerMatrix==null){
centerMatrix = new Matrix();
}
centerMatrix.set(getMatrix());
float[] values = new float[9];
centerMatrix.getValues(values);
values[Matrix.MTRANS_X]=0f;
values[Matrix.MTRANS_Y]=0f;
centerMatrix.setValues(values);
setTouchDelegate();
}
/**
*恢復(fù)
*/
public void focusMidLocation() {
TreeViewLog.e(TAG, "focusMidLocation: "+getMatrix());
float[] centerM = new float[9];
if(centerMatrix==null){
TreeViewLog.e(TAG, "no centerMatrix!!!");
return;
}
centerMatrix.getValues(centerM);
float[] now = new float[9];
getMatrix().getValues(now);
if(now[Matrix.MSCALE_X]>0&&now[Matrix.MSCALE_Y]>0){
animate().scaleX(centerM[Matrix.MSCALE_X])
.translationX(centerM[Matrix.MTRANS_X])
.scaleY(centerM[Matrix.MSCALE_Y])
.translationY(centerM[Matrix.MTRANS_Y])
.setDuration(DEFAULT_FOCUS_DURATION)
.start();
}
}
拖動(dòng)編輯樹狀圖結(jié)構(gòu)
想要拖動(dòng)編輯樹狀圖結(jié)構(gòu)要有如下幾個(gè)步驟:
請(qǐng)求父View不要攔截觸摸事件
在TreeViewContainer中使用ViewDragHelper實(shí)現(xiàn)捕獲View,以目標(biāo)Node的所有Node一并記錄原始位置
拖動(dòng)目標(biāo)View組
在移動(dòng)過程中诀黍,計(jì)算跟是不是碰撞到某個(gè)節(jié)點(diǎn)View了,如果是那么記錄碰撞的節(jié)點(diǎn)
在釋放時(shí)枣宫,如果有碰撞節(jié)點(diǎn)吃环,那么走添加刪除節(jié)點(diǎn)流程即可
在釋放時(shí),如果沒有碰撞點(diǎn)歇拆,則使用Scroller回滾到初始位置
請(qǐng)求父View不要攔截觸摸事件, 這個(gè)不要搞混了故觅,是parent.requestDisallowInterceptTouchEvent(isEditMode);而不是直接requestDisallowInterceptTouchEvent
protected void requestMoveNodeByDragging(boolean isEditMode) {
this.isDraggingNodeMode = isEditMode;
ViewParent parent = getParent();
if (parent instanceof View) {
parent.requestDisallowInterceptTouchEvent(isEditMode);
}
}
這里簡(jiǎn)單說一下ViewDragHelper的使用, 官方說ViewDragHelper是在自定義ViewGroup時(shí)非常有用的工具類渠啊。它提供了一系列有用的操作及狀態(tài)跟蹤使用戶可以在父類的中拖動(dòng)或改變子View的位置。注重, 限于拖動(dòng)及改變位置贯溅,對(duì)于放縮那就無能為力了, 不過剛好拖動(dòng)編輯節(jié)點(diǎn)這個(gè)功能不使用放縮。它的原理也是译柏,判斷有沒滑動(dòng)一定距離姐霍,或者是否到達(dá)了邊界來攔截觸摸事件。
//1 初始化
dragHelper = ViewDragHelper.create(this, dragCallback);
//2 判斷攔截及處理onTouchEvent
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercept = dragHelper.shouldInterceptTouchEvent(event);
TreeViewLog.e(TAG, "onInterceptTouchEvent: "+MotionEvent.actionToString(event.getAction())+" intercept:"+intercept);
return isDraggingNodeMode && intercept;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
TreeViewLog.e(TAG, "onTouchEvent: "+MotionEvent.actionToString(event.getAction()));
if(isDraggingNodeMode) {
dragHelper.processTouchEvent(event);
}
return isDraggingNodeMode;
}
//3 實(shí)現(xiàn)Callback
private final ViewDragHelper.Callback dragCallback = new ViewDragHelper.Callback(){
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
//是否捕獲拖動(dòng)的View
return false;
}
@Override
public int getViewHorizontalDragRange(@NonNull View child) {
//在判斷是否攔截時(shí)胯府,判斷是否超出水平移動(dòng)范圍
return Integer.MAX_VALUE;
}
@Override
public int getViewVerticalDragRange(@NonNull View child) {
//在判斷是否攔截時(shí)骂因,判斷是否超出垂直移動(dòng)范圍
return Integer.MAX_VALUE;
}
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
//水平移動(dòng)位置差赃泡,返回希望移動(dòng)后的位置
//特別注意在攔截階段 返回left與原來一樣,說明到達(dá)邊界影所,不攔截
return left;
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
//垂直移動(dòng)位置差猴娩,返回希望移動(dòng)后的位置
//特別注意在攔截階段 返回left與原來一樣勺阐,說明到達(dá)邊界,不攔截
return top;
}
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
//釋放捕獲的View
}
};
那么捕獲時(shí)蟆豫,開始記錄位置
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
//如果是拖動(dòng)編輯功能懒闷,那么使用記錄要移動(dòng)的塊
if(isDraggingNodeMode && dragBlock.load(child)){
child.setTag(R.id.edit_and_dragging,IS_EDIT_DRAGGING);
child.setElevation(Z_SELECT);
return true;
}
return false;
}
拖動(dòng)一組View時(shí),因?yàn)檫@組View的相對(duì)位置是不變的帮辟,所以可以都是無論是垂直方向還是水平方向都使用同一個(gè)dx玩焰,dy
public void drag(int dx, int dy){
if(!mScroller.isFinished()){
return;
}
this.isDragging = true;
for (int i = 0; i < tmp.size(); i++) {
View view = tmp.get(i);
//offset變化的是布局昔园,不是變換矩陣并炮。而這里拖動(dòng)沒有影響container的Matrix
view.offsetLeftAndRight(dx);
view.offsetTopAndBottom(dy);
}
}
拖動(dòng)過程中逃魄,要計(jì)算是否碰撞到其他View
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
//攔截前返回left說明沒有到邊界可以攔截壹若, 攔截后返回原來位置皂冰,說明不用dragHelper來幫忙移動(dòng),我們自己來一共目標(biāo)View
if(dragHelper.getViewDragState()==ViewDragHelper.STATE_DRAGGING){
final int oldLeft = child.getLeft();
dragBlock.drag(dx,0);
//拖動(dòng)過程中不斷判斷是否碰撞
estimateToHitTarget(child);
invalidate();
return oldLeft;
}else{
return left;
}
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
//與上面代碼一致
...
}
//如果撞擊了赂蕴,那么invalidate概说,畫撞擊提醒
private void drawDragBackGround(View view){
Object fTag = view.getTag(R.id.the_hit_target);
boolean getHit = fTag != null;
if(getHit){
//draw
.....
mPaint.reset();
mPaint.setColor(Color.parseColor("#4FF1286C"));
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
PointF centerPoint = getCenterPoint(view);
drawInfo.getCanvas().drawCircle(centerPoint.x,centerPoint.y,(float)fR,mPaint);
PointPool.free(centerPoint);
}
}
釋放時(shí)嚣伐,如果有目標(biāo)那么刪除再添加,走刪除添加流程放典;如果沒有基茵,那么使用Scroller協(xié)助回滾
//釋放
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
TreeViewLog.d(TAG, "onViewReleased: ");
Object fTag = releasedChild.getTag(R.id.the_hit_target);
boolean getHit = fTag != null;
//如果及記錄了撞擊點(diǎn),刪除再添加弥臼,走刪除添加流程
if(getHit){
TreeViewHolder<?> targetHolder = getTreeViewHolder((NodeModel)fTag);
NodeModel<?> targetHolderNode = targetHolder.getNode();
TreeViewHolder<?> releasedChildHolder = (TreeViewHolder<?>)releasedChild.getTag(R.id.item_holder);
NodeModel<?> releasedChildHolderNode = releasedChildHolder.getNode();
if(releasedChildHolderNode.getParentNode()!=null){
mTreeModel.removeNode(releasedChildHolderNode.getParentNode(),releasedChildHolderNode);
}
mTreeModel.addNode(targetHolderNode,releasedChildHolderNode);
mTreeModel.calculateTreeNodesDeep();
if(isAnimateMove()){
recordAnchorLocationOnViewPort(false,targetHolderNode);
}
requestLayout();
}else{
//recover 如果沒有径缅,那么使用Scroller協(xié)助回滾
dragBlock.smoothRecover(releasedChild);
}
dragBlock.setDragging(false);
releasedChild.setElevation(Z_NOR);
releasedChild.setTag(R.id.edit_and_dragging,null);
releasedChild.setTag(R.id.the_hit_target, null);
invalidate();
}
//注意重寫container的computeScroll烙肺,實(shí)現(xiàn)更新
@Override
public void computeScroll() {
if(dragBlock.computeScroll()){
invalidate();
}
}
寫在最后
到到這里就介紹完,整個(gè)樹狀節(jié)點(diǎn)圖的拖動(dòng)放縮兆旬,添加刪除節(jié)點(diǎn)丽猬,拖動(dòng)編輯等這幾個(gè)功能的實(shí)現(xiàn)原理了,當(dāng)然里面還有很多實(shí)現(xiàn)細(xì)節(jié)脚祟。你可以把這篇文章作為源碼查看的引導(dǎo),細(xì)節(jié)方面也還有很多待完善的地方为黎。后面這個(gè)開源應(yīng)該會(huì)繼續(xù)更新行您,大家也可以一起探討,fork出來一起改炕檩。如果覺得不錯(cuò)請(qǐng)給個(gè)星呢捌斧。
這個(gè)項(xiàng)目如果有人用就會(huì)持續(xù)更新下去。喜歡點(diǎn)個(gè)贊妇押,謝謝姓迅。