Android使用Canvas?2D實(shí)現(xiàn)循環(huán)菜單效果
一、前言
循環(huán)菜單有很多種自定義方式,我們可以利用ViewPager或者RecyclerView + CarouselLayoutManager 或者RecyclerView + PageSnapHelper來(lái)實(shí)現(xiàn)這種效果,今天我們使用Canvas 2D來(lái)實(shí)現(xiàn)這種效果。
二、實(shí)現(xiàn)
LoopView 是常見(jiàn)的循環(huán) View,一般應(yīng)用于循環(huán)展示菜單項(xiàng)目,本次實(shí)現(xiàn)的是一組循環(huán)菜單,按照垂直方向,實(shí)際上,如果把某些變量互換,可以實(shí)現(xiàn)輪播圖效果。
最終目標(biāo)
- 在滑動(dòng)過(guò)程中記錄偏移的位置,將畫(huà)出界面的從列表中移除,分別向兩端添加。
- 離中心點(diǎn)越近,半徑就會(huì)越大
- 模仿Recyler機(jī)制,偏移到界面以外的item回收利用
2.1 定義菜單項(xiàng)
首先這里定義一下菜單Item,主要標(biāo)記顏色和文本內(nèi)容
public static class LoopItem { private int color; private String text; public LoopItem(String text, int color) { this.color = color; this.text = text; } public int getColor() { return color; } public void setColor(int color) { this.color = color; } public String getText() { return text; } public void setText(String text) { this.text = text; } }
接下來(lái)需要定義繪制任務(wù),將菜單數(shù)據(jù)和繪制任務(wù)解耦。
我們這里需要
- 半徑
- x,y坐標(biāo)
- 半徑縮放增量
public static class DrawTask<T extends LoopItem> { private T loopItem; private float radius; //半徑,定值 private float x; private float y; private float scaleOffset = 0; // 半徑縮放偏移量,離中心越遠(yuǎn),此值就會(huì)越小 public DrawTask(float x, float y, float radius) { this.radius = radius; this.x = x; this.y = y; } public void setLoopItem(T loopItem) { this.loopItem = loopItem; } public void draw(Canvas canvas, TextPaint textPaint) { if (loopItem == null) return; textPaint.setColor(loopItem.getColor()); textPaint.setStyle(Paint.Style.FILL); textPaint.setShadowLayer(10, 0, 5, 0x99444444); //繪制圓 canvas.drawCircle(x, y, radius + scaleOffset, textPaint); textPaint.setColor(Color.WHITE); textPaint.setStyle(Paint.Style.FILL); //繪制文本 String text = loopItem.getText(); float textWidth = textPaint.measureText(text); float baseline = getTextPaintBaseline(textPaint); canvas.drawText(text, -textWidth / 2, y + baseline, textPaint); } public T getLoopItem() { return loopItem; } }
2.2 半徑計(jì)算
半徑計(jì)算其實(shí)只需要按默的最小邊的一半除以要展示的數(shù)量,為什么要這樣計(jì)算呢?因?yàn)檫@樣可以保證圓心等距,我們這里實(shí)現(xiàn)的效果其實(shí)是放大圓而不是縮小圓的方式,因此,默認(rèn)情況
int MAX_VISIBLE_COUNT = 5 //這個(gè)值建議是奇數(shù) circleRadius = Math.min(w / 2F, h / 2F) / MAX_VISIBLE_COUNT;
2.3 通過(guò)位置偏移進(jìn)行復(fù)用和回收
這里主要是模仿Recycler機(jī)制,對(duì)DrawTask回收和復(fù)用
//回收前處理,保證偏移連續(xù) private void recyclerBefore(int height) { if (isTouchEventUp) { float centerOffset = getMinYOffset(); resetItemYOffset(height, centerOffset); } else { resetItemYOffset(height, offsetY); } isTouchEventUp = false; } //回收后處理,保證Item連續(xù) private void recyclerAfter(int height) { if (isTouchEventUp) { float centerOffset = getMinYOffset(); resetItemYOffset(height, centerOffset); } else { resetItemYOffset(height, 0); } } //進(jìn)行回收和復(fù)用,用head和tail指針對(duì)兩側(cè)外的Item移除和復(fù)用 private void recycler() { if (drawTasks.size() < (MAX_VISIBLE_COUNT - 2)) return; Collections.sort(drawTasks, drawTaskComparatorY); DrawTask head = drawTasks.get(0); //head 指針 DrawTask tail = drawTasks.get(drawTasks.size() - 1); //尾指針 int height = getHeight(); if (head.y < -(height / 2F + circleRadius)) { drawTasks.remove(head); addToCachePool(head); head.setLoopItem(null); //回收 } else { DrawTask drawTask = getCachePool(); //復(fù)用 LoopItem loopItem = head.getLoopItem(); LoopItem preLoopItem = getPreLoopItem(loopItem); drawTask.setLoopItem(preLoopItem); drawTask.y = head.y - circleRadius * 2; drawTasks.add(0, drawTask); } if (tail.y > (height / 2F + circleRadius)) { drawTasks.remove(tail); addToCachePool(tail); tail.setLoopItem(null); } else { DrawTask drawTask = getCachePool(); LoopItem loopItem = tail.getLoopItem(); LoopItem nextLoopItem = getNextLoopItem(loopItem); drawTask.setLoopItem(nextLoopItem); drawTask.y = tail.y + circleRadius * 2; drawTasks.add(drawTask); } }
2.4 防止靠近中心的View被繪制
遠(yuǎn)離中心的Item要先繪制,意味著靠近邊緣的要優(yōu)先繪制,防止蓋住中心的Item,因此每次都需要排序 這里的outOffset半徑偏移值,半徑越小的就會(huì)排在前面
Collections.sort(drawTasks, new Comparator<DrawTask>() { @Override public int compare(DrawTask left, DrawTask right) { float dx = Math.abs(left.y) - Math.abs(right.y); if (dx > 0) { return 1; } if (dx == 0) { return 0; } return -1; } });
2.5 獲取離中心點(diǎn)最近的Item的y值
scaleOffset越大,離圓心越近,通過(guò)這種方式就能篩選出靠近圓心的Item Y坐標(biāo)
private float getMinYOffset() { float minY = 0; float offset = 0; for (int i = 0; i < drawTasks.size(); i++) { DrawTask drawTask = drawTasks.get(i); if (Math.abs(drawTask.scaleOffset) > offset) { minY = -drawTask.y; offset = drawTask.scaleOffset; } } return minY; }
2.6 根據(jù)滑動(dòng)方向重新計(jì)算每個(gè)item的偏移
Item是需要移動(dòng)的,因此在事件處理的時(shí)候一定要進(jìn)行偏移處理,因此滑動(dòng)過(guò)程需要對(duì)Y值進(jìn)行有效處理,當(dāng)然要避免為1,防止View出現(xiàn)縮小而不是滑動(dòng)的效果。
private void resetItemYOffset(int height, float centerOffset) { for (int i = 0; i < drawTasks.size(); i++) { DrawTask task = drawTasks.get(i); task.y = (task.y + centerOffset); float ratio = Math.abs(task.y) / (height / 2F); if (ratio > 1f) { ratio = 1f; } task.outOffset = ((10 + circleRadius) * 3 / 4f) * (1 - ratio); } }
2.7 事件處理
我們要支持Item移動(dòng),因此必然要處理TouchEvent,首先我們需要在ACTION_DOWN時(shí)攔截事件,其次需要處理ACTION_MOVE事件和ACTION_UP事件中產(chǎn)生的位置偏移。
另外,我們保留系統(tǒng)內(nèi)默認(rèn)View對(duì)事件處理的方式,具體原理就是在onTouchEvent返回之前調(diào)用super.onTouchEvent方法
super.onTouchEvent(event); return true;
下面是事件處理完整的方法,基本是常規(guī)操作
@Override public boolean onTouchEvent(MotionEvent event) { int action = event.getActionMasked(); isTouchEventUp = false; switch (action) { case MotionEvent.ACTION_DOWN: offsetY = 0; startEventX = event.getX() - getWidth() / 2F; startEventY = event.getY() - getHeight() / 2F; super.onTouchEvent(event); return true; case MotionEvent.ACTION_MOVE: float eventX = event.getX(); float eventY = event.getY(); if (eventY < 0) { eventY = 0; } if (eventX < 0) { eventX = 0; } if (eventY > getWidth()) { eventX = getWidth(); } if (eventY > getHeight()) { eventY = getHeight(); } float currentX = eventX - getWidth() / 2F; float currentY = eventY - getHeight() / 2F; float dx = currentX - startEventX; float dy = currentY - startEventY; if (Math.abs(dx) < Math.abs(dy) && Math.abs(dy) >= slopTouch) { isTouchMove = true; } if (!isTouchMove) { break; } offsetY = dy; startEventX = currentX; startEventY = currentY; postInvalidate(); super.onTouchEvent(event); return true; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_OUTSIDE: case MotionEvent.ACTION_UP: isTouchMove = false; isTouchEventUp = true; offsetY = 0; Log.d("eventup", "offsetY=" + offsetY); postInvalidate(); break; } return super.onTouchEvent(event); }
三、使用方法
使用方法
LoopView loopView = findViewById(R.id.loopviews); final List<LoopView.LoopItem> loopItems = new ArrayList<>(); int[] colors = { Color.RED, Color.CYAN, Color.GRAY, Color.GREEN, Color.BLACK, Color.MAGENTA, 0xffff9922, 0xffFF4081, 0xffFFEAC4 }; String[] items = { "新聞", "科技", "歷史", "軍事", "小說(shuō)", "娛樂(lè)", "電影", "電視劇", }; for (int i = 0; i < items.length; i++) { LoopView.LoopItem loopItem = new LoopView.LoopItem(items[i], colors[i % colors.length]); loopItems.add(loopItem); } loopView.setLoopItems(loopItems); } LoopView loopView = new LoopView(this); loopView.setLoopItems(loopItems); FrameLayout frameLayout = new FrameLayout(this); FrameLayout.MarginLayoutParams layoutParams = new FrameLayout.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,720); layoutParams.topMargin = 100; layoutParams.leftMargin = 50; layoutParams.rightMargin = 50; frameLayout.addView(loopView,layoutParams); setContentView(frameLayout);
四、總結(jié)
4.1 整體效果
其實(shí)效果上還是可以的,本質(zhì)上和ListView和RecyclerView思想類似,但是循環(huán)這一塊兒其實(shí)和WheelView 思想類似。
4.2 點(diǎn)擊事件處理
實(shí)際上本篇的View市支持點(diǎn)擊事件的,當(dāng)時(shí)點(diǎn)擊區(qū)域沒(méi)有判斷,不過(guò)也是比較好處理,只要對(duì)DrawTask排序,保證最中間的Item可以點(diǎn)擊即可,篇幅有限,這里就不處理了。
4.3 全部代碼
public class LoopView extends View { private static final int MAX_VISIBLE_COUNT = 5; private TextPaint mTextPaint = null; private DisplayMetrics displayMetrics = null; private int mLineWidth = 1; private int mTextSize = 14; private int slopTouch = 0; private float circleRadius; private final List<DrawTask> drawTasks = new ArrayList<>(); private final List<DrawTask> cacheDrawTasks = new ArrayList<>(); private final List<LoopItem> loopItems = new ArrayList<>(); boolean isInit = false; private float startEventX = 0; private float startEventY = 0; private boolean isTouchMove = false; private float offsetY = 0; boolean isTouchEventUp = false; public LoopView(Context context) { this(context, null); } public LoopView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public LoopView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setClickable(true); setFocusable(true); setFocusableInTouchMode(true); displayMetrics = context.getResources().getDisplayMetrics(); mTextPaint = createPaint(); slopTouch = ViewConfiguration.get(context).getScaledTouchSlop(); setLayerType(LAYER_TYPE_SOFTWARE, null); initDesignEditMode(); } private void initDesignEditMode() { if (!isInEditMode()) return; int[] colors = { Color.RED, Color.CYAN, Color.YELLOW, Color.GRAY, Color.GREEN, Color.BLACK, Color.MAGENTA, 0xffff9922, }; String[] items = { "新聞", "科技", "歷史", "軍事", "小說(shuō)", "娛樂(lè)", "電影", "電視劇", }; for (int i = 0; i < items.length; i++) { LoopItem loopItem = new LoopItem(items[i], colors[i % colors.length]); loopItems.add(loopItem); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); if (widthMode != MeasureSpec.EXACTLY) { widthSize = displayMetrics.widthPixels; } int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY) { heightSize = (int) (displayMetrics.widthPixels * 0.9f); } setMeasuredDimension(widthSize, heightSize); } private TextPaint createPaint() { // 實(shí)例化畫(huà)筆并打開(kāi)抗鋸齒 TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG); paint.setAntiAlias(true); paint.setStrokeWidth(dpTopx(mLineWidth)); paint.setTextSize(dpTopx(mTextSize)); return paint; } private float dpTopx(float dp) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); } /** * 基線到中線的距離=(Descent+Ascent)/2-Descent * 注意,實(shí)際獲取到的Ascent是負(fù)數(shù)。公式推導(dǎo)過(guò)程如下: * 中線到BOTTOM的距離是(Descent+Ascent)/2,這個(gè)距離又等于Descent+中線到基線的距離,即(Descent+Ascent)/2=基線到中線的距離+Descent。 */ public static float getTextPaintBaseline(Paint p) { Paint.FontMetrics fontMetrics = p.getFontMetrics(); return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); circleRadius = Math.min(w / 2F, h / 2F) / MAX_VISIBLE_COUNT; } Comparator<DrawTask> drawTaskComparator = new Comparator<DrawTask>() { @Override public int compare(DrawTask left, DrawTask right) { float dx = Math.abs(right.y) - Math.abs(left.y); if (dx > 0) { return 1; } if (dx == 0) { return 0; } return -1; } }; Comparator<DrawTask> drawTaskComparatorY = new Comparator<DrawTask>() { @Override public int compare(DrawTask left, DrawTask right) { float dx = left.y - right.y; if (dx > 0) { return 1; } if (dx == 0) { return 0; } return -1; } }; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int width = getWidth(); int height = getHeight(); int id = canvas.save(); canvas.translate(width / 2F, height / 2F); initCircle(); //前期重置,以便recycler復(fù)用 recyclerBefore(height); //復(fù)用和移除 recycler(); //再次處理,防止view復(fù)用之后產(chǎn)生其他位移 recyclerAfter(height); Collections.sort(drawTasks, drawTaskComparator); for (int i = 0; i < drawTasks.size(); i++) { drawTasks.get(i).draw(canvas, mTextPaint); } drawGuideline(canvas, width); canvas.restoreToCount(id); } private float getMinYOffset() { float minY = 0; float offset = 0; for (int i = 0; i < drawTasks.size(); i++) { DrawTask drawTask = drawTasks.get(i); if (Math.abs(drawTask.scaleOffset) > offset) { minY = -drawTask.y; offset = drawTask.scaleOffset; } } return minY; } private void recyclerAfter(int height) { if (isTouchEventUp) { float centerOffset = getMinYOffset(); resetItemYOffset(height, centerOffset); } else { resetItemYOffset(height, 0); } } private void recyclerBefore(int height) { if (isTouchEventUp) { float centerOffset = getMinYOffset(); resetItemYOffset(height, centerOffset); } else { resetItemYOffset(height, offsetY); } isTouchEventUp = false; } private void recycler() { if (drawTasks.size() < (MAX_VISIBLE_COUNT - 2)) return; Collections.sort(drawTasks, drawTaskComparatorY); DrawTask head = drawTasks.get(0); DrawTask tail = drawTasks.get(drawTasks.size() - 1); int height = getHeight(); if (head.y < -(height / 2F + circleRadius)) { drawTasks.remove(head); addToCachePool(head); head.setLoopItem(null); } else { DrawTask drawTask = getCachePool(); LoopItem loopItem = head.getLoopItem(); LoopItem preLoopItem = getPreLoopItem(loopItem); drawTask.setLoopItem(preLoopItem); drawTask.y = head.y - circleRadius * 2; drawTasks.add(0, drawTask); } if (tail.y > (height / 2F + circleRadius)) { drawTasks.remove(tail); addToCachePool(tail); tail.setLoopItem(null); } else { DrawTask drawTask = getCachePool(); LoopItem loopItem = tail.getLoopItem(); LoopItem nextLoopItem = getNextLoopItem(loopItem); drawTask.setLoopItem(nextLoopItem); drawTask.y = tail.y + circleRadius * 2; drawTasks.add(drawTask); } } private void resetItemYOffset(int height, float scaleOffset) { for (int i = 0; i < drawTasks.size(); i++) { DrawTask task = drawTasks.get(i); task.y = (task.y + scaleOffset); float ratio = Math.abs(task.y) / (height / 2F); if (ratio > 1f) { ratio = 1f; } task.scaleOffset = ((10 + circleRadius) * 3 / 4f) * (1 - ratio); } } RectF guideRect = new RectF(); private void drawGuideline(Canvas canvas, int width) { if (!isInEditMode()) return; mTextPaint.setColor(Color.BLACK); mTextPaint.setStyle(Paint.Style.FILL); int i = 0; int counter = 0; while (counter < MAX_VISIBLE_COUNT) { float topY = i * 2 * circleRadius; guideRect.left = -width / 2f; guideRect.right = width / 2f; guideRect.top = topY - 0.5f; guideRect.bottom = topY + 0.5f; canvas.drawRect(guideRect, mTextPaint); counter++; float bottomY = -i * 2 * circleRadius; if (topY == bottomY) { i++; continue; } guideRect.top = bottomY - 0.5f; guideRect.bottom = bottomY + 0.5f; canvas.drawRect(guideRect, mTextPaint); counter++; i++; } } private LoopItem getNextLoopItem(LoopItem loopItem) { int index = loopItems.indexOf(loopItem); if (index < loopItems.size() - 1) { return loopItems.get(index + 1); } return loopItems.get(0); } private LoopItem getPreLoopItem(LoopItem loopItem) { int index = loopItems.indexOf(loopItem); if (index > 0) { return loopItems.get(index - 1); } return loopItems.get(loopItems.size() - 1); } private DrawTask getCachePool() { if (cacheDrawTasks.size() > 0) { return cacheDrawTasks.remove(0); } DrawTask drawTask = createDrawTask(); return drawTask; } private void addToCachePool(DrawTask top) { cacheDrawTasks.add(top); } private void initCircle() { if (isInit) { return; } isInit = true; List<DrawTask> drawTaskList = new ArrayList<>(); int i = 0; while (drawTaskList.size() < MAX_VISIBLE_COUNT) { float topY = i * 2 * circleRadius; DrawTask drawTask = new DrawTask(0, topY, circleRadius); drawTaskList.add(drawTask); float bottomY = -i * 2 * circleRadius; if (topY == bottomY) { i++; continue; } drawTask = new DrawTask(0, bottomY, circleRadius); drawTaskList.add(drawTask); i++; } Collections.sort(drawTaskList, new Comparator<DrawTask>() { @Override public int compare(DrawTask left, DrawTask right) { float dx = left.y - right.y; if (dx > 0) { return 1; } if (dx == 0) { return 0; } return -1; } }); drawTasks.clear(); if (loopItems.size() == 0) return; for (int j = 0; j < drawTaskList.size(); j++) { drawTaskList.get(j).setLoopItem(loopItems.get(j % loopItems.size())); } drawTasks.addAll(drawTaskList); } private DrawTask createDrawTask() { DrawTask drawTask = new DrawTask(0, 0, circleRadius); return drawTask; } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getActionMasked(); isTouchEventUp = false; switch (action) { case MotionEvent.ACTION_DOWN: offsetY = 0; startEventX = event.getX() - getWidth() / 2F; startEventY = event.getY() - getHeight() / 2F; return true; case MotionEvent.ACTION_MOVE: float eventX = event.getX(); float eventY = event.getY(); if (eventY < 0) { eventY = 0; } if (eventX < 0) { eventX = 0; } if (eventY > getWidth()) { eventX = getWidth(); } if (eventY > getHeight()) { eventY = getHeight(); } float currentX = eventX - getWidth() / 2F; float currentY = eventY - getHeight() / 2F; float dx = currentX - startEventX; float dy = currentY - startEventY; if (Math.abs(dx) < Math.abs(dy) && Math.abs(dy) >= slopTouch) { isTouchMove = true; } if (!isTouchMove) { break; } offsetY = dy; startEventX = currentX; startEventY = currentY; postInvalidate(); return true; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_OUTSIDE: case MotionEvent.ACTION_UP: isTouchMove = false; isTouchEventUp = true; offsetY = 0; Log.d("eventup", "offsetY=" + offsetY); invalidate(); break; } return super.onTouchEvent(event); } public void setLoopItems(List<LoopItem> loopItems) { this.loopItems.clear(); this.drawTasks.clear(); this.cacheDrawTasks.clear(); this.isInit = false; if (loopItems != null) { this.loopItems.addAll(loopItems); } postInvalidate(); } public static class DrawTask<T extends LoopItem> { private T loopItem; private float radius; private float x; private float y; private float scaleOffset = 0; public DrawTask(float x, float y, float radius) { this.radius = radius; this.x = x; this.y = y; } public void setLoopItem(T loopItem) { this.loopItem = loopItem; } public void draw(Canvas canvas, TextPaint textPaint) { if (loopItem == null) return; textPaint.setColor(loopItem.getColor()); textPaint.setStyle(Paint.Style.FILL); textPaint.setShadowLayer(10, 0, 5, 0x99444444); canvas.drawCircle(x, y, radius + scaleOffset, textPaint); textPaint.setColor(Color.WHITE); textPaint.setStyle(Paint.Style.FILL); String text = loopItem.getText(); float textWidth = textPaint.measureText(text); float baseline = getTextPaintBaseline(textPaint); textPaint.setShadowLayer(0, 0, 0, Color.TRANSPARENT); canvas.drawText(text, -textWidth / 2, y + baseline, textPaint); } public T getLoopItem() { return loopItem; } } public static class LoopItem { private int color; private String text; public LoopItem(String text, int color) { this.color = color; this.text = text; } public int getColor() { return color; } public void setColor(int color) { this.color = color; } public String getText() { return text; } public void setText(String text) { this.text = text; } } }
以上就是Android使用Canvas 2D實(shí)現(xiàn)循環(huán)菜單效果的詳細(xì)內(nèi)容,更多關(guān)于Android Canvas 2D循環(huán)菜單的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android 網(wǎng)絡(luò)請(qǐng)求框架解析之okhttp與okio
HTTP是現(xiàn)代應(yīng)用常用的一種交換數(shù)據(jù)和媒體的網(wǎng)絡(luò)方式,高效地使用HTTP能讓資源加載更快,節(jié)省帶寬,OkHttp是一個(gè)高效的HTTP客戶端,下面這篇文章主要給大家介紹了關(guān)于OkHttp如何用于安卓網(wǎng)絡(luò)請(qǐng)求,需要的朋友可以參考下2021-10-10Android實(shí)現(xiàn)控件拖動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)控件拖動(dòng)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07Android自定義控件實(shí)現(xiàn)驗(yàn)證碼倒計(jì)時(shí)
這篇文章主要為大家詳細(xì)介紹了Android自定義控件實(shí)現(xiàn)驗(yàn)證碼倒計(jì)時(shí)的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03OpenGL Shader實(shí)例分析(8)彩色光圈效果
這篇文章主要為大家詳細(xì)介紹了OpenGL Shader實(shí)例分析第8篇,彩色光圈效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02Android 自定義View實(shí)現(xiàn)多節(jié)點(diǎn)進(jìn)度條功能
這篇文章主要介紹了Android 自定義View實(shí)現(xiàn)多節(jié)點(diǎn)進(jìn)度條,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-05-05Android入門(mén)之日歷選擇與時(shí)間選擇組件的使用
這篇文章主要為大家詳細(xì)介紹了Android中TimePicker時(shí)間選擇與DatePicker日期選擇組件的使用方法,文中的示例代碼講解詳細(xì),需要的朋友可以參考下2022-11-11Android實(shí)現(xiàn)iPhone晃動(dòng)撤銷輸入功能 Android仿微信搖一搖功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)iPhone晃動(dòng)撤銷輸入功能,Android仿微信搖一搖功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07