前言
說來也奇怪,高中學程式碼的時候,整天在刷一些noip的題目,鑽研各種演算法,什麼遞迴、分治、動態規劃。而真正工作後,發現很少用不到,直到這個頁面才讓我用到演算法。其實這個頁面,是我前年寫的,但是一直偷懶,不想整理髮布,去年的時候,在csdn上釋出過一些,但是沒怎麼認真寫,今天乘著週末認真給大家講講,希望能勾起大家對演算法的回憶。
專案需求是一個思維導圖、每個節點的個數以及資料由服務端返回,這就需要每一次點選都得計算位置以及繪製佈局。
效果
這種思維導圖有兩種模式,一種是可以無限點選各個節點(上圖),不清除之前的節點;另外一種是當點選同級節點時,其他節點的子節點清除(下圖)。
這兩種模式,都可以隨時隨意通過右上角切換按鈕進行無縫切換。
思路
1.佈局:
這個佈局是一張圖,可能會很大,支援上下左右拖拽,這個時候,我想到了HVScrollView,只要在裡面放一個RelativeLayout,隨便設定一個長寬500dp,之後有新節點,像RelativeLayout中addview即可使佈局增大,支援各種滾動。當節點需要清除時,呼叫removeview即可刪除佈局,減少寬高,節約記憶體。
2.節點:
暫時先把每個節點看作一個button,繪製的位置是根據數量來計算,其中x位置是前一個節點+某個固定值,y位置為前一個節點y-當前節點數量*每個節點高度/2
x=前一個x+a //a為節點間距。
y=前一個y-n*b/2 //n為當前節點數量 b為每個節點佔位高度。複製程式碼
3.線條
線條是4階貝塞爾曲線,四個節點分別為下圖。
其實第一個版本沒有采用貝塞爾曲線,採用的是直線圖,導致下級節點可能會重複,所以在程式中不得不加入offset偏移量,便宜量則通過各級節點高度來計算。
4.位置優化
有些節點在繪製的時候,可能高於每個值,或者佔了別的節點位置,這個時候就得優化位置,我暫採用,一個資料去計算每級的最高位置,然後只和這個位置進行比較。這種做法有個缺點就是隻能向下繪製,即使節點中間有位置,也沒辦法把下一節點方進去。
5.遞迴
不難發現程式碼中每個節點都是由上一個節點繪製出來,所以程式碼中只要處理一個節點,然後遞迴呼叫即可。
6.節點擦除
因為可能會擦除節點,所以要儘可能記錄每個節點,這樣才方便擦除。這裡暫時使用堆疊去記錄,你可以理解成它是一個陣列。
實現
幾個要點講完了,下面就一步一步實現,主要還是多扯思路。
1.節點開場有個動畫,動畫程式碼如下:
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.定義節點實體類,根據實際需求來定義
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業務中遇到的,程式碼中可以忽略。
詳細程式碼如下:
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.堆疊
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…
覺得好的話,記得關注我哦!