android LabelView實(shí)現(xiàn)標(biāo)簽云效果
今天我們來(lái)做一個(gè)android上的標(biāo)簽云效果, 雖然還不是很完美,但是已經(jīng)足夠可以展現(xiàn)標(biāo)簽云的效果了,首先來(lái)看看效果吧。
額,錄屏只能錄到這個(gè)份上了,湊活著看吧。今天我們就來(lái)實(shí)現(xiàn)一下這個(gè)效果, 這次我選擇直接繼承view來(lái), 什么? 這樣的效果不是SurfaceView擅長(zhǎng)的嗎? 為什么要view,其實(shí)都可以了, 我選擇view,是因?yàn)椋侯~,我對(duì)SurfaceView還不是很熟悉。
廢話少說(shuō), 下面開(kāi)始上代碼
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; // 是否靜止, 默認(rèn)false, 可用干xml : label:is_static="false" private int[][] mLocations; // 每個(gè)label的位置 x/y private int[][] mDirections; // 每個(gè)label的方向 x/y private int[][] mSpeeds; // 每個(gè)label的x/y速度 x/y private int[][] mTextWidthAndHeight; // 每個(gè)labeltext的大小 width/height private String[] mLabels; // 設(shè)置的labels private int[] mFontSizes; // 每個(gè)label的字體大小 // 默認(rèn)配色方案 private int[] mColorSchema = {0XFFFF0000, 0XFF00FF00, 0XFF0000FF, 0XFFCCCCCC, 0XFFFFFFFF}; private int mTouchSlop; // 最小touch private int mDownX = -1; private int mDownY = -1; private int mDownIndex = -1; // 點(diǎn)擊的index private Paint mPaint; private Thread mThread; private OnItemClickListener mListener; // item點(diǎn)擊事件 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; } /** * 獲取當(dāng)前點(diǎn)擊的label的位置 * @return label的位置,沒(méi)有點(diǎn)中返回-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; } /** * 開(kāi)啟子線程不斷刷新位置并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寬高 * 并開(kāi)啟線程 */ 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(); } /** * 是否設(shè)置label * @return true or false */ private boolean hasContents() { return mLabels != null && mLabels.length > 0; } /** * 設(shè)置labels * @see setLabels(String[] labels) * @param labels */ public void setLabels(List<String> labels) { setLabels((String[]) labels.toArray()); } /** * 設(shè)置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(); } /** * 設(shè)置配色方案 * @param colorSchema */ public void setColorSchema(int[] colorSchema) { mColorSchema = colorSchema; } /** * 設(shè)置每個(gè)item的x/y速度 * <p> * speeds.length > labels.length 忽略多余的 * <p> * speeds.length < labels.length 將重復(fù)使用 * * @param speeds */ public void setSpeeds(int[][] speeds) { mSpeeds = speeds; } /** * 設(shè)置item點(diǎn)擊的監(jiān)聽(tīng)事件 * @param l */ public void setOnItemClickListener(OnItemClickListener l) { getParent().requestDisallowInterceptTouchEvent(true); mListener = l; } /** * item的點(diǎn)擊監(jiān)聽(tīng)事件 */ public interface OnItemClickListener { public void onItemClick(int index, String label); } }
上來(lái)先弄了4個(gè)常量上去,干嘛用的呢? 是要判斷每個(gè)item的方向的,因?yàn)楫?dāng)達(dá)到某個(gè)邊界的時(shí)候,item要向相反的方向移動(dòng)。
第二個(gè)構(gòu)造方法中, 獲取了一個(gè)自定義屬性,還有就是初始化的Paint。
繼續(xù)看onLayout, 其實(shí)onLayout我們什么都沒(méi)干,只是調(diào)用了init方法, 來(lái)看看init方法。
/** * 初始化位置、方向、label寬高 * 并開(kāi)啟線程 */ 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方法中,上來(lái)先判斷一下,是否設(shè)置了標(biāo)簽,如果沒(méi)有設(shè)置直接返回,省得事多。
10~13行,目的就是獲取item在該view中移動(dòng)的上下左右邊界,畢竟item還是要在整個(gè)view中移動(dòng)的嘛,不能超出了view的邊界。
17行,開(kāi)始一個(gè)for循環(huán),去遍歷所有的標(biāo)簽。
18~20行,是隨機(jī)初始化一個(gè)位置,所以,我們的標(biāo)簽每次出現(xiàn)的位置都是隨機(jī)的,并沒(méi)有什么規(guī)律,但接下來(lái)的移動(dòng)是有規(guī)律的,總不能到處亂蹦吧。
接著,22行,保存了這個(gè)位置,因?yàn)槲覀兿旅嬉粩嗟娜バ薷倪@個(gè)位置。
23行,隨機(jī)了一個(gè)字體大小,24、25行,隨機(jī)了該標(biāo)簽x/y初始的方向。
27行,去設(shè)置了當(dāng)前標(biāo)簽的字體大小,28行,是獲取標(biāo)簽的寬度和高度,并在下面保存在了一個(gè)二維數(shù)組中,為什么是二維數(shù)組,我們有多個(gè)標(biāo)簽嘛, 每個(gè)標(biāo)簽都要保存它的寬度和高度。
最后,如果我們沒(méi)有顯示的聲明labelview是靜止的,則去調(diào)用run方法。
繼續(xù)跟進(jìn)代碼,看看run方法的內(nèi)臟。
/** * 開(kāi)啟子線程不斷刷新位置并postInvalidate */ private void run() { if(mThread != null && mThread.isAlive()) { return; } mThread = new Thread(mStartRunning); mThread.start(); }
5~7行,如果線程已經(jīng)開(kāi)啟,直接return 防止多個(gè)線程共存,這樣造成的后果就是標(biāo)簽越來(lái)越快。
9、10行,去啟動(dòng)一個(gè)線程,并有一個(gè)mStartRunning的Runnable參數(shù)。
那么我們繼續(xù)來(lái)看看這個(gè)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(); } } };
這個(gè)Runnable其實(shí)才是標(biāo)簽云實(shí)現(xiàn)的關(guān)鍵,我們就是在這個(gè)線程中去修改每個(gè)標(biāo)簽的位置,并通知view去重繪的。
而且可以看到,在run中是一個(gè)死循環(huán),這樣我們的標(biāo)簽才能無(wú)休止的移動(dòng),接下來(lái)就是讓線程去休息100ms,總不能一個(gè)勁的去移動(dòng)吧,速度太快了也不好,也要考慮性能問(wèn)題。
接下來(lái)第7行,去遍歷所有的標(biāo)簽,8~23行,通過(guò)判斷當(dāng)前的位置是不是達(dá)到了某個(gè)邊界,如果到了,則修改方向?yàn)橄喾吹姆较?,例如現(xiàn)在到了view的最上面,那接下來(lái),這個(gè)標(biāo)簽就得往下移動(dòng)了。
25、26行,默認(rèn)了x/y的速度,為什么是說(shuō)默認(rèn)了呢, 因?yàn)槊總€(gè)標(biāo)簽的x/y速度我們都可以通過(guò)方法去設(shè)置。
接下來(lái)28~34行,做了一個(gè)判斷,大體意思就是:如果設(shè)置的那些速度總數(shù)大于當(dāng)前標(biāo)簽在標(biāo)簽s中的位置,則去找對(duì)應(yīng)位置的速度,否則,重新從前面獲取速度。
36、37行就是根據(jù)x/y上的方向去修改當(dāng)前標(biāo)簽的坐標(biāo)了。
最后,調(diào)用了postInvalidate(),通知view去刷新界面,這里是用的postInvalidate()因?yàn)槲覀兪窃诰€程中調(diào)用的,切記。
postInvalidate()后,肯定就要走onDraw()去繪制這些標(biāo)簽了,那么我們就來(lái)看看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); } }
上來(lái)還是判斷了一下,如果沒(méi)有設(shè)置標(biāo)簽,直接返回。 如果有標(biāo)簽,那么去遍歷所有標(biāo)簽,并設(shè)置對(duì)應(yīng)的字體大小,還記得嗎? 我們?cè)诔跏蓟臅r(shí)候隨機(jī)了每個(gè)標(biāo)簽的字體大小,接下來(lái)去設(shè)置該標(biāo)簽的顏色,一個(gè)if else 原理和設(shè)置速度那個(gè)是一樣的,最關(guān)鍵的就是下面,調(diào)用了canvas.drawText()將該標(biāo)簽畫(huà)到屏幕上,mLocations中我們是保存了每個(gè)標(biāo)簽的位置,而且是在線程中不斷的去修改這個(gè)位置的。
到這里,其實(shí)我們的LabelView就能動(dòng)起來(lái)了,不過(guò)那幾個(gè)設(shè)置標(biāo)簽,速度,顏色的方法還有說(shuō)。其實(shí)很簡(jiǎn)單,來(lái)看一下吧。
/** * 設(shè)置labels * @see setLabels(String[] labels) * @param labels */ public void setLabels(List<String> labels) { setLabels((String[]) labels.toArray()); } /** * 設(shè)置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(); } /** * 設(shè)置配色方案 * @param colorSchema */ public void setColorSchema(int[] colorSchema) { mColorSchema = colorSchema; } /** * 設(shè)置每個(gè)item的x/y速度 * <p> * speeds.length > labels.length 忽略多余的 * <p> * speeds.length < labels.length 將重復(fù)使用 * * @param speeds */ public void setSpeeds(int[][] speeds) { mSpeeds = speeds; }
這幾個(gè)蛋疼的方法中,唯一可說(shuō)的就是setLabels(String[] labels)了,因?yàn)樵谶@個(gè)方法中還做了點(diǎn)工作。 仔細(xì)觀察,這方法除了設(shè)置了標(biāo)簽s外,其他的就是初始化了幾個(gè)數(shù)組,都表示什么,相信都應(yīng)該很清楚了,還有就是在這里我們初始化了默認(rèn)速度為1。
剛上來(lái)做演示的時(shí)候,LabelView還能item點(diǎn)擊,這是怎么做到的呢? 普通的onClick肯定是不行的,因?yàn)槲覀儾⒉恢傈c(diǎn)擊的x/y坐標(biāo),所以只能通過(guò)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中我們只關(guān)心了down和up事件,因?yàn)橐淮吸c(diǎn)擊就是down和up的組合嘛。
在down中,我們獲取了當(dāng)前事件發(fā)生的x/y坐標(biāo),并且獲取了當(dāng)前點(diǎn)擊的item,當(dāng)前是通過(guò)getClickIndex()方法去獲取的,這個(gè)方法稍候說(shuō);再來(lái)看看up,在up中,我們通過(guò)當(dāng)前的x/y和在down時(shí)的x/y對(duì)比,如果這兩點(diǎn)的距離小于系統(tǒng)認(rèn)為的最小滑動(dòng)距離,才能說(shuō)明點(diǎn)擊有效,如果你down了以后,拉了一個(gè)長(zhǎng)線,再u(mài)p,那肯定不是一次有效的點(diǎn)擊,當(dāng)然點(diǎn)擊有效了還不能說(shuō)明一切,只有命中標(biāo)簽了才行,所以還去判斷了mDownIndex是否為一個(gè)有效的值,然后如果設(shè)置了ItemClick,就去回調(diào)它。
那mDownIndex到底是怎么獲取的呢? 我們來(lái)getClickIndex()一探究竟。
/** * 獲取當(dāng)前點(diǎn)擊的label的位置 * @return label的位置,沒(méi)有點(diǎn)中返回-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; }
首先定義了兩個(gè)Rect,一個(gè)是點(diǎn)擊的rect,另一個(gè)是標(biāo)簽的rect,然后去遍歷保存的最新的每個(gè)標(biāo)簽的位置,在循環(huán)中,我們通過(guò)Rect.set()方法分別設(shè)置了down的矩形的上下左右和當(dāng)前標(biāo)簽的上下左右,然后通過(guò)Rect.intersect()方法去判斷這兩個(gè)矩形是否有交集,有交集就證明點(diǎn)擊到了該標(biāo)簽,直接返回該標(biāo)簽在標(biāo)簽s中的位置,如果都沒(méi)有返回-1表示你丫亂點(diǎn)!
好了,到這里,整個(gè)LabelView就弄好了,趕緊去下載源碼體驗(yàn)一把吧,當(dāng)然還不算很完美,完美的解決方案等用到它的時(shí)候再去解決,嘿嘿,反正我們已經(jīng)有一個(gè)思路了。
哦,對(duì)了,還沒(méi)給出源碼的下載地址,看這里
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
HandlerThread的使用場(chǎng)景和用法詳解
這篇文章主要介紹了HandlerThread的使用場(chǎng)景和用法詳解,HandlerThread是Android中的一個(gè)線程類,它是Thread的子類,并且內(nèi)部封裝了Looper和Handler,提供了更方便的消息處理和線程操作,需要的朋友可以參考下2023-07-07基于android startActivityForResult的學(xué)習(xí)心得總結(jié)
本篇文章是對(duì)android中的startActivityForResult進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05Notification消息通知 自定義消息通知內(nèi)容布局
這篇文章主要為大家詳細(xì)介紹了Notification消息通知,消息合并且顯示條數(shù),自定義消息通知內(nèi)容布局,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09Android編程之Button控件配合Toast控件用法分析
這篇文章主要介紹了Android編程之Button控件配合Toast控件用法,結(jié)合實(shí)例形式分析了Button控件及Toast控件的功能及具體使用技巧,需要的朋友可以參考下2015-12-12Android嵌套滾動(dòng)與協(xié)調(diào)滾動(dòng)的實(shí)現(xiàn)方式匯總
如何實(shí)現(xiàn)這種協(xié)調(diào)滾動(dòng)的布局呢,我們使用CoordinatorLayout+AppBarLayout或者CoordinatorLayout+Behavior實(shí)現(xiàn),另一種方案是MotionLayout,我們看看都是怎么實(shí)現(xiàn)的吧2022-06-06Android 限制edittext 整數(shù)和小數(shù)位數(shù) 過(guò)濾器(詳解)
下面小編就為大家?guī)?lái)一篇Android 限制edittext 整數(shù)和小數(shù)位數(shù) 過(guò)濾器(詳解)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04詳解Android PopupWindow怎么合理控制彈出位置(showAtLocation)
本篇文章主要介紹了詳解Android PopupWindow怎么合理控制彈出位置(showAtLocation),具有一定的參考價(jià)值,有興趣的可以了解一下2017-10-10