Android基于繪制緩沖實(shí)現(xiàn)煙花效果
前言
三月以前,我也寫(xiě)過(guò)《Android 煙花效果》,這篇我相當(dāng)于做了個(gè)基礎(chǔ)框架,在此基礎(chǔ)上擴(kuò)展和填充,就能擴(kuò)展出很多效果。不過(guò),當(dāng)時(shí),我在這篇文章中著重強(qiáng)調(diào)了一件事
重點(diǎn):構(gòu)建閉合空間
之所以強(qiáng)調(diào)這件事的原因是,只有閉合空間的圖形才能填充顏色、圖片紋理。我們知道,Canvas 繪制方法僅僅只有圓、弧、矩形、圓角矩形是可以閉合的,除此之外就是Path了。
想象一下,如果讓你畫(huà)一個(gè)三角形并填充上顏色,你可能的方法只有通過(guò)裁剪Path或者使用Path繪制才行,而Path也有性能問(wèn)題。
另外,閉合空間的填充也是件不容易的事。
所以,那篇文章中的煙花效果,本質(zhì)上還不夠完美,因?yàn)橐恍┨厥獾奶畛湫Ч€是很難實(shí)現(xiàn)。
新方案
目前我覺(jué)得可行的方案有兩種
基于數(shù)學(xué)和Paint線寬漸變
如:貝塞爾曲線函數(shù) + strokeWidth漸漸增大 + Color 變化
這種方式是利用貝塞爾曲線計(jì)算出路徑(不用Path,根據(jù)數(shù)學(xué)公式描繪),然后再規(guī)定的時(shí)間內(nèi)讓Paint的strokeWidth隨著貝塞爾曲線 * time的偏移而增大,就能繪制出效果不錯(cuò)的的煙花條。
基于繪制緩沖
首先,要知道什么是緩沖,緩沖其實(shí)就是通常意義上存儲(chǔ)數(shù)據(jù)的對(duì)象,比如byte數(shù)組、ByteBuffer等,但如果再聚焦Android 平臺(tái),我們還有FBO、VBO等。當(dāng)然,最容易被忽略的是Bitmap,Bitmap 其實(shí)也是FBO的一種,不過(guò)這里我稱之為“可視化緩沖”。
如果追蹤的具體的對(duì)象上,除了Bitmap之外,Layer也是緩沖。
為什么使用緩沖可以優(yōu)化煙花效果呢?
我們先了解下緩沖的特性:
- 占用空間較大,狹義上來(lái)說(shuō),這種數(shù)據(jù)不僅僅占用空間大,而且(虛擬)內(nèi)存需要連續(xù)
- 空間可復(fù)用性強(qiáng),如享元模式的ByteBuffer、alpha離屏渲染buffer、inBitmap等
- 會(huì)產(chǎn)生臟數(shù)據(jù),比如上一次buffer中的數(shù)據(jù),如果沒(méi)有清理的話依然會(huì)保存
- 數(shù)據(jù)可復(fù)用性強(qiáng),臟數(shù)據(jù)并不一定“臟”,有時(shí)還能復(fù)用
我們最終利用的還是空間可復(fù)用性和數(shù)據(jù)可復(fù)用性,如果我們以每次都在上次的數(shù)據(jù)中繪制,那么,意味著可以繪制出更多效果,間接解決了閉合空間填充問(wèn)題。
那么,本篇我們選哪種呢?
最終方案
本篇,我們就選擇基于緩沖的方案了,因?yàn)榭偟膩?lái)說(shuō),第一種方式可能需要很多次的繪制,相當(dāng)考驗(yàn)CPU。而使用繪制緩沖的的話,我們還可以復(fù)用上次的數(shù)據(jù),這就相當(dāng)于將上一次的繪制畫(huà)面保留,然后再一次繪制時(shí),在之前的基礎(chǔ)上進(jìn)一步完善,這種顯然是利用“空間換取時(shí)間”的做法。
詳細(xì)設(shè)計(jì)
本篇使用了繪制緩沖,原則上使用Bitmap是可以的,但是在使用的過(guò)程中發(fā)現(xiàn),Bitmap在xformode繪制時(shí)性能還是很差,顯然提升流暢度是必要原則。那么,你可能想到利用線程異步繪制,是的,我也打算這么做,但是想到使用線程渲染,那為什么不使用TextureView、SurfaceView或者GLSurfaceView呢?于是,我就沒(méi)有再使用Bitmap的想法了。
但是,基于做以往的經(jīng)驗(yàn),我選了個(gè)兼容性最好性能最差的TextureView,其實(shí)我這里本打算選GLSurfaceView的,因?yàn)槠湫阅芎图嫒菪远际蔷又兴?,不過(guò)涉及到頂點(diǎn)、紋理的一套東西,打算后續(xù)在音視頻專欄寫(xiě)這類文章,因此本篇就選TexureView了。
簡(jiǎn)單說(shuō)下SurfaceView的問(wèn)題,性能最好,但其不適合在滑動(dòng)的頁(yè)面調(diào)用,因?yàn)橛行┰O(shè)備會(huì)出現(xiàn)畫(huà)面漂移和縮放的問(wèn)題,另外不支持clipchildren等,理論上也是適合本篇的,但是如果app回到后臺(tái),其Surface會(huì)自動(dòng)銷毀,因此,控制線程的邏輯就會(huì)有些復(fù)雜。
在這里我們看下TextureView源碼,其創(chuàng)建的SurfaceTexture并不是單緩沖模式,但是又有設(shè)置緩沖bufferSize大小的操作,此外TextLayer負(fù)責(zé)提供緩沖,因此,這里至少是雙緩沖。
mLayer = mAttachInfo.mThreadedRenderer.createTextureLayer(); boolean createNewSurface = (mSurface == null); if (createNewSurface) { // Create a new SurfaceTexture for the layer. mSurface = new SurfaceTexture(false); //非單緩沖 nCreateNativeWindow(mSurface); } mLayer.setSurfaceTexture(mSurface); mSurface.setDefaultBufferSize(getWidth(), getHeight()); mSurface.setOnFrameAvailableListener(mUpdateListener, mAttachInfo.mHandler); if (mListener != null && createNewSurface) { mListener.onSurfaceTextureAvailable(mSurface, getWidth(), getHeight()); } mLayer.setLayerPaint(mLayerPaint); }
下面是我們的詳細(xì)流程。
實(shí)現(xiàn)煙花邏輯
下面是我們本篇的實(shí)現(xiàn)流程。
定義FireExploreView
我們本篇基于TextureView實(shí)現(xiàn)繪制邏輯,而TextureView必須要開(kāi)啟硬件加速,其次我們要實(shí)現(xiàn)TextureView.SurfaceTextureListener,用于監(jiān)聽(tīng)SurfaceTexture的創(chuàng)建和銷毀。理論上,TextureView的SurfaceTexture可以復(fù)用的,其次,如果onSurfaceTextureDestroyed返回false,那么SurfaceTexture的銷毀是由你自己控制的,TextureView不會(huì)主動(dòng)銷毀。
@Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return false; }
另外,我們要知道,默認(rèn)情況下TextureView使用的是TextureLayer,繪制完成之后,需要在RenderThread上使用gl去合成,這也是性能較差的主要原因。尤其是低配設(shè)備,使用TextureView也做不到性能優(yōu)化,最終還是得使用SurfaceView或者GLTextureView或者GLSurfaceView,當(dāng)然我比較推薦GL系列,主要是離屏渲染可以避免MediaCodec切換Surface引發(fā)黑屏和卡住的問(wèn)題。
當(dāng)然,這里我們肯定也要使用到線程和Surface了,相關(guān)代碼如下
@Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { drawThread = new Thread(this); this.surfaceTexture = surfaceTexture; this.surface = new Surface(this.surfaceTexture); this.isRunning = true; this.drawThread.start(); } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { isRunning = false; if (drawThread != null) { try { drawThread.interrupt(); }catch (Throwable e){ e.printStackTrace(); } } drawThread = null; //不讓TextureView 銷毀SurfaceTexture,這里返回false return false; }
定義粒子
無(wú)論任何時(shí)候,不要把粒子不當(dāng)對(duì)象,一些開(kāi)發(fā)者對(duì)粒子對(duì)象嗤之以鼻,這顯然是不對(duì)的,不受管理的粒子憑什么聽(tīng)你的指揮。
當(dāng)然,任何粒子的運(yùn)動(dòng)需要符合運(yùn)動(dòng)學(xué)方程,而二維平面的運(yùn)動(dòng)是可以拆分為X軸和Y軸單方向的運(yùn)動(dòng)的。
static final float gravity = 0.0f; static final float fraction = 0.88f; static final float speed = 50f; //最大速度 static class Particle { private float opacity; //透明度 private float dy; // y 軸速度 private float dx; // x 軸速度 private int color; //此顏色 private float radius; //半徑 private float y; // y坐標(biāo) private float x; // x坐標(biāo) Particle(float x, float y, float r, int color, float speedX, float speedY) { this.x = x; this.y = y; this.radius = r; this.color = color; this.dx = speedX; this.dy = speedY; this.opacity = 1f; } void draw(Canvas canvas, Paint paint) { int save = canvas.save(); paint.setAlpha((int) (this.opacity * 255)); paint.setColor(this.color); canvas.drawCircle(this.x, this.y, this.radius, paint); canvas.restoreToCount(save); } void update() { this.dy += gravity; //加上重力因子,那么就會(huì)出現(xiàn)粒子重力現(xiàn)象,這里我們不使用時(shí)間了,這樣簡(jiǎn)單點(diǎn) this.dx *= fraction; // fraction 是小于1的,用于降低速度 this.dy *= fraction; // fraction 是小于1的,用于降低速度 this.x += this.dx; this.y += this.dy; this.opacity -= 0.03; //透明度遞減 } }
上面是粒子以及更新方法、繪制邏輯。
管理粒子
我們使用List管理粒子
static final int maxParticleCount = 300; List<Particle> particles = new ArrayList<>(maxParticleCount);
初始化粒子
粒子的初始化是非常重要的,初始化位置的正確與否會(huì)影響粒子的整體效果,顯然,這里我們需要注意。
float angleIncrement = (float) ((Math.PI * 2) / maxParticleCount); //平分 360度 float[] hsl = new float[3]; for (int i = 0; i < maxParticleCount; i++) { hsl[0] = (float) (Math.random() * 360); hsl[1] = 0.5f; hsl[2] = 0.5f; int hslToColor = HSLToColor(hsl); Particle p = new Particle(x, y, 2.5f, hslToColor, (float) (Math.cos(angleIncrement * i) * Math.random() * speed), (float) (Math.sin(angleIncrement * i) * Math.random() * speed) ); particles.add(p); }
不過(guò),在這里我們還需要注意的是,這里我們使用HLS,這是一種色彩空間,和RGB不一樣的是,他有Hue(色調(diào))、飽和度、亮度為基準(zhǔn),因此,有利于亮色的表示,因此適合獲取強(qiáng)調(diào)亮度的色彩。
與rgb的轉(zhuǎn)換邏輯如下
public static int HSLToColor(@NonNull float[] hsl) { final float h = hsl[0]; final float s = hsl[1]; final float l = hsl[2]; final float c = (1f - Math.abs(2 * l - 1f)) * s; final float m = l - 0.5f * c; final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f)); final int hueSegment = (int) h / 60; int r = 0, g = 0, b = 0; switch (hueSegment) { case 0: r = Math.round(255 * (c + m)); g = Math.round(255 * (x + m)); b = Math.round(255 * m); break; case 1: r = Math.round(255 * (x + m)); g = Math.round(255 * (c + m)); b = Math.round(255 * m); break; case 2: r = Math.round(255 * m); g = Math.round(255 * (c + m)); b = Math.round(255 * (x + m)); break; case 3: r = Math.round(255 * m); g = Math.round(255 * (x + m)); b = Math.round(255 * (c + m)); break; case 4: r = Math.round(255 * (x + m)); g = Math.round(255 * m); b = Math.round(255 * (c + m)); break; case 5: case 6: r = Math.round(255 * (c + m)); g = Math.round(255 * m); b = Math.round(255 * (x + m)); break; } r = constrain(r, 0, 255); g = constrain(g, 0, 255); b = constrain(b, 0, 255); return Color.rgb(r, g, b); } private static int constrain(int amount, int low, int high) { return amount < low ? low : Math.min(amount, high); }
粒子繪制
繪制當(dāng)然簡(jiǎn)單了,方法實(shí)現(xiàn)不是很復(fù)雜,調(diào)用如下邏輯即可,當(dāng)然,opacity<=0 的粒子我們并沒(méi)有移除,原因是因?yàn)閞emove 時(shí), 可能引發(fā)ArrayList內(nèi)存重整,這個(gè)是相當(dāng)消耗性能的,因此,還不如遍歷效率高。
protected void drawParticles(Canvas canvas) { canvas.drawColor(0x10000000); //為了讓煙花減弱效果,每次加深繪制 for (int i = 0; i < particles.size(); i++) { Particle particle = particles.get(i); if (particle.opacity > 0) { particle.draw(canvas, mPaint); particle.update(); } } }
緩沖復(fù)用
那么,以上就是完整的繪制邏輯了,至于Surface調(diào)用邏輯呢,其實(shí)也很簡(jiǎn)單。
不過(guò)這里要注意的是,只有接受到command=true的時(shí)候,我們才清理畫(huà)布,不然,我們要保留緩沖區(qū)中的數(shù)據(jù)。我們知道,一般View在onDraw的時(shí)候,RenderNode給你的Canvas都是清理過(guò)的,而這里,我們每次通過(guò)lockCanvas拿到的Canvas是帶有上次緩沖數(shù)據(jù)的。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { canvas = surface.lockHardwareCanvas(); } else { canvas = surface.lockCanvas(null); } if(isCommand){ canvas.drawColor(0x99000000, PorterDuff.Mode.CLEAR); //清理畫(huà)布 explode(getWidth() / 2f, getHeight() / 2f); //粒子初始化 isCommand = false; } //繪制粒子 drawParticles(canvas); surface.unlockCanvasAndPost(canvas);
顯然,我們能得到兩條經(jīng)驗(yàn):
- lockCanvas獲取到的Canvas是帶有上次繪制數(shù)據(jù)的
- 利用緩沖繪制不僅強(qiáng)調(diào)結(jié)果,而且還強(qiáng)調(diào)過(guò)程,一般的Canvas繪制僅僅強(qiáng)調(diào)結(jié)果
Blend效果增強(qiáng)
實(shí)際上面的效果還有點(diǎn)差,就是尖端亮度太低,為此,我們可以使用Blend進(jìn)行增強(qiáng),我們?cè)O(shè)置BlendMode為PLUS,另外上面我們的重力是0,現(xiàn)在我們調(diào)整一下gravity=0.25f。
PaintCompat.setBlendMode(mPaint, BlendModeCompat.PLUS);
效果
多線程繪制
總的來(lái)說(shuō),TextureView可以在一些情況下顯著提升性能,當(dāng)然,前提是你的主線程流暢。
這里的邏輯就是TextureView的用法了,我們就不繼續(xù)深入了,本篇末尾提供源碼。
新問(wèn)題
評(píng)論區(qū)有同學(xué)反饋,在真機(jī)上很卡,我試了一下,發(fā)現(xiàn)不是卡,而是TextureView 不是單緩沖,兩次緩沖在沒(méi)有CLEAR時(shí)會(huì)有交替閃爍問(wèn)題。
因此,為了優(yōu)化閃爍問(wèn)題,我把可視化緩沖Bitmap重新加進(jìn)來(lái),使用之后在上是沒(méi)有問(wèn)題的,但是由于Android 6.0 之前的系統(tǒng)無(wú)法使用lockHardwareCanvas,卡頓是比較明顯的。
為啥模擬器表現(xiàn)比較好,可能刷新率比較低。
性能優(yōu)化
由于使用Bitmap作為緩沖,性能有所降低,我們這里進(jìn)行如下優(yōu)化
- 減少繪制區(qū)域大小
- 移除Surface 清理 canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
- Android 6.0+版本使用硬件Canvas
縮小繪制區(qū)域收益明顯,后續(xù)考慮先縮小后繪制,再利用Matrix放大。
總結(jié)
以上是本篇的內(nèi)容,也是我們要掌握的技巧,很多時(shí)候,我們對(duì)Canvas的繪制,過(guò)于強(qiáng)調(diào)結(jié)果,結(jié)果設(shè)計(jì)了很多復(fù)雜的算法,其實(shí),基于過(guò)程的繪制顯然更加簡(jiǎn)單和優(yōu)化。
到這里本篇就結(jié)束了,希望本篇對(duì)你有所幫助。
源碼
public class FireExploreView extends TextureView implements TextureView.SurfaceTextureListener, Runnable { private TextPaint mPaint; private SurfaceTexture surfaceTexture; private Surface surface; private BitmapCanvas mBitmapCanvas; private boolean updateOnSizeChanged = false; private volatile boolean isRunning = false; private final Object lockSurface = new Object(); { initPaint(); } public FireExploreView(Context context) { this(context, null); } public FireExploreView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); setSurfaceTextureListener(this); } private void initPaint() { //否則提供給外部紋理繪制 mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL_AND_STROKE); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setStyle(Paint.Style.FILL); PaintCompat.setBlendMode(mPaint, BlendModeCompat.PLUS); } static final float gravity = 0.21f; static final float fraction = 0.88f; static final int maxParticleCount = 300; List<Particle> particles = new ArrayList<>(maxParticleCount); float[] hsl = new float[3]; volatile boolean isCommand = false; static final float speed = 60f; Thread drawThread = null; public void startExplore() { isCommand = true; } //初始化粒子 void explode(float x, float y) { float angleIncrement = (float) ((Math.PI * 2) / maxParticleCount); for (int i = 0; i < maxParticleCount; i++) { hsl[0] = (float) (Math.random() * 360); hsl[1] = 0.5f; hsl[2] = 0.5f; int hslToColor = HSLToColor(hsl); Particle p = null; if (particles.size() > i) { p = particles.get(i); } if (p == null) { p = new Particle(); particles.add(p); } p.init(x, y, 4f, hslToColor, (float) (Math.cos(angleIncrement * i) * Math.random() * speed), (float) (Math.sin(angleIncrement * i) * Math.random() * speed) ); } } protected void drawParticles(Canvas canvas) { canvas.drawColor(0x10000000); for (int i = 0; i < particles.size(); i++) { Particle particle = particles.get(i); if (particle.opacity > 0) { particle.draw(canvas, mPaint); particle.update(); } } } static class Particle { private float opacity; private float dy; private float dx; private int color; private float radius; private float y; private float x; public void init(float x, float y, float r, int color, float speedX, float speedY) { this.x = x; this.y = y; this.radius = r; this.color = color; this.dx = speedX; this.dy = speedY; this.opacity = 1f; } void draw(Canvas canvas, Paint paint) { int save = canvas.save(); paint.setColor(argb((int) (this.opacity * 255),Color.red(this.color),Color.green(this.color),Color.blue(this.color))); canvas.drawCircle(this.x, this.y, this.radius, paint); canvas.restoreToCount(save); } void update() { this.dy += gravity; this.dx *= fraction; this.dy *= fraction; this.x += this.dx; this.y += this.dy; this.opacity -= 0.02; } } Matrix matrix = new Matrix(); @Override public void run() { while (true) { synchronized (this) { try { this.wait(16); } catch (InterruptedException e) { e.printStackTrace(); } } if (!isRunning || Thread.currentThread().isInterrupted()) { synchronized (lockSurface) { if (surface != null && surface.isValid()) { surface.release(); } surface = null; } break; } Canvas canvas = null; synchronized (lockSurface) { if(mBitmapCanvas == null || updateOnSizeChanged) { updateOnSizeChanged = false; mBitmapCanvas = createBitmapCanvas(getWidth(),getHeight()); } if(isCommand){ mBitmapCanvas.bitmap.eraseColor(0x00000000); explode(mBitmapCanvas.getWidth() / 2f, mBitmapCanvas.getHeight() / 2f); isCommand = false; } //這里其實(shí)目前沒(méi)有加鎖的必要,考慮到如果有其他SurfaceTexture相關(guān)操作會(huì)加鎖,這里先加鎖吧 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { canvas = surface.lockHardwareCanvas(); }else { canvas = surface.lockCanvas(null); } Bitmap bitmap = mBitmapCanvas.getBitmap(); drawParticles(mBitmapCanvas); matrix.reset(); matrix.setTranslate((getWidth() - bitmap.getWidth()) / 2f, (getHeight() - bitmap.getHeight()) / 2f); canvas.drawBitmap(mBitmapCanvas.getBitmap(), matrix, null); surface.unlockCanvasAndPost(canvas); } } } private BitmapCanvas createBitmapCanvas(int width,int height){ if(mBitmapCanvas != null){ mBitmapCanvas.recycle(); } int size = Math.max(Math.min(width,height),1); return new BitmapCanvas(Bitmap.createBitmap(size,size, Bitmap.Config.ARGB_8888)); } static class BitmapCanvas extends Canvas{ Bitmap bitmap; public BitmapCanvas(Bitmap bitmap) { super(bitmap); this.bitmap = bitmap; } public Bitmap getBitmap() { return bitmap; } public void recycle() { if(bitmap == null || bitmap.isRecycled()){ return; } bitmap.recycle(); } } @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { this.drawThread = new Thread(this); this.surfaceTexture = surfaceTexture; this.surface = new Surface(this.surfaceTexture); this.isRunning = true; this.drawThread.start(); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { updateOnSizeChanged = true; } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { isRunning = false; if (drawThread != null) { try { drawThread.interrupt(); }catch (Throwable e){ e.printStackTrace(); } } drawThread = null; return false; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { } @ColorInt public static int HSLToColor(@NonNull float[] hsl) { final float h = hsl[0]; final float s = hsl[1]; final float l = hsl[2]; final float c = (1f - Math.abs(2 * l - 1f)) * s; final float m = l - 0.5f * c; final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f)); final int hueSegment = (int) h / 60; int r = 0, g = 0, b = 0; switch (hueSegment) { case 0: r = Math.round(255 * (c + m)); g = Math.round(255 * (x + m)); b = Math.round(255 * m); break; case 1: r = Math.round(255 * (x + m)); g = Math.round(255 * (c + m)); b = Math.round(255 * m); break; case 2: r = Math.round(255 * m); g = Math.round(255 * (c + m)); b = Math.round(255 * (x + m)); break; case 3: r = Math.round(255 * m); g = Math.round(255 * (x + m)); b = Math.round(255 * (c + m)); break; case 4: r = Math.round(255 * (x + m)); g = Math.round(255 * m); b = Math.round(255 * (c + m)); break; case 5: case 6: r = Math.round(255 * (c + m)); g = Math.round(255 * m); b = Math.round(255 * (x + m)); break; } r = constrain(r, 0, 255); g = constrain(g, 0, 255); b = constrain(b, 0, 255); return Color.rgb(r, g, b); } private static int constrain(int amount, int low, int high) { return amount < low ? low : Math.min(amount, high); } public static int argb( @IntRange(from = 0, to = 255) int alpha, @IntRange(from = 0, to = 255) int red, @IntRange(from = 0, to = 255) int green, @IntRange(from = 0, to = 255) int blue) { return (alpha << 24) | (red << 16) | (green << 8) | blue; } public void release(){ synchronized (lockSurface) { isRunning = false; updateOnSizeChanged = false; if (surface != null && surface.isValid()) { surface.release(); } surface = null; } } }
以上就是Android基于繪制緩沖實(shí)現(xiàn)煙花效果的詳細(xì)內(nèi)容,更多關(guān)于Android煙花效果的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android?Flutter在點(diǎn)擊事件上添加動(dòng)畫(huà)效果實(shí)現(xiàn)全過(guò)程
這篇文章主要給大家介紹了關(guān)于Android?Flutter在點(diǎn)擊事件上添加動(dòng)畫(huà)效果實(shí)現(xiàn)的相關(guān)資料,通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)Android具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-03-03Flutter?fluro時(shí)報(bào)錯(cuò)type?'String'?is?not?a?subty
這篇文章主要介紹了Flutter使用fluro時(shí)報(bào)錯(cuò)type?'String'?is?not?a?subtype?of?type?'Queue<Task>'解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助2023-12-12Android編程實(shí)現(xiàn)ListView中item部分區(qū)域添加點(diǎn)擊事件功能
這篇文章主要介紹了Android編程實(shí)現(xiàn)ListView中item部分區(qū)域添加點(diǎn)擊事件功能,涉及Android ListView相關(guān)適配器及事件響應(yīng)操作技巧,需要的朋友可以參考下2018-01-01android開(kāi)發(fā)教程之獲取使用當(dāng)前api的應(yīng)用程序名稱
開(kāi)發(fā)手機(jī)安全管家的時(shí)候,比如要打電話,或者照相需要知道是哪個(gè)應(yīng)用程序在調(diào)用,就可以在API接口中調(diào)用下面的代碼2014-02-02Android動(dòng)態(tài)添加menu菜單的簡(jiǎn)單方法
Android動(dòng)態(tài)添加menu菜單的簡(jiǎn)單方法,需要的朋友可以參考一下2013-06-06Android 圖片處理避免出現(xiàn)oom的方法詳解
本篇文章主要介紹了Android 圖片處理避免出現(xiàn)oom的方法詳解,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09