利用遞歸算法疙渣、堆棧打造一個android可擦除思維導(dǎo)圖

前言

說來也奇怪匙奴,高中學(xué)代碼的時候,整天在刷一些noip的題目妄荔,鉆研各種算法泼菌,什么遞歸谍肤、分治、動態(tài)規(guī)劃哗伯。而真正工作后荒揣,發(fā)現(xiàn)很少用不到,直到這個頁面才讓我用到算法焊刹。其實這個頁面系任,是我前年寫的,但是一直偷懶虐块,不想整理發(fā)布俩滥,去年的時候,在csdn上發(fā)布過一些贺奠,但是沒怎么認真寫霜旧,今天乘著周末認真給大家講講,希望能勾起大家對算法的回憶儡率。
項目需求是一個思維導(dǎo)圖挂据、每個節(jié)點的個數(shù)以及數(shù)據(jù)由服務(wù)端返回,這就需要每一次點擊都得計算位置以及繪制布局儿普。

效果

思維導(dǎo)圖
思維導(dǎo)圖

這種思維導(dǎo)圖有兩種模式崎逃,一種是可以無限點擊各個節(jié)點(上圖),不清除之前的節(jié)點眉孩;另外一種是當點擊同級節(jié)點時个绍,其他節(jié)點的子節(jié)點清除(下圖)。


思維導(dǎo)圖
思維導(dǎo)圖

這兩種模式勺像,都可以隨時隨意通過右上角切換按鈕進行無縫切換障贸。

思路

1.布局:
這個布局是一張圖,可能會很大吟宦,支持上下左右拖拽篮洁,這個時候,我想到了HVScrollView殃姓,只要在里面放一個RelativeLayout袁波,隨便設(shè)置一個長寬500dp,之后有新節(jié)點蜗侈,像RelativeLayout中addview即可使布局增大篷牌,支持各種滾動。當節(jié)點需要清除時踏幻,調(diào)用removeview即可刪除布局枷颊,減少寬高,節(jié)約內(nèi)存。
2.節(jié)點:
暫時先把每個節(jié)點看作一個button夭苗,繪制的位置是根據(jù)數(shù)量來計算信卡,其中x位置是前一個節(jié)點+某個固定值,y位置為前一個節(jié)點y-當前節(jié)點數(shù)量*每個節(jié)點高度/2

x=前一個x+a   //a為節(jié)點間距题造。
y=前一個y-n*b/2 //n為當前節(jié)點數(shù)量 b為每個節(jié)點占位高度傍菇。

3.線條
線條是4階貝塞爾曲線,四個節(jié)點分別為下圖界赔。



其實第一個版本沒有采用貝塞爾曲線丢习,采用的是直線圖,導(dǎo)致下級節(jié)點可能會重復(fù)淮悼,所以在程序中不得不加入offset偏移量咐低,便宜量則通過各級節(jié)點高度來計算。
4.位置優(yōu)化
有些節(jié)點在繪制的時候敛惊,可能高于每個值渊鞋,或者占了別的節(jié)點位置,這個時候就得優(yōu)化位置瞧挤,我暫采用,一個數(shù)據(jù)去計算每級的最高位置儡湾,然后只和這個位置進行比較特恬。這種做法有個缺點就是只能向下繪制,即使節(jié)點中間有位置徐钠,也沒辦法把下一節(jié)點方進去癌刽。
5.遞歸
不難發(fā)現(xiàn)代碼中每個節(jié)點都是由上一個節(jié)點繪制出來,所以代碼中只要處理一個節(jié)點尝丐,然后遞歸調(diào)用即可显拜。
6.節(jié)點擦除
因為可能會擦除節(jié)點,所以要盡可能記錄每個節(jié)點爹袁,這樣才方便擦除远荠。這里暫時使用堆棧去記錄,你可以理解成它是一個數(shù)組失息。

實現(xiàn)

幾個要點講完了譬淳,下面就一步一步實現(xiàn),主要還是多扯思路盹兢。
1.節(jié)點開場有個動畫邻梆,動畫代碼如下:

        ScaleAnimation animation = new ScaleAnimation(0.0f,1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);
        animation.setInterpolator(new BounceInterpolator());
        animation.setStartOffset(tree_current == 1 ? 1050 : 50);// 動畫秒數(shù)。
        animation.setFillAfter(true);
        animation.setDuration(700);

2.定義節(jié)點實體類绎秒,根據(jù)實際需求來定義

    public class nodechild {
        private String id;
        private String name;
        private String buteid;
        private String butetype;
        private String nodetype;
        private String ispass;

        public String getNodetype() {
            return nodetype;
        }

        public void setNodetype(String nodetype) {
            this.nodetype = nodetype;
        }

        public nodechild(String id, String name, String buteid, String butetype, String nodetype) {
            super();
            this.id = id;
            this.name = name;
            this.buteid = buteid;
            this.butetype = butetype;
            this.nodetype = nodetype;

        }

        public nodechild(String id, String name) {
            super();
            this.id = id;
            this.name = name;
        }

        public nodechild(String id, String name, String ispass) {
            super();
            this.id = id;
            this.name = name;
            this.ispass = ispass;
        }

        public String getIspass() {
            return ispass;
        }

        public void setIspass(String ispass) {
            this.ispass = ispass;
        }

        public String getButeid() {
            return buteid;
        }

        public void setButeid(String buteid) {
            this.buteid = buteid;
        }

        public String getButetype() {
            return butetype;
        }

        public void setButetype(String butetype) {
            this.butetype = butetype;
        }

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

3.設(shè)計drawbutton繪制一個button的方法

public void drawbutton(int button_y, int button_x, int line_x, final int tree_h, final nodechild[] nc,String nodeid)  {}

button_x為當前節(jié)點x坐標
button_y為當前節(jié)點的y坐標
line_x為線條x坐標
tree_h為樹高浦妄,即層級
nc為下層節(jié)點
nodeid業(yè)務(wù)中遇到的,代碼中可以忽略。
詳細代碼如下:

    public void drawbutton(int button_y, int button_x, int line_x, final int tree_current, final nodechild[] nc, String nodeid) {
//        存儲線的起點y坐標
        int line_y = button_y;
//        這個只是為了區(qū)分業(yè)務(wù)中偶數(shù)層button寬度為300剂娄,齊數(shù)層為200
        button_x = tree_current % 2 == 1 ? button_x : button_x - 100;
//        得到下一層級需要繪制的數(shù)量
        int num = 1;
        if (tree_current != 1) num = nc.length;// 下一層個數(shù)
//        得到下一級第一個按鈕的y坐標
        button_y = button_y - (num - 1) * bt_width / 2;
        if (button_y < tree_xnum[tree_current]) {
            button_y = tree_xnum[tree_current] + 100;
        }
//        移動當前布局到頁面中心
        if (tree_current > 2) hv.scrollTo(button_x - 400, button_y - 100);
        if (tree_xnum[tree_current] < button_y + 200 + (num - 1) * bt_width)
            tree_xnum[tree_current] = button_y + 200 + (num - 1) * bt_width;
//        存儲下一級首個button坐標
        final int button_y_f = button_y;
        final int button_x_f = button_x;
        for (int i = 0; i < num; i++) {
            final int bt_paly_y = bt_width;
            int bt_w = tree_current % 2 == 0 ? bt_width : 200;
            int bt_h = 200;
//            定義及設(shè)置button屬性
            bt[i] = new Button(NodeActivity.this);
            if (tree_current % 2 != 0) {
                bt[i].setBackgroundResource(R.drawable.allokbutton);
            } else {
                bt[i].setBackgroundResource(R.drawable.button33);
            }
            bt[i].setTextColor(Color.WHITE);
            bt[i].setTextSize(15 - (int) Math.sqrt(nc[i].getName().length() - 1));
            bt[i].setText(nc[i].getName());
//            定義及設(shè)置出場動畫
            final String nc_id = nc[i].getId();
            ScaleAnimation animation = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f,
                    Animation.RELATIVE_TO_SELF, 0.5f);
            animation.setInterpolator(new BounceInterpolator());
            animation.setStartOffset(tree_current == 1 ? 1050 : 50);// 動畫秒數(shù)蠢涝。
            animation.setFillAfter(true);
            animation.setDuration(700);
            bt[i].startAnimation(animation);
            final int i1 = i;
//            設(shè)置監(jiān)聽
            bt[i].setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
//                    如果是擦除模式,擦除其他同級節(jié)點及線條
                    if (model) mstack.pop(tree_current);
//                    防止多次點擊宜咒,偷懶的解決辦法
                    if (((Button)v).getHint() != null) {
                        Toast.makeText(getApplicationContext(), ((Button)v).getText(), Toast.LENGTH_LONG).show();
                        return;
                    }
                    ((Button)v).setHint("1");
                    insertLayout.setEnabled(false);
                    int w = button_y_f + i1 * bt_paly_y;
                    int h = button_x_f + bt_paly_y / 2 * 3;
                    getRemoteInfo(w, h, button_y_f + i1 * bt_paly_y, button_x_f, tree_current + 1, nc_id,
                            nc[i1].getButeid());
                }
            });
//            把button通過布局add到頁面里
            layoutParams[i] = new RelativeLayout.LayoutParams(bt_w, bt_h);
            layoutParams[i].topMargin = button_y + i * bt_paly_y;
            layoutParams[i].leftMargin = button_x;
            insertLayout.addView(bt[i], layoutParams[i]);

//            把線繪制到頁面里
            if (tree_current != 1) {
                if (button_y + 100 + i * 300 - (line_y + 100) >= 0) {//為了優(yōu)化內(nèi)存惠赫,也是醉了
                    view = new DrawGeometryView(this, 50, 50, button_x + 100 - (line_x + bt_paly_y) + 50 + (tree_current % 2 == 0 ? 100 : 0), button_y + 100 + i * 300
                            - (line_y + 100) + 50, nc[i].getButetype());
                    layoutParams1[i] = new RelativeLayout.LayoutParams(Math.abs(line_x - button_x) + 500, 100 + button_y + i * 300 - line_y);
                    view.invalidate();
                    layoutParams1[i].topMargin = (line_y + 100) - 50;// line_y-600;//Math.min(line_y+100,button_y+100
                    layoutParams1[i].leftMargin = (line_x + bt_paly_y) - 50;// line_x+300;
                    if (tree_current % 2 == 0) layoutParams1[i].leftMargin -= 100;
                    insertLayout.addView(view, layoutParams1[i]);
                } else {
                    view = new DrawGeometryView(this, 50, -(button_y + 100 + i * 300 - (line_y + 100)) + 50, button_x - line_x - 150 + (tree_current % 2 == 0 ? 100 : 0), 50,
                            nc[i].getButetype());
                    layoutParams1[i] = new RelativeLayout.LayoutParams(Math.abs(line_x - button_x) + 500, 100 + Math.abs(button_y + i * 300
                            - line_y));
                    view.invalidate();
                    layoutParams1[i].topMargin = (button_y + 100 + i * 300) - 50;// line_y-600;//Math.min(line_y+100,button_y+100
                    layoutParams1[i].leftMargin = (line_x + bt_paly_y) - 50;// line_x+300;
                    if (tree_current % 2 == 0) layoutParams1[i].leftMargin -= 100;
                    insertLayout.addView(view, layoutParams1[i]);
                }
//                line入棧
                mstack.push(view, tree_current);
            }
//            button入棧
            mstack.push(bt[i], tree_current);
        }
    }

注釋寫的很全,有一些數(shù)值沒抽取出來故黑,有點亂儿咱,但不影響閱讀。
4.劃線方法

public class DrawGeometryView extends View {
    private int beginx=0;
    private int beginy=0;
    private int stopx=100;
    private int stopy=100;
    private int offset=0;
    private String word="dd";
    /**
     *
     * @param context
     * @param attrs
     */
    public DrawGeometryView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     *
     * @param context
     */
    public DrawGeometryView(Context context,int beginx,int beginy,int stopx,int stopy,String word) {
        super(context);
        this.beginx=beginx;
        this.beginy=beginy;
        this.stopx=stopx;
        this.stopy=stopy;
        if (word==null) word="";
        this.word=word;

    }
    public int Dp2Px(Context context, float dp) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dp * scale + 0.5f);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint redPaint = new Paint(); // 紅色畫筆
        redPaint.setAntiAlias(true); // 抗鋸齒效果,顯得繪圖平滑
        redPaint.setColor(Color.WHITE); // 設(shè)置畫筆顏色
        redPaint.setStrokeWidth(5.0f);// 設(shè)置筆觸寬度
        redPaint.setStyle(Style.STROKE);// 設(shè)置畫筆的填充類型(完全填充)
        redPaint.setTextSize(50);//字體

        Path mPath=new Path();
        mPath.reset();
        //起點
        mPath.moveTo(beginx, beginy);
        //貝塞爾曲線
        mPath.cubicTo(beginx+80, beginy, beginx+80, stopy,stopx-100, stopy);
        //畫path
        canvas.drawPath(mPath, redPaint);
    }

}

這個方法里還有一些項目里的文字繪制场晶,我刪掉了部分代碼混埠。
5.堆棧

 public class Mystack {
        View[] v = new View[1500];
        int[] treehigh = new int[1500];
        int size = 0;

        public void push(View view, int treecurrent) {
            size++;
            v[size] = view;
            treehigh[size] = treecurrent;
        }

        public void pop(int treecurrent) {
            while (treehigh[size] > treecurrent && size > 0) {
                if (size > 0) insertLayout.removeView(v[size]);
                size--;
            }
            for (int j = 49; j > treecurrent; j--) {//樹高清0
                tree_xnum[j] = 0;
            }
            for (int x = size; x > 0; x--) {
                if (treehigh[x] > treecurrent) {
                    insertLayout.removeView(v[x]);
                }//修復(fù)棧頂元素被前一層樹元素占用bug,但是會浪費少量內(nèi)存诗轻,考慮到內(nèi)存很小钳宪,暫時不優(yōu)化吧。
                if (treehigh[x] == treecurrent) {
                    try {
                        ((Button) v[x]).setHint(null);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

這段代碼主要是用一個數(shù)組去存view扳炬,其實我應(yīng)該用SparseArray的吏颖,當時隨手寫了普通數(shù)組,后來也懶得改恨樟。push把view存入數(shù)組半醉,pop遍歷后把層級高的view清除并移除元素。
5.至于切換模式的代碼劝术,那就簡單了缩多,就是一個取非操作

        murp_nodemodel_title.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getApplicationContext(), !model ? "已切換到擦除模式,點擊節(jié)點會擦除后面節(jié)點养晋,趕快試試吧衬吆。" : "已切換到正常模式,所有節(jié)點在一張圖上绳泉,趕快試試吧逊抡。", Toast.LENGTH_LONG).show();
                model = !model;
            }
        });

總結(jié)

總體上實現(xiàn)了思維導(dǎo)圖的繪制,但是圈纺,還有很多地方值得優(yōu)化秦忿,比如節(jié)點寬高沒有抽取出來;堆棧也需要優(yōu)化蛾娶;計算節(jié)點占位高度不夠嚴謹灯谣;如果大家有時間,可以折騰下哦蛔琅。
源碼地址https://github.com/qq273681448/Mindmap
覺得好的話胎许,記得關(guān)注我哦!
csdn博客:
http://blog.csdn.net/qq273681448/
簡書:
http://www.reibang.com/users/ef6207f116a2/timeline
掘金:
https://juejin.im/user/57cd55218ac247006459c40c
Github:
https://github.com/qq273681448

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市辜窑,隨后出現(xiàn)的幾起案子钩述,更是在濱河造成了極大的恐慌,老刑警劉巖穆碎,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件牙勘,死亡現(xiàn)場離奇詭異,居然都是意外死亡所禀,警方通過查閱死者的電腦和手機方面,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來色徘,“玉大人恭金,你說我怎么就攤上這事」硬撸” “怎么了横腿?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長斤寂。 經(jīng)常有香客問我耿焊,道長,這世上最難降的妖魔是什么遍搞? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任搀别,我火速辦了婚禮,結(jié)果婚禮上尾抑,老公的妹妹穿的比我還像新娘。我一直安慰自己蒂培,他們只是感情好再愈,可當我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著护戳,像睡著了一般翎冲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上媳荒,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天抗悍,我揣著相機與錄音,去河邊找鬼钳枕。 笑死缴渊,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的鱼炒。 我是一名探鬼主播衔沼,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了指蚁?” 一聲冷哼從身側(cè)響起菩佑,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎凝化,沒想到半個月后稍坯,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡搓劫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年瞧哟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片糟把。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡绢涡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出遣疯,到底是詐尸還是另有隱情雄可,我是刑警寧澤,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布缠犀,位于F島的核電站数苫,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏辨液。R本人自食惡果不足惜虐急,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望滔迈。 院中可真熱鬧止吁,春花似錦、人聲如沸燎悍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谈山。三九已至俄删,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間奏路,已是汗流浹背畴椰。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鸽粉,地道東北人斜脂。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像潜叛,于是被迫代替她去往敵國和親秽褒。 傳聞我的和親對象是個殘疾皇子壶硅,可洞房花燭夜當晚...
    茶點故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內(nèi)容