国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 系統 > Android > 正文

android LabelView實現標簽云效果

2019-10-22 18:10:20
字體:
來源:轉載
供稿:網友

今天我們來做一個android上的標簽云效果, 雖然還不是很完美,但是已經足夠可以展現標簽云的效果了,首先來看看效果吧。

android,LabelView,標簽云

額,錄屏只能錄到這個份上了,湊活著看吧。今天我們就來實現一下這個效果, 這次我選擇直接繼承view來, 什么? 這樣的效果不是SurfaceView擅長的嗎? 為什么要view,其實都可以了, 我選擇view,是因為:額,我對SurfaceView還不是很熟悉。

廢話少說, 下面開始上代碼

public class LabelView extends View {  private static final int DIRECTION_LEFT = 0; // 向左  private static final int DIRECTION_RIGHT = 1; // 向右  private static final int DIRECITON_TOP = 2; // 向上  private static final int DIRECTION_BOTTOM = 3; // 向下    private boolean isStatic; // 是否靜止, 默認false, 可用干xml : label:is_static="false"    private int[][] mLocations; // 每個label的位置 x/y  private int[][] mDirections; // 每個label的方向 x/y  private int[][] mSpeeds; // 每個label的x/y速度 x/y  private int[][] mTextWidthAndHeight; // 每個labeltext的大小 width/height    private String[] mLabels; // 設置的labels  private int[] mFontSizes; // 每個label的字體大小  // 默認配色方案  private int[] mColorSchema = {0XFFFF0000, 0XFF00FF00, 0XFF0000FF, 0XFFCCCCCC, 0XFFFFFFFF};    private int mTouchSlop; // 最小touch  private int mDownX = -1;  private int mDownY = -1;  private int mDownIndex = -1; // 點擊的index    private Paint mPaint;    private Thread mThread;    private OnItemClickListener mListener; // item點擊事件    public LabelView(Context context, AttributeSet attrs) {   this(context, attrs, 0);  }   public LabelView(Context context, AttributeSet attrs, int defStyleAttr) {   super(context, attrs, defStyleAttr);      TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LabelView, defStyleAttr, 0);   isStatic = ta.getBoolean(R.styleable.LabelView_is_static, false);   ta.recycle();      mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();      mPaint = new Paint();   mPaint.setAntiAlias(true);  }    @Override  protected void onLayout(boolean changed, int left, int top, int right,    int bottom) {   super.onLayout(changed, left, top, right, bottom);   init();  }    @Override  protected void onDraw(Canvas canvas) {   if(!hasContents()) {    return;   }      for (int i = 0; i < mLabels.length; i++) {    mPaint.setTextSize(mFontSizes[i]);        if(i < mColorSchema.length) mPaint.setColor(mColorSchema[i]);    else mPaint.setColor(mColorSchema[i-mColorSchema.length]);        canvas.drawText(mLabels[i], mLocations[i][0], mLocations[i][1], mPaint);   }  }    @Override  public boolean onTouchEvent(MotionEvent ev) {   switch (ev.getAction()) {   case MotionEvent.ACTION_DOWN:    mDownX = (int) ev.getX();    mDownY = (int) ev.getY();    mDownIndex = getClickIndex();    break;   case MotionEvent.ACTION_UP:    int nowX = (int) ev.getX();    int nowY = (int) ev.getY();    if (nowX - mDownX < mTouchSlop && nowY - mDownY < mTouchSlop      && mDownIndex != -1 && mListener != null) {     mListener.onItemClick(mDownIndex, mLabels[mDownIndex]);    }        mDownX = mDownY = mDownIndex = -1;    break;   }      return true;  }    /**   * 獲取當前點擊的label的位置   * @return label的位置,沒有點中返回-1   */  private int getClickIndex() {   Rect downRect = new Rect();   Rect locationRect = new Rect();   for(int i=0;i<mLocations.length;i++) {    downRect.set(mDownX - mTextWidthAndHeight[i][0], mDownY      - mTextWidthAndHeight[i][1], mDownX      + mTextWidthAndHeight[i][0], mDownY      + mTextWidthAndHeight[i][1]);        locationRect.set(mLocations[i][0], mLocations[i][1],      mLocations[i][0] + mTextWidthAndHeight[i][0],      mLocations[i][1] + mTextWidthAndHeight[i][1]);        if(locationRect.intersect(downRect)) {     return i;    }   }   return -1;  }    /**   * 開啟子線程不斷刷新位置并postInvalidate   */  private void run() {   if(mThread != null && mThread.isAlive()) {    return;   }      mThread = new Thread(mStartRunning);   mThread.start();  }    private Runnable mStartRunning = new Runnable() {   @Override   public void run() {    for(;;) {     SystemClock.sleep(100);          for (int i = 0; i < mLabels.length; i++) {      if (mLocations[i][0] <= getPaddingLeft()) {       mDirections[i][0] = DIRECTION_RIGHT;      }            if (mLocations[i][0] >= getMeasuredWidth()        - getPaddingRight() - mTextWidthAndHeight[i][0]) {       mDirections[i][0] = DIRECTION_LEFT;      }            if(mLocations[i][1] <= getPaddingTop() + mTextWidthAndHeight[i][1]) {       mDirections[i][1] = DIRECTION_BOTTOM;      }            if (mLocations[i][1] >= getMeasuredHeight() - getPaddingBottom()) {       mDirections[i][1] = DIRECITON_TOP;      }            int xSpeed = 1;      int ySpeed = 2;            if(i < mSpeeds.length) {       xSpeed = mSpeeds[i][0];       ySpeed = mSpeeds[i][1];      }      else {       xSpeed = mSpeeds[i-mSpeeds.length][0];       ySpeed = mSpeeds[i-mSpeeds.length][1];      }            mLocations[i][0] += mDirections[i][0] == DIRECTION_RIGHT ? xSpeed : -xSpeed;      mLocations[i][1] += mDirections[i][1] == DIRECTION_BOTTOM ? ySpeed : -ySpeed;     }          postInvalidate();    }   }  };    /**   * 初始化位置、方向、label寬高   * 并開啟線程   */  private void init() {   if(!hasContents()) {    return;   }      int minX = getPaddingLeft();   int minY = getPaddingTop();   int maxX = getMeasuredWidth() - getPaddingRight();   int maxY = getMeasuredHeight() - getPaddingBottom();      Rect textBounds = new Rect();      for (int i = 0; i < mLabels.length; i++) {    int[] location = new int[2];    location[0] = minX + (int) (Math.random() * maxX);    location[1] = minY + (int) (Math.random() * maxY);        mLocations[i] = location;    mFontSizes[i] = 15 + (int) (Math.random() * 30);    mDirections[i][0] = Math.random() > 0.5 ? DIRECTION_RIGHT : DIRECTION_LEFT;    mDirections[i][1] = Math.random() > 0.5 ? DIRECTION_BOTTOM : DIRECITON_TOP;        mPaint.setTextSize(mFontSizes[i]);    mPaint.getTextBounds(mLabels[i], 0, mLabels[i].length(), textBounds);    mTextWidthAndHeight[i][0] = textBounds.width();    mTextWidthAndHeight[i][1] = textBounds.height();   }      if(!isStatic) run();  }    /**   * 是否設置label   * @return true or false   */  private boolean hasContents() {   return mLabels != null && mLabels.length > 0;  }   /**   * 設置labels   * @see setLabels(String[] labels)   * @param labels   */  public void setLabels(List<String> labels) {   setLabels((String[]) labels.toArray());  }    /**   * 設置labels   * @param labels   */  public void setLabels(String[] labels) {   mLabels = labels;   mLocations = new int[labels.length][2];   mFontSizes = new int[labels.length];   mDirections = new int[labels.length][2];   mTextWidthAndHeight = new int[labels.length][2];      mSpeeds = new int[labels.length][2];   for(int speed[] : mSpeeds) {    speed[0] = speed[1] = 1;   }      requestLayout();  }    /**   * 設置配色方案   * @param colorSchema   */  public void setColorSchema(int[] colorSchema) {   mColorSchema = colorSchema;  }    /**   * 設置每個item的x/y速度   * <p>   * speeds.length > labels.length 忽略多余的   * <p>   * speeds.length < labels.length 將重復使用   *   * @param speeds   */  public void setSpeeds(int[][] speeds) {   mSpeeds = speeds;  }    /**   * 設置item點擊的監聽事件   * @param l   */  public void setOnItemClickListener(OnItemClickListener l) {   getParent().requestDisallowInterceptTouchEvent(true);   mListener = l;  }    /**   * item的點擊監聽事件   */  public interface OnItemClickListener {   public void onItemClick(int index, String label);  } } 

上來先弄了4個常量上去,干嘛用的呢? 是要判斷每個item的方向的,因為當達到某個邊界的時候,item要向相反的方向移動。

第二個構造方法中, 獲取了一個自定義屬性,還有就是初始化的Paint。

繼續看onLayout, 其實onLayout我們什么都沒干,只是調用了init方法, 來看看init方法。

/**  * 初始化位置、方向、label寬高  * 并開啟線程  */ private void init() {  if(!hasContents()) {   return;  }     int minX = getPaddingLeft();  int minY = getPaddingTop();  int maxX = getMeasuredWidth() - getPaddingRight();  int maxY = getMeasuredHeight() - getPaddingBottom();     Rect textBounds = new Rect();     for (int i = 0; i < mLabels.length; i++) {   int[] location = new int[2];   location[0] = minX + (int) (Math.random() * maxX);   location[1] = minY + (int) (Math.random() * maxY);       mLocations[i] = location;   mFontSizes[i] = 15 + (int) (Math.random() * 30);   mDirections[i][0] = Math.random() > 0.5 ? DIRECTION_RIGHT : DIRECTION_LEFT;   mDirections[i][1] = Math.random() > 0.5 ? DIRECTION_BOTTOM : DIRECITON_TOP;       mPaint.setTextSize(mFontSizes[i]);   mPaint.getTextBounds(mLabels[i], 0, mLabels[i].length(), textBounds);   mTextWidthAndHeight[i][0] = textBounds.width();   mTextWidthAndHeight[i][1] = textBounds.height();  }     if(!isStatic) run(); } 

init方法中,上來先判斷一下,是否設置了標簽,如果沒有設置直接返回,省得事多。
10~13行,目的就是獲取item在該view中移動的上下左右邊界,畢竟item還是要在整個view中移動的嘛,不能超出了view的邊界。

17行,開始一個for循環,去遍歷所有的標簽。

18~20行,是隨機初始化一個位置,所以,我們的標簽每次出現的位置都是隨機的,并沒有什么規律,但接下來的移動是有規律的,總不能到處亂蹦吧。

接著,22行,保存了這個位置,因為我們下面要不斷的去修改這個位置。

23行,隨機了一個字體大小,24、25行,隨機了該標簽x/y初始的方向。

27行,去設置了當前標簽的字體大小,28行,是獲取標簽的寬度和高度,并在下面保存在了一個二維數組中,為什么是二維數組,我們有多個標簽嘛, 每個標簽都要保存它的寬度和高度。

最后,如果我們沒有顯示的聲明labelview是靜止的,則去調用run方法。

繼續跟進代碼,看看run方法的內臟。

/**  * 開啟子線程不斷刷新位置并postInvalidate  */ private void run() {  if(mThread != null && mThread.isAlive()) {   return;  }    mThread = new Thread(mStartRunning);  mThread.start(); } 

5~7行,如果線程已經開啟,直接return 防止多個線程共存,這樣造成的后果就是標簽越來越快。
9、10行,去啟動一個線程,并有一個mStartRunning的Runnable參數。

那么我們繼續來看看這個Runnable。

 

private Runnable mStartRunning = new Runnable() {  @Override  public void run() {   for(;;) {    SystemClock.sleep(100);         for (int i = 0; i < mLabels.length; i++) {     if (mLocations[i][0] <= getPaddingLeft()) {      mDirections[i][0] = DIRECTION_RIGHT;     }           if (mLocations[i][0] >= getMeasuredWidth()       - getPaddingRight() - mTextWidthAndHeight[i][0]) {      mDirections[i][0] = DIRECTION_LEFT;     }         if(mLocations[i][1] <= getPaddingTop() + mTextWidthAndHeight[i][1]) {      mDirections[i][1] = DIRECTION_BOTTOM;     }           if (mLocations[i][1] >= getMeasuredHeight() - getPaddingBottom()) {      mDirections[i][1] = DIRECITON_TOP;     }           int xSpeed = 1;     int ySpeed = 2;           if(i < mSpeeds.length) {      xSpeed = mSpeeds[i][0];      ySpeed = mSpeeds[i][1];     }else {      xSpeed = mSpeeds[i-mSpeeds.length][0];      ySpeed = mSpeeds[i-mSpeeds.length][1];     }           mLocations[i][0] += mDirections[i][0] == DIRECTION_RIGHT ? xSpeed : -xSpeed;     mLocations[i][1] += mDirections[i][1] == DIRECTION_BOTTOM ? ySpeed : -ySpeed;    }         postInvalidate();   }  } }; 

這個Runnable其實才是標簽云實現的關鍵,我們就是在這個線程中去修改每個標簽的位置,并通知view去重繪的。
而且可以看到,在run中是一個死循環,這樣我們的標簽才能無休止的移動,接下來就是讓線程去休息100ms,總不能一個勁的去移動吧,速度太快了也不好,也要考慮性能問題。

接下來第7行,去遍歷所有的標簽,8~23行,通過判斷當前的位置是不是達到了某個邊界,如果到了,則修改方向為相反的方向,例如現在到了view的最上面,那接下來,這個標簽就得往下移動了。

25、26行,默認了x/y的速度,為什么是說默認了呢, 因為每個標簽的x/y速度我們都可以通過方法去設置。

接下來28~34行,做了一個判斷,大體意思就是:如果設置的那些速度總數大于當前標簽在標簽s中的位置,則去找對應位置的速度,否則,重新從前面獲取速度。

36、37行就是根據x/y上的方向去修改當前標簽的坐標了。

最后,調用了postInvalidate(),通知view去刷新界面,這里是用的postInvalidate()因為我們是在線程中調用的,切記。

postInvalidate()后,肯定就要走onDraw()去繪制這些標簽了,那么我們就來看看onDraw吧。

@Override protected void onDraw(Canvas canvas) {  if(!hasContents()) {   return;  }     for (int i = 0; i < mLabels.length; i++) {   mPaint.setTextSize(mFontSizes[i]);       if(i < mColorSchema.length) mPaint.setColor(mColorSchema[i]);   else mPaint.setColor(mColorSchema[i-mColorSchema.length]);       canvas.drawText(mLabels[i], mLocations[i][0], mLocations[i][1], mPaint);  } } 

上來還是判斷了一下,如果沒有設置標簽,直接返回。 如果有標簽,那么去遍歷所有標簽,并設置對應的字體大小,還記得嗎? 我們在初始化的時候隨機了每個標簽的字體大小,接下來去設置該標簽的顏色,一個if else 原理和設置速度那個是一樣的,最關鍵的就是下面,調用了canvas.drawText()將該標簽畫到屏幕上,mLocations中我們是保存了每個標簽的位置,而且是在線程中不斷的去修改這個位置的。
到這里,其實我們的LabelView就能動起來了,不過那幾個設置標簽,速度,顏色的方法還有說。其實很簡單,來看一下吧。

 

/**  * 設置labels  * @see setLabels(String[] labels)  * @param labels  */ public void setLabels(List<String> labels) {  setLabels((String[]) labels.toArray()); }   /**  * 設置labels  * @param labels  */ public void setLabels(String[] labels) {  mLabels = labels;  mLocations = new int[labels.length][2];  mFontSizes = new int[labels.length];  mDirections = new int[labels.length][2];  mTextWidthAndHeight = new int[labels.length][2];     mSpeeds = new int[labels.length][2];  for(int speed[] : mSpeeds) {   speed[0] = speed[1] = 1;  }     requestLayout(); }   /**  * 設置配色方案  * @param colorSchema  */ public void setColorSchema(int[] colorSchema) {  mColorSchema = colorSchema; }   /**  * 設置每個item的x/y速度  * <p>  * speeds.length > labels.length 忽略多余的  * <p>  * speeds.length < labels.length 將重復使用  *  * @param speeds  */ public void setSpeeds(int[][] speeds) {  mSpeeds = speeds; } 

這幾個蛋疼的方法中,唯一可說的就是setLabels(String[] labels)了,因為在這個方法中還做了點工作。 仔細觀察,這方法除了設置了標簽s外,其他的就是初始化了幾個數組,都表示什么,相信都應該很清楚了,還有就是在這里我們初始化了默認速度為1。

剛上來做演示的時候,LabelView還能item點擊,這是怎么做到的呢? 普通的onClick肯定是不行的,因為我們并不知道點擊的x/y坐標,所以只能通過onTouchEvent入手了。

@Override public boolean onTouchEvent(MotionEvent ev) {  switch (ev.getAction()) {  case MotionEvent.ACTION_DOWN:   mDownX = (int) ev.getX();   mDownY = (int) ev.getY();   mDownIndex = getClickIndex();   break;  case MotionEvent.ACTION_UP:   int nowX = (int) ev.getX();   int nowY = (int) ev.getY();   if (nowX - mDownX < mTouchSlop && nowY - mDownY < mTouchSlop     && mDownIndex != -1 && mListener != null) {    mListener.onItemClick(mDownIndex, mLabels[mDownIndex]);   }       mDownX = mDownY = mDownIndex = -1;   break;  }     return true; } 

在onTouch中我們只關心了down和up事件,因為一次點擊就是down和up的組合嘛。
在down中,我們獲取了當前事件發生的x/y坐標,并且獲取了當前點擊的item,當前是通過getClickIndex()方法去獲取的,這個方法稍候說;再來看看up,在up中,我們通過當前的x/y和在down時的x/y對比,如果這兩點的距離小于系統認為的最小滑動距離,才能說明點擊有效,如果你down了以后,拉了一個長線,再up,那肯定不是一次有效的點擊,當然點擊有效了還不能說明一切,只有命中標簽了才行,所以還去判斷了mDownIndex是否為一個有效的值,然后如果設置了ItemClick,就去回調它。

那mDownIndex到底是怎么獲取的呢? 我們來getClickIndex()一探究竟。

/**  * 獲取當前點擊的label的位置  * @return label的位置,沒有點中返回-1  */ private int getClickIndex() {  Rect downRect = new Rect();  Rect locationRect = new Rect();  for(int i=0;i<mLocations.length;i++) {   downRect.set(mDownX - mTextWidthAndHeight[i][0], mDownY     - mTextWidthAndHeight[i][1], mDownX     + mTextWidthAndHeight[i][0], mDownY     + mTextWidthAndHeight[i][1]);       locationRect.set(mLocations[i][0], mLocations[i][1],     mLocations[i][0] + mTextWidthAndHeight[i][0],     mLocations[i][1] + mTextWidthAndHeight[i][1]);       if(locationRect.intersect(downRect)) {    return i;   }  }  return -1; } 

首先定義了兩個Rect,一個是點擊的rect,另一個是標簽的rect,然后去遍歷保存的最新的每個標簽的位置,在循環中,我們通過Rect.set()方法分別設置了down的矩形的上下左右和當前標簽的上下左右,然后通過Rect.intersect()方法去判斷這兩個矩形是否有交集,有交集就證明點擊到了該標簽,直接返回該標簽在標簽s中的位置,如果都沒有返回-1表示你丫亂點!

好了,到這里,整個LabelView就弄好了,趕緊去下載源碼體驗一把吧,當然還不算很完美,完美的解決方案等用到它的時候再去解決,嘿嘿,反正我們已經有一個思路了。

哦,對了,還沒給出源碼的下載地址,看這里

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VEVB武林網。


注:相關教程知識閱讀請移步到Android開發頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 宣化县| 任丘市| 武清区| 宜君县| 星子县| 思南县| 北安市| 康乐县| 秭归县| 清涧县| 兰西县| 剑阁县| 简阳市| 昆山市| 大田县| 松原市| 商洛市| 盘山县| 天门市| 峨边| 惠州市| 福州市| 清水河县| 马关县| 滦南县| 伊金霍洛旗| 保定市| 潮州市| 武功县| 湖南省| 仁怀市| 阿巴嘎旗| 慈溪市| 宁远县| 咸阳市| 冷水江市| 阿拉善左旗| 日照市| 藁城市| 尼玛县| 清苑县|