利用遞歸算法、堆棧打造一個android可擦除思惟導圖

前言

說來也奇怪,高中學代碼的時候,成天在刷一些noip的題目,鑽研各類算法,什麼遞歸、分治、動態規劃。而真正工做後,發現不多用不到,直到這個頁面才讓我用到算法。其實這個頁面,是我前年寫的,可是一直偷懶,不想整理髮布,去年的時候,在csdn上發佈過一些,可是沒怎麼認真寫,今天乘着週末認真給你們講講,但願能勾起你們對算法的回憶。
項目需求是一個思惟導圖、每一個節點的個數以及數據由服務端返回,這就須要每一次點擊都得計算位置以及繪製佈局。javascript

效果

思惟導圖

這種思惟導圖有兩種模式,一種是能夠無限點擊各個節點(上圖),不清除以前的節點;另一種是當點擊同級節點時,其餘節點的子節點清除(下圖)。
思惟導圖

這兩種模式,均可以隨時隨意經過右上角切換按鈕進行無縫切換。

思路

1.佈局:
這個佈局是一張圖,可能會很大,支持上下左右拖拽,這個時候,我想到了HVScrollView,只要在裏面放一個RelativeLayout,隨便設置一個長寬500dp,以後有新節點,像RelativeLayout中addview便可使佈局增大,支持各類滾動。當節點須要清除時,調用removeview便可刪除佈局,減小寬高,節約內存。
2.節點:
暫時先把每一個節點看做一個button,繪製的位置是根據數量來計算,其中x位置是前一個節點+某個固定值,y位置爲前一個節點y-當前節點數量*每一個節點高度/2java

x=前一個x+a   //a爲節點間距。
y=前一個y-n*b/2 //n爲當前節點數量 b爲每一個節點佔位高度。複製代碼

3.線條
線條是4階貝塞爾曲線,四個節點分別爲下圖。
node


其實第一個版本沒有采用貝塞爾曲線,採用的是直線圖,致使下級節點可能會重複,因此在程序中不得不加入offset偏移量,便宜量則經過各級節點高度來計算。
4.位置優化
有些節點在繪製的時候,可能高於每一個值,或者佔了別的節點位置,這個時候就得優化位置,我暫採用,一個數據去計算每級的最高位置,而後只和這個位置進行比較。這種作法有個缺點就是隻能向下繪製,即便節點中間有位置,也沒辦法把下一節點方進去。
5.遞歸
不難發現代碼中每一個節點都是由上一個節點繪製出來,因此代碼中只要處理一個節點,而後遞歸調用便可。
6.節點擦除
由於可能會擦除節點,因此要儘量記錄每一個節點,這樣才方便擦除。這裏暫時使用堆棧去記錄,你能夠理解成它是一個數組。

實現

幾個要點講完了,下面就一步一步實現,主要仍是多扯思路。
1.節點開場有個動畫,動畫代碼以下:git

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);// 動畫秒數。
        animation.setFillAfter(true);
        animation.setDuration(700);複製代碼

2.定義節點實體類,根據實際需求來定義github

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.設計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爲當前節點x座標
button_y爲當前節點的y座標
line_x爲線條x座標
tree_h爲樹高,即層級
nc爲下層節點
nodeid業務中遇到的,代碼中能夠忽略。
詳細代碼以下:canvas

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;
// 這個只是爲了區分業務中偶數層button寬度爲300,齊數層爲200
        button_x = tree_current % 2 == 1 ? button_x : button_x - 100;
// 獲得下一層級須要繪製的數量
        int num = 1;
        if (tree_current != 1) num = nc.length;// 下一層個數
// 獲得下一級第一個按鈕的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;
// 定義及設置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());
// 定義及設置出場動畫
            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);// 動畫秒數。
            animation.setFillAfter(true);
            animation.setDuration(700);
            bt[i].startAnimation(animation);
            final int i1 = i;
// 設置監聽
            bt[i].setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
// 若是是擦除模式,擦除其餘同級節點及線條
                    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) {//爲了優化內存,也是醉了
                    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);
        }
    }複製代碼

註釋寫的很全,有一些數值沒抽取出來,有點亂,但不影響閱讀。
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); // 設置畫筆顏色
        redPaint.setStrokeWidth(5.0f);// 設置筆觸寬度
        redPaint.setStyle(Style.STROKE);// 設置畫筆的填充類型(徹底填充)
        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.堆棧ide

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]);
                }//修復棧頂元素被前一層樹元素佔用bug,可是會浪費少許內存,考慮到內存很小,暫時不優化吧。
                if (treehigh[x] == treecurrent) {
                    try {
                        ((Button) v[x]).setHint(null);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }複製代碼

這段代碼主要是用一個數組去存view,其實我應該用SparseArray的,當時隨手寫了普通數組,後來也懶得改。push把view存入數組,pop遍歷後把層級高的view清除並移除元素。
5.至於切換模式的代碼,那就簡單了,就是一個取非操做佈局

murp_nodemodel_title.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getApplicationContext(), !model ? "已切換到擦除模式,點擊節點會擦除後面節點,趕快試試吧。" : "已切換到正常模式,全部節點在一張圖上,趕快試試吧。", Toast.LENGTH_LONG).show();
                model = !model;
            }
        });複製代碼

總結

整體上實現了思惟導圖的繪製,可是,還有不少地方值得優化,好比節點寬高沒有抽取出來;堆棧也須要優化;計算節點佔位高度不夠嚴謹;若是你們有時間,能夠折騰下哦。
源碼地址github.com/qq273681448…以爲好的話,記得關注我哦!

相關文章
相關標籤/搜索