ゼロ。はじめに
最近、ある場所に動画の特殊効果を加えてほしいという依頼があったのですが、最終的にプランが決まり、実現方法は2つありました。
1.表示するロティアニメーション
2つ目はGIF風のアニメーションを表示する方法です。
時間のコストを考慮すると、GIF のような方法が推奨されます。
主な理由は、サードパーティの画像読み込みフレームワークが、画像のダウンロード、表示、リサイクル、キャッシュなどを含む完全な画像表示プロセスをカプセル化しているためです。直接呼び出すこともできますが、UI が与えるアニメーションが WebP 形式であるため、使用する過程で多くの困難な問題が発生しましたが、幸いにも最終的に原因が判明し、解決されました。
1. 具体的な実装コード
依存関係を追加する
//Glide库
//implementation 'com.github.bumptech.glide:glide:4.7.1'//support
implementation 'com.github.bumptech.glide:glide:4.12.0'//androidx
annotationProcessor "com.github.bumptech.glide:compiler:4.12.0"//androidx
//Glide支持webp动图的库
implementation "com.github.zjupure:webpdecoder:2.0.4.12.0"
XML 内には通常の ImageView があるため、ここには掲載しません
Java レイヤー コードを具体的に実装する
WebpDrawable mWebpDrawable = null;
private void startWebpGifAni(ImageView iv,String url,int defaultIcon){
// if(mWebpDrawable!=null&&!mWebpDrawable.isRunning()){
// mWebpDrawable.startFromFirstFrame();
// mWebpDrawable.stop();
// }
//webp动图
Transformation<Bitmap> transformation = new CenterInside();
Glide.with(this)
.load(url)//不是本地资源就改为url即可
.optionalTransform(transformation)
.optionalTransform(WebpDrawable.class, new WebpDrawableTransformation(transformation))
.addListener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
iv.setImageResource(defaultIcon);
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
if (resource instanceof WebpDrawable) {
mWebpDrawable = (WebpDrawable) resource;
try {
//已知三方库的bug,webp的动图每一帧的时间间隔于实际的有所偏差,需要反射三方库去修改
//https://github.com/zjupure/GlideWebpDecoder/issues/33
Field gifStateField = mWebpDrawable.getClass().getDeclaredField("state");
gifStateField.setAccessible(true);//开放权限
Class gifStateClass = Class.forName("com.bumptech.glide.integration.webp.decoder.WebpDrawable$WebpState");
Field gifFrameLoaderField = gifStateClass.getDeclaredField("frameLoader");
gifFrameLoaderField.setAccessible(true);
Class gifFrameLoaderClass = Class.forName("com.bumptech.glide.integration.webp.decoder.WebpFrameLoader");
Field gifDecoderField = gifFrameLoaderClass.getDeclaredField("webpDecoder");
gifDecoderField.setAccessible(true);
WebpDecoder webpDecoder = (WebpDecoder) gifDecoderField.get(gifFrameLoaderField.get(gifStateField.get(resource)));
Field durations = webpDecoder.getClass().getDeclaredField("mFrameDurations");
durations.setAccessible(true);
int[] args = (int[]) durations.get(webpDecoder);
if (args.length > 0) {
for (int i = 0; i < args.length; i++) {
if (args[i] > 30) {
//加载glide会比ios慢 这边把gif的间隔减少15s
args[i] = args[i] - 15;
}
}
}
durations.set(webpDecoder, args);
} catch (Exception e) {
e.printStackTrace();
}
//需要设置为循环1次才会有onAnimationEnd回调
mWebpDrawable.setLoopCount(1);
mWebpDrawable.registerAnimationCallback(new Animatable2Compat.AnimationCallback() {
@Override
public void onAnimationStart(Drawable drawable) {
super.onAnimationStart(drawable);
}
@Override
public void onAnimationEnd(Drawable drawable) {
super.onAnimationEnd(drawable);
//第二次播放webp动图的时候 会显示改webp动图最后一帧的图片 然后才能正常显示
if (mWebpDrawable != null && !mWebpDrawable.isRunning()) {
mWebpDrawable.startFromFirstFrame();
mWebpDrawable.stop();
}
mWebpDrawable.unregisterAnimationCallback(this);
}
});
}
return false;
}
})
// .skipMemoryCache(true)
.into(iv);
}
private void cancelGifOnResume(){
//解决使用webp动图播放一次的时候 页面重新显示之后 webp动图还会播放一次的问题
//在onresume调用即可
if(mWebpDrawable==null){
return;
}
try {
Field isRunning = mWebpDrawable.getClass().getDeclaredField("isRunning");
isRunning.setAccessible(true);
isRunning.setBoolean(mWebpDrawable,true);
} catch (Exception e) {
e.printStackTrace();
}
}
OK、ここにあるコードはこれですべてです。
次は、これらの問題を解決する方法についての私のアイデアです。少し長くなるかもしれません。興味があれば読んでください。もちろん、より良い方法がある場合は、指摘してください。コメントエリア。
2. 発生した問題と解決策
当初の計画が決まったときに、まずプロジェクト内の既存のコードを試してみたところ、アニメーションが表示できないことがわかりました。baidu を少し調べたところ、fresco が WebP アニメーションの表示をサポートしていることがわかりました。また、投稿します。コードはここにあります。
private void startAni(SimpleDraweeView iv, String webp1){
ControllerListener controllerListener = new BaseControllerListener<ImageInfo>() {
@Override
public void onFinalImageSet(String id, @Nullable ImageInfo imageInfo, @Nullable Animatable animatable) {
if (animatable != null && AnimatedDrawable2.class.isInstance(animatable)) {
final AnimatedDrawable2 animatedDrawable2 = (AnimatedDrawable2) animatable;
animatedDrawable2.start();
final int totalCnt = animatedDrawable2.getFrameCount();
animatedDrawable2.setAnimationListener(new BaseAnimationListener() {
private int lastFrame; //防止无限循环 适时退出动画
@Override
public void onAnimationFrame(AnimatedDrawable2 drawable, int frameNumber) {
if (!(lastFrame == 0 && totalCnt <= 1) && lastFrame <= frameNumber) {
lastFrame = frameNumber;
} else {
animatedDrawable2.stop();
}
}
@Override
public void onAnimationStart(AnimatedDrawable2 drawable) {
lastFrame = -1;
}
@Override
public void onAnimationStop(AnimatedDrawable2 drawable) {
}
});
}
};
};
DraweeController controller = Fresco.newDraweeControllerBuilder()
.setUri(Uri.parse(webp1))
.setOldController(iv.getController())
.setControllerListener(controllerListener)
.build();
iv.setController(controller);
}
しかし、フレスコの三者パッケージが大きすぎ、オリジナルプロジェクトが導入されていないため、この計画は無効となります。。。
その後、WebP アニメーションをサポートできる Glide の拡張ライブラリ webpdecoder を見つけました。
しかし、使用する過程でいくつかの問題が見つかりました。
1. アニメーションの再生速度が少し遅い
コードを統合した後、アニメーションが少し遅くなることがわかったので、コードを深く掘り下げて原因を見つけました
//WebpDrawable.java
public void startFromFirstFrame() {
Preconditions.checkArgument(!isRunning, "You cannot restart a currently running animation.");
state.frameLoader.setNextStartFromFirstFrame();
start();
}
public void start() {
isStarted = true;
resetLoopCount();
if(isVisible) {
startRunning();
}
}
private void startRunning() {
Preconditions.checkArgument(!isRecycled, "You cannot start a recycled Drawable. Ensure thatyou clear any references to the Drawable when clearing the corresponding request.");
if(state.frameLoader.getFrameCount() == 1) {
invalidateSelf();
} else if(!isRunning) {
isRunning = true;
state.frameLoader.subscribe(this);
invalidateSelf();
}
}
画像が最終的に state.frameLoader.subscribe(this); このコード行によってロードされ、webpDrawable の作成時に FrameLoader が提供されることがわかります。
WebpDrawable(WebpFrameLoader frameLoader, BitmapPool bitmapPool, Paint paint) {
this(new WebpState(bitmapPool, frameLoader));
this.paint = paint;
}
次に見てみましょう
//WebpFrameLoader.java
void subscribe(FrameCallback frameCallback) {
if (isCleared) {
throw new IllegalStateException("Cannot subscribe to a cleared frame loader");
}
if (callbacks.contains(frameCallback)) {
throw new IllegalStateException("Cannot subscribe twice in a row");
}
boolean start = callbacks.isEmpty();
callbacks.add(frameCallback);
if (start) {
start();
}
}
private void start() {
if (isRunning) {
return;
}
isRunning = true;
isCleared = false;
loadNextFrame();
}
private void loadNextFrame() {
if (!isRunning || isLoadPending) {
return;
}
if (startFromFirstFrame) {
Preconditions.checkArgument(
pendingTarget == null, "Pending target must be null when starting from the first frame");
webpDecoder.resetFrameIndex();
startFromFirstFrame = false;
}
if (pendingTarget != null) {
DelayTarget temp = pendingTarget;
pendingTarget = null;
onFrameReady(temp);
return;
}
isLoadPending = true;
// Get the delay before incrementing the pointer because the delay indicates the amount of time
// we want to spend on the current frame.
int delay = webpDecoder.getNextDelay();
long targetTime = SystemClock.uptimeMillis() + delay;
webpDecoder.advance();
int frameIndex = webpDecoder.getCurrentFrameIndex();
next = new DelayTarget(handler, frameIndex, targetTime);
WebpFrameCacheStrategy cacheStrategy = webpDecoder.getCacheStrategy();
RequestOptions options = RequestOptions.signatureOf(getFrameSignature(frameIndex))
.skipMemoryCache(cacheStrategy.noCache());
requestBuilder.apply(options).load(webpDecoder).into(next);
}
各フレームのピクチャは、loadNextFrame メソッドを通じて読み取られて表示されていることがわかります。特定の表示時間は、これら 2 行のコードによって制御されます。
int 遅延 = webpDecoder.getNextDelay(); 長い targetTime = SystemClock.uptimeMillis() + 遅延;
引き続き、webpDecoder の関連ソース コードを見てみましょう。
//webpDecoder.java
private final int[] mFrameDurations;
private final WebpFrameInfo[] mFrameInfos;
@Override
public int getNextDelay() {
if (mFrameDurations.length == 0 || mFramePointer < 0) {
return 0;
}
return getDelay(mFramePointer);
}
@Override
public int getDelay(int n) {
int delay = -1;
if ((n >= 0) && (n < mFrameDurations.length)) {
delay = mFrameDurations[n];
}
return delay;
}
これは、WebpImage クラスのネイティブ メソッドによって返されるパラメーター mFrameDurations によって制御されていることがわかります。次に、各フレーム間の間隔を制御するには、このクラスのこの属性を取得するだけで済みます。
試してみましたが、直接設定する方法はありません。リフレクションを使用してこのオブジェクトを見つけ、このパラメータを変更します。
try {
//已知三方库的bug,webp的动图每一帧的时间间隔于实际的有所偏差,需要反射三方库去修改
//https://github.com/zjupure/GlideWebpDecoder/issues/33
Field gifStateField = mWebpDrawable.getClass().getDeclaredField("state");
gifStateField.setAccessible(true);//开放权限
Class gifStateClass = Class.forName("com.bumptech.glide.integration.webp.decoder.WebpDrawable$WebpState");
Field gifFrameLoaderField = gifStateClass.getDeclaredField("frameLoader");
gifFrameLoaderField.setAccessible(true);
Class gifFrameLoaderClass = Class.forName("com.bumptech.glide.integration.webp.decoder.WebpFrameLoader");
Field gifDecoderField = gifFrameLoaderClass.getDeclaredField("webpDecoder");
gifDecoderField.setAccessible(true);
WebpDecoder webpDecoder = (WebpDecoder) gifDecoderField.get(gifFrameLoaderField.get(gifStateField.get(resource)));
Field durations = webpDecoder.getClass().getDeclaredField("mFrameDurations");
durations.setAccessible(true);
int[] args = (int[]) durations.get(webpDecoder);
if (args.length > 0) {
for (int i = 0; i < args.length; i++) {
if (args[i] > 30) {
//加载glide会比ios慢 这边把gif的间隔减少15s
args[i] = args[i] - 15;
}
}
}
durations.set(webpDecoder, args);
} catch (Exception e) {
e.printStackTrace();
}
ここで私が言ったのは、各フレームを 15 ミリ秒ずつ高速化するということです。この値は固定されておらず、必要に応じて自分で変更できますが、変更された配列の長さが元の配列の長さ以上であることを確認する必要があります。配列。
2. ページが切り替わった後、シングルプレイ アニメーションが再度再生されます (onResume を呼び出します)。
この問題はインターフェイスの切り替え時に発見されましたが、その後、どのように設定してもこの問題は回避できないことが判明し、その後、ページの再開時にコンポーネントが startFromFirstFrame メソッドを 1 回呼び出すことが判明しました。Glide はページのライフサイクルを監視するため、インターフェースに切り替えるたびに startFromFirstFrame メソッドが呼び出されることが予想されます。
このメソッドが不要な場合は、isRunning を true に設定する必要があります
//WebpDrawable.java
public void startFromFirstFrame() {
Preconditions.checkArgument(!isRunning, "You cannot restart a currently running animation.");
state.frameLoader.setNextStartFromFirstFrame();
start();
}
public void start() {
isStarted = true;
resetLoopCount();
if(isVisible) {
startRunning();
}
}
private void startRunning() {
Preconditions.checkArgument(!isRecycled, "You cannot start a recycled Drawable. Ensure thatyou clear any references to the Drawable when clearing the corresponding request.");
if(state.frameLoader.getFrameCount() == 1) {
invalidateSelf();
} else if(!isRunning) {
//只要将isRunning设置成true 动画将不会被触发
isRunning = true;
state.frameLoader.subscribe(this);
invalidateSelf();
}
}
最後に、ページの onresume で isRunning を true に設定することにしました。
@Override
protected void onResume() {
super.onResume();
cancelGifOnResume();
...
}
private void cancelGifOnResume(){
if(mWebpDrawable==null){
return;
}
try {
Field isRunning = mWebpDrawable.getClass().getDeclaredField("isRunning");
isRunning.setAccessible(true);
isRunning.setBoolean(mWebpDrawable,true);
} catch (Exception e) {
e.printStackTrace();
}
}
3. アニメーションを 2 回目に再生すると、最初のフレームがアニメーションの最後のフレームになります。
これは効果の最終テスト中に発見され、オンライン後に得られた方法は 1 つだけです。
//禁止Glide缓存gif图片,否则会导致每次切换页面会先显示gif图片最后一帧,然后才开始播放动画
RequestOptions options = new RequestOptions() .skipMemoryCache(true);
この問題を回避するには、キャッシュをスキップするように設定します。実際の測定では、このプロセス中に小さな WebP アニメーション効果がまだ可能ですが、アニメーションがわずかに大きい限り、効果は特に良くなく、明らかに空白になります。最初(読み込み処理の途中)、その後は正常に表示できるようになります。結局のところ、メモリから画像を取得する速度が最も速いです。
次に、github にアクセスし、Google 検索しても結果がありません。落ち着いて、インターネット上でこのソリューションのアイデアを再分析してください。
SkipMemoryCache(true) は、メモリ キャッシュやディスク キャッシュを使用しないことを意味します。では、なぜそれをするのでしょうか?Glideではキャッシュがディスクキャッシュ、メモリキャッシュ、使用時に優先的にマッチングされるメモリキャッシュ、ディスクキャッシュに分かれています。 つまり、skipMemoryCache(true)を使用しない処理ではメモリキャッシュが優先的にマッチングされ、これがメモリキャッシュも同じである必要があります。
したがって、この画像を 2 回目にロードするときに使用されるメモリ キャッシュされた画像は、前回の最後の最後のフレームの画像です。
では、どうやって対処すればいいのでしょうか?ここで私はトリックを採用し、単一のアニメーションの終了時にアニメーションを一度開始し、すぐに停止しました。これは、メモリ キャッシュ内のアニメーションを最初のフレームにリセットするのと同じです。
mWebpDrawable.registerAnimationCallback(new Animatable2Compat.AnimationCallback() {
@Override
public void onAnimationStart(Drawable drawable) {
super.onAnimationStart(drawable);
}
@Override
public void onAnimationEnd(Drawable drawable) {
super.onAnimationEnd(drawable);
if (mWebpDrawable != null && !mWebpDrawable.isRunning()) {
//重置内存缓存动图为第一帧
mWebpDrawable.startFromFirstFrame();
mWebpDrawable.stop();
}
mWebpDrawable.unregisterAnimationCallback(this);
}
});
参考:
Glide は WebP アニメーションを読み込み、アニメーションの再生の終了を監視します_Dway のブログ-CSDN Blog_glide は WebP を読み込みます
Glide (4.6.1) に基づく Android 読み込み GIF の練習_mayundoyouknow のブログ-CSDN blog_android glide 読み込み GIF