<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          厲害了,Android自定義樹狀圖控件來了!

          共 11889字,需瀏覽 24分鐘

           ·

          2022-04-16 17:48

          ?BATcoder技術(shù)群,讓一部分人先進大廠

          大家好,我是劉望舒,騰訊最具價值專家,著有三本業(yè)內(nèi)知名暢銷書,連續(xù)五年蟬聯(lián)電子工業(yè)出版社年度優(yōu)秀作者,百度百科收錄的資深技術(shù)專家。

          前華為面試官、獨角獸公司技術(shù)總監(jiān)。


          想要加入?BATcoder技術(shù)群,公號回復(fù)BAT?即可。

          作者:傳道士?鏈接:https://juejin.cn/post/7080519395163766791

          文章目錄

          • 1、簡介
          • 2、效果展示
          • 3、使用步驟
          • 4、實現(xiàn)基本布局流程
          • 5、實現(xiàn)自由放縮及拖動
          • 6、實現(xiàn)添加刪除及節(jié)點動畫
          • 7、實現(xiàn)樹狀圖的回歸適應(yīng)屏幕
          • 8、實現(xiàn)拖到編輯樹狀圖結(jié)構(gòu)
          • 9、寫在最后

          簡介

          github連接: https://links.jianshu.com/go?to=https://github.com/guaishouN/android-tree-view.git

          目前沒發(fā)現(xiàn)比較好的Android樹狀圖開源控件,于是決定自己寫一個開源控件,對比了一下市面上關(guān)于思維導(dǎo)圖或者樹狀圖顯示(如xMind,mind master等)的app,本文開源框架并不遜色。實現(xiàn)這個樹狀圖過程中主要綜合應(yīng)用了很多自定義控件關(guān)鍵知識點,比如自定義ViewGroup的步驟、觸摸事件的處理、動畫使用、Scroller及慣性滑動、ViewDragHelper的使用等等。主要實現(xiàn)了下面幾個功能點。

          • 絲滑的跟隨手指放縮,拖動,及慣性滑動

          • 自動動畫回歸屏幕中心

          • 支持子節(jié)點復(fù)雜布局自定義,并且節(jié)點布局點擊事件與滑動不沖突

          • 節(jié)點間的連接線自定義

          • 可刪除動態(tài)節(jié)點

          • 可動態(tài)添加節(jié)點

          • 支持拖動調(diào)整節(jié)點關(guān)系

          • 增刪、移動結(jié)構(gòu)添加動畫效果

          效果展示

          基礎(chǔ)--連接線, 布局, 自定義節(jié)點View

          添加

          刪除

          拖動節(jié)點編輯書樹狀圖結(jié)構(gòu)

          放縮拖動不影響點擊

          放縮拖動及適應(yīng)窗口

          使用步驟

          下面說明中Animal類是僅僅用于舉例的bean

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

          按照以下四個步驟使用該開源控件

          1、 通過繼承 TreeViewAdapter實現(xiàn)節(jié)點數(shù)據(jù)與節(jié)點視圖的綁定

          public?class?AnimalTreeViewAdapter?extends?TreeViewAdapter<Animal>?{
          ????private?DashLine?dashLine?=??new?DashLine(Color.parseColor("#F06292"),6);
          ????@Override
          ????public?TreeViewHolder?onCreateViewHolder(@NonNull?ViewGroup?viewGroup,?NodeModel?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?holder)?{
          ????????//TODO?get?view?and?node?from?holder,?and?then?control?your?item?view
          ????????View?itemView?=?holder.getView();
          ????????NodeModel?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è)置布局風格(向右展開或垂直向下展開)、父節(jié)點與子節(jié)點的間隙、子節(jié)點間的間隙、節(jié)點間的連線(已經(jīng)實現(xiàn)了直線、光滑曲線、虛線、根狀線,也可通過BaseLine實現(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、 把AdapterLayoutManager設(shè)置到你的樹狀圖

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

          4 、設(shè)置節(jié)點數(shù)據(jù)

          //Create?a?TreeModel?by?using?a?root?node.
          NodeModel?node0?=?new?NodeModel<>(new?Animal(R.drawable.ic_01,"root"));
          TreeModel?treeModel?=?new?TreeModel<>(root);

          //Other?nodes.
          NodeModel?node1?=?new?NodeModel<>(new?Animal(R.drawable.ic_02,"sub0"));
          NodeModel?node2?=?new?NodeModel<>(new?Animal(R.drawable.ic_03,"sub1"));
          NodeModel?node3?=?new?NodeModel<>(new?Animal(R.drawable.ic_04,"sub2"));
          NodeModel?node4?=?new?NodeModel<>(new?Animal(R.drawable.ic_05,"sub3"));
          NodeModel?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);

          實現(xiàn)基本的布局流程

          這里涉及View自定義的基本三部曲onMeasureonLayoutonDrawonDispatchDraw, 其中我把onMeasureonLayout布局的交給了一個特定的類LayoutManager處理,并且把節(jié)點的子View生成及綁定交給Adapter處理,在onDispatchDraw中畫節(jié)點的連線也交給Adapter處理。這樣可以極大地方便使用者自定義連線及節(jié)點View,甚至是自定義LayoutManager。另外在onSizeChange中記錄控件的大小。

          這幾個關(guān)鍵點的流程是onMeasure->onLayout->onSizeChanged->onDrawonDispatchDraw

          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?????????????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);
          ????????//記錄適應(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>?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);
          ????????}
          ????}

          實現(xiàn)自由放縮及拖動

          這部分是核心點,乍一看很簡單,不就是處理下dispaTouchEventonInterceptTouchEventonTouchEvent就可以了嗎?沒錯是都是在這幾個函數(shù)中處理,但是要知道以下這幾個難點:

          • 這個自定義控件要放縮或移動過程中,通過onTouchEvent中MotionEvent.getX()拿到的觸摸事件也是放縮后觸點相對父View的位置,而getRaw又不是所有SDK版本都支持的,因為不能獲取穩(wěn)定的觸點數(shù)據(jù),所以可能放縮會出現(xiàn)震動的現(xiàn)象
          • 這個樹狀圖自定義控件子節(jié)點View也是ViewGroup,至少拖動放縮不能影響子節(jié)點View里的控件點擊事件
          • 另外還要考慮,回歸屏幕中心控制、增刪節(jié)點要穩(wěn)定目標節(jié)點View顯示、反變換獲取View相對屏幕位置等, 實現(xiàn)放縮及拖動時的觸點跟隨

          對于問題1,可以再加一層一樣大小的ViewGroup(其實就是GysoTreeView,它是一個殼)用來接收觸摸事件,這樣因為這個接收觸摸事件的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用來處理觸摸事件,有點像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,則觸發(fā)攔截
          ????????if(action?==?MotionEvent.ACTION_MOVE?&&?mTouchSlop?????????????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,為了不影響節(jié)點View的點擊事件,我們不能使用Canvas去移送或放縮,否則點擊位置會錯亂。另外,也不能使用Sroller去控制,因為scrollTo滾動控制不會記錄在View變換Matrix中,為了方便控制不使用scrollTo, 而是使用setTranslationYsetScaleY, 這樣可以很方便根據(jù)變換矩陣來控制整個樹狀圖。

          對于問題3,控制變換及反變換, setPivotX(0)這樣你可以很方便的通過x0*scale+translate = x1確定變換關(guān)系

          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);

          實現(xiàn)添加刪除節(jié)點動畫

          實現(xiàn)思路很簡單,保存當前相對目標節(jié)點位置信息,增刪節(jié)點后,把重新測量布局的位置作為最新位置,位置變化進度用0->1間的百分比表示

          首先,保存當前相對目標節(jié)點位置信息,如果是刪除則選其父節(jié)點作為目標節(jié)點,如果是添加節(jié)點,那么選添加子節(jié)點的父節(jié)點作為目標節(jié)點,記錄這個節(jié)點相對屏幕的位置,及這時的放縮比例,并且記錄所有其他節(jié)點View相對這個目標節(jié)點的位置。寫代碼過程中,使用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,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,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ā)重新測量、布局。但是這時不要急著畫到屏幕,先根據(jù)目標節(jié)點原來在屏幕的位置,及放縮大小,反變換使目標節(jié)點不至于產(chǎn)生跳動的感覺。

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

          ????????????????????//fix?pre?size?and?location?根據(jù)目標節(jié)點在手機中屏幕的位置重新移動,避免跳動
          ????????????????????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ù)相對位置還原添加刪除前的位置,0->1變換到最終最新位置

          ????@Override
          ????public?void?performLayout(final?TreeViewContainer?treeViewContainer)?{
          ????????final?TreeModel?mTreeModel?=?treeViewContainer.getTreeModel();
          ????????if?(mTreeModel?!=?null)?{
          ????????????mTreeModel.doTraversalNodes(new?ITraversal>()?{
          ????????????????@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,ViewBox>?relativeLocationMap?=?(Map,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ù)相對位置畫出原來的位置
          ????????????????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();
          ????????}
          ????}

          實現(xiàn)樹狀圖的回歸適應(yīng)屏幕

          這個功能點相對簡單,前提是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();
          ????}

          ????/**
          ????*恢復(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();
          ????????}
          ????}

          拖動編輯樹狀圖結(jié)構(gòu)

          想要拖動編輯樹狀圖結(jié)構(gòu)要有如下幾個步驟:

          • 請求父View不要攔截觸摸事件

          • TreeViewContainer中使用ViewDragHelper實現(xiàn)捕獲View,以目標Node的所有Node一并記錄原始位置

          • 拖動目標View組

          • 在移動過程中,計算跟是不是碰撞到某個節(jié)點View了,如果是那么記錄碰撞的節(jié)點

          • 在釋放時,如果有碰撞節(jié)點,那么走添加刪除節(jié)點流程即可

          • 在釋放時,如果沒有碰撞點,則使用Scroller回滾到初始位置

          請求父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時非常有用的工具類。它提供了一系列有用的操作及狀態(tài)跟蹤使用戶可以在父類的中拖動或改變子View的位置。注重, 限于拖動及改變位置,對于放縮那就無能為力了, 不過剛好拖動編輯節(jié)點這個功能不使用放縮。它的原理也是,判斷有沒滑動一定距離,或者是否到達了邊界來攔截觸摸事件。

          //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?實現(xiàn)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的相對位置是不變的,所以可以都是無論是垂直方向還是水平方向都使用同一個dxdy

          ????public?void?drag(int?dx,?int?dy){
          ????????if(!mScroller.isFinished()){
          ????????????return;
          ????????}
          ????????this.isDragging?=?true;
          ????????for?(int?i?=?0;?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協(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;
          ????//如果及記錄了撞擊點,刪除再添加,走刪除添加流程
          ????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,實現(xiàn)更新
          @Override
          public?void?computeScroll()?{
          ????if(dragBlock.computeScroll()){
          ????????invalidate();
          ????}
          }

          寫在最后

          到到這里就介紹完,整個樹狀節(jié)點圖的拖動放縮,添加刪除節(jié)點,拖動編輯等這幾個功能的實現(xiàn)原理了,當然里面還有很多實現(xiàn)細節(jié)。你可以把這篇文章作為源碼查看的引導(dǎo),細節(jié)方面也還有很多待完善的地方。后面這個開源應(yīng)該會繼續(xù)更新,大家也可以一起探討,fork出來一起改。如果覺得不錯請給個星呢。



          ? 耗時2年,Android進階三部曲第三部《Android進階指北》出版!

          ? 『BATcoder』做了多年安卓還沒編譯過源碼?一個視頻帶你玩轉(zhuǎn)!

          ? 『BATcoder』我去!安裝Ubuntu還有坑?

          ? 重生!進階三部曲第一部《Android進階之光》第2版 出版!

          為了防止失聯(lián),歡迎關(guān)注我的小號

          ??微信改了推送機制,真愛請星標本公號??
          瀏覽 59
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美成人无码A片免费一区澳门 | re久久6热 | 精品久久久中文字幕 | 综合插插 | 国产内射免费视频 |