list = mFrameList.getFrameItemList();
387 | if (CommonUtil.isEmpty(list)) {
388 | Log.e(TAG, "startWithFrameSrc, frame list parse error, list is empty.");
389 | return;
390 | }
391 |
392 | if (mUseCache) {
393 | mBlobCache = BlobCacheManager.getInstance().getBlobCache(
394 | mFrameList.getFileName(),
395 | mFrameList.getMaxEntries(),
396 | mFrameList.getMaxBytes(),
397 | mFrameList.getVersion());
398 | }
399 |
400 | // 纠正索引
401 | mIndexDecoding.set(0);
402 | mIndexDrawing.set(0);
403 |
404 | // 重置队列
405 | resetBitmapQueue();
406 | if (isDestroy()) {
407 | Log.e(TAG, "startWithFrameSrc, 222 is destroy, return.");
408 | return;
409 | }
410 |
411 | // 预解码两个bitmap到解码队列
412 | int index = mIndexDecoding.getAndIncrement();
413 | LinkedBitmap linkedBitmap = new LinkedBitmap();
414 | linkedBitmap.bitmap = decodeBitmap(mFrameList.getFrameItemByIndex(index));
415 | putDecodedBitmap(linkedBitmap);
416 |
417 | index = mIndexDecoding.getAndIncrement();
418 | LinkedBitmap linkedBitmap1 = new LinkedBitmap();
419 | linkedBitmap1.bitmap = decodeBitmap(mFrameList.getFrameItemByIndex(index));
420 | putDecodedBitmap(linkedBitmap1);
421 |
422 | if (isDestroy()) {
423 | Log.e(TAG, "startWithFrameSrc, 333 is destroy, return.");
424 | return;
425 | }
426 |
427 | Log.i(TAG, "startWithFrameSrc, start draw, resId=" + mCurrentResId);
428 | if (mDecodeHandler == null) {
429 | Log.e(TAG, "startWithFrameSrc, decode handler is null, may be is destroy.");
430 | return;
431 | }
432 | // 开始播放动画
433 | setStatus(FrameViewStatus.START);
434 | mDecodeHandler.post(new DecodeRunnable());
435 | if (mIsSurfaceAlive.get()) {
436 | startDrawThread();
437 | } else {
438 | Log.i(TAG, "startWithFrameSrc, surface is not alive, resId=" + mCurrentResId);
439 | mNeedToStartDrawThread = true;
440 | }
441 | }
442 | });
443 | }
444 |
445 | public boolean isPause() {
446 | return mStatus.get() == FrameViewStatus.PAUSE;
447 | }
448 |
449 | public boolean isDestroy() {
450 | return mStatus.get() == FrameViewStatus.DESTROY;
451 | }
452 |
453 | public boolean isStart() {
454 | return mStatus.get() == FrameViewStatus.START;
455 | }
456 |
457 | /**
458 | * 暂停动画,停留在当前一帧
459 | */
460 | public void pause() {
461 | Log.i(TAG, "stop, status=" + mStatus);
462 | if (isDestroy()) {
463 | return;
464 | }
465 | setStatus(FrameViewStatus.PAUSE);
466 | }
467 |
468 | public void resume() {
469 | Log.i(TAG, "resume, status=" + mStatus);
470 | if (isDestroy()) {
471 | return;
472 | }
473 |
474 | setStatus(FrameViewStatus.START);
475 | startDrawThread();
476 | }
477 |
478 | /**
479 | * 开启解码线程
480 | */
481 | private void startDecodeThread(Runnable runnable) {
482 | if (mDecodeHandlerThread == null) {
483 | mDecodeHandlerThread = new HandlerThread(DECODE_THREAD_NAME);
484 | }
485 | if (!mDecodeHandlerThread.isAlive()) {
486 | mDecodeHandlerThread.start();
487 | }
488 | if (mDecodeHandler == null) {
489 | mDecodeHandler = new Handler(mDecodeHandlerThread.getLooper());
490 | }
491 | mDecodeHandler.removeCallbacksAndMessages(null);
492 | mDecodeHandler.post(runnable);
493 | }
494 |
495 | /**
496 | * 开启绘制线程
497 | */
498 | private void startDrawThread() {
499 | Log.i(TAG, "startDrawThread, resId=" + mCurrentResId + ", status=" + mStatus);
500 | if (isDestroy()) {
501 | return;
502 | }
503 | if (mDrawHandlerThread == null) {
504 | mDrawHandlerThread = new HandlerThread(DRAW_THREAD_NAME);
505 | }
506 | if (!mDrawHandlerThread.isAlive()) {
507 | mDrawHandlerThread.start();
508 | }
509 |
510 | if (mDrawHandler == null) {
511 | mDrawHandler = new Handler(mDrawHandlerThread.getLooper());
512 | }
513 | mDrawHandler.removeCallbacksAndMessages(null);
514 | mDrawHandler.post(new DrawRunnable());
515 | }
516 |
517 | private void resetBitmapQueue() {
518 | try {
519 | // 尝试获取【绘制锁】【解码锁】,避免正在解码时回收了mDecodeOptions中的inBitmap,进而导致崩溃
520 | // (超时50毫秒,防止阻塞主线程)
521 | mDecodingLock.tryLock(50, TimeUnit.MILLISECONDS);
522 | mDrawingLock.tryLock(50, TimeUnit.MILLISECONDS);
523 | try {
524 | mDecodedBitmapQueue.resetData();
525 | } catch (Exception ex) {
526 | ex.printStackTrace();
527 | }
528 |
529 | try {
530 | mDrawnBitmapQueue.resetData();
531 | } catch (Exception ex) {
532 | ex.printStackTrace();
533 | }
534 | mDecodeOptions.inBitmap = null;
535 | } catch (Exception ex) {
536 | ex.printStackTrace();
537 | } finally {
538 | try {
539 | mDecodingLock.unlock();
540 | } catch (Exception ex) {
541 | ex.printStackTrace();
542 | }
543 | try {
544 | mDrawingLock.unlock();
545 | } catch (Exception ex) {
546 | ex.printStackTrace();
547 | }
548 | }
549 | }
550 |
551 | private void destroyBitmapQueue() {
552 | try {
553 | // 尝试获取【绘制锁】【解码锁】,避免正在解码时回收了mDecodeOptions中的inBitmap,进而导致崩溃
554 | // (超时50毫秒,防止阻塞主线程)
555 | mDecodingLock.tryLock(50, TimeUnit.MILLISECONDS);
556 | mDrawingLock.tryLock(50, TimeUnit.MILLISECONDS);
557 | try {
558 | mDecodedBitmapQueue.destroy();
559 | } catch (Exception ex) {
560 | ex.printStackTrace();
561 | }
562 |
563 | try {
564 | mDrawnBitmapQueue.destroy();
565 | } catch (Exception ex) {
566 | ex.printStackTrace();
567 | }
568 | mDecodeOptions.inBitmap = null;
569 | } catch (Exception ex) {
570 | ex.printStackTrace();
571 | } finally {
572 | try {
573 | mDecodingLock.unlock();
574 | } catch (Exception ex) {
575 | ex.printStackTrace();
576 | }
577 | try {
578 | mDrawingLock.unlock();
579 | } catch (Exception ex) {
580 | ex.printStackTrace();
581 | }
582 | }
583 | }
584 |
585 | /**
586 | * 彻底销毁并释放资源
587 | */
588 | public void destroy() {
589 | if (isDestroy()) {
590 | return;
591 | }
592 | Log.i(TAG, "destroy FrameTextureView, start.");
593 | setStatus(FrameViewStatus.DESTROY);
594 | destroyHandler();
595 | destroyBitmapQueue();
596 | destroyThread();
597 | destroyBytesBuffer();
598 | destroyLookupRequest();
599 | clearKeyMap();
600 | clearPixelsBuffer();
601 | Log.i(TAG, "destroy FrameTextureView, end.");
602 | }
603 |
604 | private void destroyHandler() {
605 | if (mDecodeHandler != null) {
606 | mDecodeHandler.removeCallbacksAndMessages(null);
607 | mDecodeHandler = null;
608 | }
609 |
610 | if (mDrawHandler != null) {
611 | mDrawHandler.removeCallbacksAndMessages(null);
612 | mDrawHandler = null;
613 | }
614 | }
615 |
616 | private void destroyThread() {
617 | try {
618 | if (mDecodeHandlerThread != null) {
619 | mDecodeHandlerThread.quit();
620 | mDecodeHandlerThread = null;
621 | }
622 | } catch (Exception ex) {
623 | ex.printStackTrace();
624 | }
625 |
626 | try {
627 | if (mDrawHandlerThread != null) {
628 | mDrawHandlerThread.quit();
629 | mDrawHandlerThread = null;
630 | }
631 | } catch (Exception ex) {
632 | ex.printStackTrace();
633 | }
634 | }
635 |
636 | private void destroyBytesBuffer() {
637 | if (mDataBuffer != null) {
638 | mDataBuffer.data = null;
639 | mDataBuffer = null;
640 | }
641 | if (mWidthBuffer != null) {
642 | mWidthBuffer.data = null;
643 | mWidthBuffer = null;
644 | }
645 | if (mHeightBuffer != null) {
646 | mHeightBuffer.data = null;
647 | mHeightBuffer = null;
648 | }
649 | }
650 |
651 | private void clearKeyMap() {
652 | if (mKeyMap != null) {
653 | mKeyMap.clear();
654 | }
655 | }
656 |
657 | private void clearPixelsBuffer() {
658 | if (mPixelsBuffer != null) {
659 | mPixelsBuffer.clear();
660 | mPixelsBuffer = null;
661 | }
662 | }
663 |
664 | private void destroyLookupRequest() {
665 | mLookupRequest = null;
666 | }
667 |
668 | public void setScaleType(@FrameScaleType int scaleType) {
669 | mScaleType = scaleType;
670 | }
671 |
672 | /**
673 | * 绘制一帧
674 | */
675 | private void drawOneFrame() {
676 | if (!mIsSurfaceAlive.get()) {
677 | Log.e(TAG, "drawOneFrame, suface is not alive.");
678 | return;
679 | }
680 | if (!isStart()) {
681 | Log.e(TAG, "drawOneFrame, status is not start, status=" + mStatus);
682 | return;
683 | }
684 | LinkedBitmap linkedBitmap = getDecodedBitmap();
685 | if (linkedBitmap != null && linkedBitmap.bitmap != null) {
686 | if (!isStart()) {
687 | // 如果是暂停状态,则取出来后,要添加到已绘制的队列里,避免恢复动画时【绘制队列】和【解码队列】都为空的情况
688 | if (isPause()) {
689 | putDrawnBitmap(linkedBitmap);
690 | }
691 | return;
692 | }
693 | if (!mIsSurfaceAlive.get()) {
694 | return;
695 | }
696 | Bitmap bitmap = linkedBitmap.bitmap;
697 | MatrixUtil.configureDrawMatrix(bitmap, getWidth(), getHeight(), mDrawMatrix, mScaleType);
698 | Canvas canvas = null;
699 | try {
700 | canvas = lockCanvas();
701 | if (canvas != null) {
702 | try {
703 | // 获取【绘制锁】,防止绘制中,surface销毁了导致崩溃
704 | mDrawingLock.lockInterruptibly();
705 | if (mIsSurfaceAlive.get() && !bitmap.isRecycled()) {
706 | clearCanvas(canvas);
707 | canvas.drawBitmap(bitmap, mDrawMatrix, null);
708 | }
709 | } catch (Exception ex) {
710 | ex.printStackTrace();
711 | } finally {
712 | try {
713 | mDrawingLock.unlock();
714 | } catch (Exception ex) {
715 | ex.printStackTrace();
716 | }
717 | }
718 | }
719 | } catch (Exception ex) {
720 | ex.printStackTrace();
721 | } finally {
722 | try {
723 | if (canvas != null) {
724 | unlockCanvasAndPost(canvas);
725 | }
726 | } catch (Exception ex) {
727 | ex.printStackTrace();
728 | }
729 | }
730 | putDrawnBitmap(linkedBitmap);
731 | }
732 | mIndexDrawing.incrementAndGet();
733 | }
734 |
735 | /**
736 | * 存储已绘制的bitmap到【已绘制的Bitmap队列】
737 | *
738 | * @param bitmap 已绘制的bitmap(不阻塞)
739 | */
740 | private void putDrawnBitmap(LinkedBitmap bitmap) {
741 | if (isDestroy()) {
742 | return;
743 | }
744 | try {
745 | mDrawnBitmapQueue.offer(bitmap);
746 | } catch (Exception ex) {
747 | ex.printStackTrace();
748 | Log.e(TAG, "putDrawnBitmap, ex=" + ex);
749 | }
750 | }
751 |
752 | /**
753 | * 存储已经解码的bitmap到【已解码的bitmap队列】
754 | *
755 | * @param bitmap 已解码bitmap(可能阻塞)
756 | */
757 | private void putDecodedBitmap(LinkedBitmap bitmap) {
758 | if (isDestroy()) {
759 | return;
760 | }
761 | try {
762 | mDecodedBitmapQueue.put(bitmap);
763 | } catch (Exception ex) {
764 | ex.printStackTrace();
765 | Log.e(TAG, "putDecodedBitmap, ex=" + ex);
766 | }
767 | }
768 |
769 | /**
770 | * 清除画布上的绘图,准备下一帧
771 | */
772 | private void clearCanvas(Canvas canvas) {
773 | canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
774 | }
775 |
776 | /**
777 | * 从【解码bitmap队列】里取bitmap
778 | * (可能会阻塞)
779 | */
780 | private LinkedBitmap getDecodedBitmap() {
781 | if (isDestroy()) {
782 | return null;
783 | }
784 | LinkedBitmap bitmap = null;
785 | try {
786 | bitmap = mDecodedBitmapQueue.take();
787 | } catch (Exception e) {
788 | e.printStackTrace();
789 | }
790 | return bitmap;
791 | }
792 |
793 | /**
794 | * 是否帧动画已经绘制到最后一帧了
795 | */
796 | private boolean isLastFrame() {
797 | if (mFrameList == null) {
798 | return true;
799 | }
800 | return mIndexDrawing.get() >= mFrameList.getFrameItemSize() - 1;
801 | }
802 |
803 | /**
804 | * 从【已绘制的bitmap队列】里取废弃的已绘制的bitmap
805 | */
806 | private LinkedBitmap getDrawnBitmap() {
807 | if (isDestroy()) {
808 | return null;
809 | }
810 | LinkedBitmap bitmap = null;
811 | try {
812 | bitmap = mDrawnBitmapQueue.take();
813 | } catch (Exception e) {
814 | e.printStackTrace();
815 | }
816 | return bitmap;
817 | }
818 |
819 | /**
820 | * 解码图片
821 | */
822 | private Bitmap decodeBitmap(FrameItem frameItem) {
823 | final String name = frameItem.getDrawableName();
824 | Bitmap bitmap = null;
825 | if (mUseCache) {
826 | if (mDataBuffer == null) {
827 | mDataBuffer = new BytesBuffer();
828 | }
829 | if (mWidthBuffer == null) {
830 | mWidthBuffer = new BytesBuffer(4);
831 | }
832 | if (mHeightBuffer == null) {
833 | mHeightBuffer = new BytesBuffer(4);
834 | }
835 | if (mKeyMap == null) {
836 | mKeyMap = new ConcurrentHashMap<>();
837 | }
838 | byte[] key = mKeyMap.get(name);
839 | if (key == null) {
840 | key = BlobCacheUtil.getBytes(name);
841 | mKeyMap.put(name, key);
842 | }
843 |
844 | if (mLookupRequest == null) {
845 | mLookupRequest = new BlobCache.LookupRequest();
846 | }
847 | try {
848 | // 获取【解码锁】,避免在解码图片时已经处于destroy状态,导致mDecodeOptions中inBitmap被回收了而崩溃
849 | mDecodingLock.lockInterruptibly();
850 | if (!isDestroy()) {
851 | BytesBuffer bytesBuffer = BlobCacheUtil.getCacheDataByName(mBlobCache, name, mDataBuffer, key, mLookupRequest);
852 | if (bytesBuffer != null && bytesBuffer.data != null) {
853 | mDataBuffer = bytesBuffer;
854 | if (mPixelsBuffer == null || mPixelsBuffer.capacity() != bytesBuffer.data.length) {
855 | mPixelsBuffer = ByteBuffer.allocate(bytesBuffer.data.length);
856 | }
857 | bitmap = BlobCacheUtil.getCacheBitmapByData(bytesBuffer, mPixelsBuffer, mDecodeOptions.inBitmap, mWidthBuffer, mHeightBuffer);
858 | }
859 | }
860 | } catch (Exception ex) {
861 | ex.printStackTrace();
862 | Log.e(TAG, "decodeBitmap, from cache, ex=" + ex + ", name=" + name);
863 | } finally {
864 | try {
865 | mDecodingLock.unlock();
866 | } catch (Exception ex) {
867 | ex.printStackTrace();
868 | }
869 | }
870 |
871 | if (bitmap != null) {
872 | return bitmap;
873 | }
874 | }
875 |
876 | try {
877 | // 获取【解码锁】,避免在解码图片时已经处于destroy状态,导致mDecodeOptions中inBitmap被回收了而崩溃
878 | mDecodingLock.lockInterruptibly();
879 | if (!isDestroy()) {
880 | bitmap = ResourceUtil.getBitmap(name, mDecodeOptions);
881 | if (mUseCache) {
882 | BlobCacheUtil.saveImageByBlobCache(bitmap, name, mBlobCache);
883 | }
884 | }
885 | } catch (Exception ex) {
886 | ex.printStackTrace();
887 | Log.e(TAG, "decodeBitmap, ex: " + ex + ", name=" + name);
888 | } finally {
889 | try {
890 | mDecodingLock.unlock();
891 | } catch (Exception ex) {
892 | ex.printStackTrace();
893 | }
894 | }
895 | return bitmap;
896 | }
897 |
898 | private class DrawRunnable implements Runnable {
899 |
900 | @Override
901 | public void run() {
902 | if (!mIsSurfaceAlive.get()) {
903 | Log.e(TAG, "DrawRunnable, surface is not alive.");
904 | return;
905 | }
906 | if (!isStart()) {
907 | Log.e(TAG, "DrawRunnable, status is not start, status=" + mStatus);
908 | return;
909 | }
910 | if (isLastFrame()) {
911 | boolean isOneShot = mFrameList.isOneShot();
912 | if (isOneShot) {
913 | setStatus(FrameViewStatus.END);
914 | } else {
915 | drawOneFrame();
916 | mIndexDrawing.set(0);
917 | }
918 | } else {
919 | drawOneFrame();
920 | }
921 |
922 | if (mDrawHandler != null) {
923 | if (mFrameList == null) {
924 | return;
925 | }
926 | if (!isStart()) {
927 | return;
928 | }
929 | if (!mIsSurfaceAlive.get()) {
930 | return;
931 | }
932 | int duration = DEFAULT_DURATION;
933 | FrameItem frameItem = mFrameList.getFrameItemByIndex(mIndexDrawing.get());
934 | if (frameItem != null) {
935 | duration = frameItem.getDuration();
936 | }
937 | mDrawHandler.postDelayed(this, duration);
938 | }
939 | }
940 | }
941 |
942 | private class DecodeRunnable implements Runnable {
943 |
944 | @Override
945 | public void run() {
946 | if (isDestroy()) {
947 | Log.e(TAG, "DecodeRunnable, is destroy.");
948 | return;
949 | }
950 | if (mFrameList == null) {
951 | Log.e(TAG, "DecodeRunnable, frame list is null.");
952 | return;
953 | }
954 |
955 | int index = mIndexDecoding.getAndIncrement();
956 | if (index >= mFrameList.getFrameItemSize()) {
957 | index = 0;
958 | mIndexDecoding.set(1);
959 | }
960 |
961 | FrameItem frameItem = mFrameList.getFrameItemByIndex(index);
962 | if (frameItem == null) {
963 | Log.e(TAG, "DecodeRunnable, index=" + index + ", frameItem is null.");
964 | return;
965 | }
966 |
967 | LinkedBitmap linkedBitmap = getDrawnBitmap();
968 | if (isDestroy()) {
969 | Log.e(TAG, "DecodeRunnable, is destroy.");
970 | return;
971 | }
972 | if (linkedBitmap == null) {
973 | linkedBitmap = new LinkedBitmap();
974 | }
975 | mDecodeOptions.inBitmap = linkedBitmap.bitmap;
976 | Bitmap bitmap = decodeBitmap(frameItem);
977 | if (isDestroy()) {
978 | Log.e(TAG, "DecodeRunnable, is destroy.");
979 | return;
980 | }
981 | if (bitmap == null) {
982 | Log.e(TAG, "DecodeRunnable, bitmap is null.");
983 | } else {
984 | linkedBitmap.bitmap = bitmap;
985 | putDecodedBitmap(linkedBitmap);
986 | }
987 |
988 | if (mDecodeHandler != null) {
989 | if (isDestroy()) {
990 | return;
991 | }
992 | mDecodeHandler.post(this);
993 | }
994 | }
995 | }
996 | }
997 |
--------------------------------------------------------------------------------
/frameanimation/src/main/java/com/hewuzhao/frameanimation/frameview/FrameViewStatus.java:
--------------------------------------------------------------------------------
1 | package com.hewuzhao.frameanimation.frameview;
2 |
3 | import androidx.annotation.IntDef;
4 |
5 | import java.lang.annotation.Retention;
6 | import java.lang.annotation.RetentionPolicy;
7 |
8 | import static com.hewuzhao.frameanimation.frameview.FrameViewStatus.DESTROY;
9 | import static com.hewuzhao.frameanimation.frameview.FrameViewStatus.END;
10 | import static com.hewuzhao.frameanimation.frameview.FrameViewStatus.IDLE;
11 | import static com.hewuzhao.frameanimation.frameview.FrameViewStatus.PAUSE;
12 | import static com.hewuzhao.frameanimation.frameview.FrameViewStatus.START;
13 |
14 | /**
15 | * @author hewuzhao
16 | * @date 2020-02-09
17 | *
18 | * 逐帧动画的播放状态
19 | */
20 | @IntDef({IDLE, START, PAUSE, END, DESTROY})
21 | @Retention(RetentionPolicy.SOURCE)
22 | public @interface FrameViewStatus {
23 |
24 | /**
25 | * 初始化
26 | */
27 | int IDLE = 1;
28 |
29 | /**
30 | * 开始
31 | */
32 | int START = 2;
33 |
34 | /**
35 | * 暂停
36 | */
37 | int PAUSE = 3;
38 |
39 | /**
40 | * 结束
41 | */
42 | int END = 4;
43 |
44 | /**
45 | * 彻底销毁
46 | */
47 | int DESTROY = 5;
48 | }
49 |
--------------------------------------------------------------------------------
/frameanimation/src/main/java/com/hewuzhao/frameanimation/frameview/LinkedBitmap.java:
--------------------------------------------------------------------------------
1 | package com.hewuzhao.frameanimation.frameview;
2 |
3 | import android.graphics.Bitmap;
4 |
5 | /**
6 | * @author hewuzhao
7 | * @date 2020-02-01
8 | */
9 | public class LinkedBitmap {
10 | public Bitmap bitmap;
11 | public LinkedBitmap next;
12 | }
13 |
--------------------------------------------------------------------------------
/frameanimation/src/main/java/com/hewuzhao/frameanimation/utils/CommonUtil.java:
--------------------------------------------------------------------------------
1 | package com.hewuzhao.frameanimation.utils;
2 |
3 | import java.io.Closeable;
4 | import java.text.DecimalFormat;
5 | import java.util.Collection;
6 |
7 | /**
8 | * @author hewuzhao
9 | * @date 2020-02-07
10 | */
11 | public class CommonUtil {
12 |
13 | public static void closeSafely(Closeable obj) {
14 | if (obj != null) {
15 | try {
16 | obj.close();
17 | } catch (Exception ex) {
18 | ex.printStackTrace();
19 | }
20 | }
21 | }
22 |
23 | public static boolean isEmpty(Collection collection) {
24 | return collection == null || collection.isEmpty();
25 | }
26 |
27 | public static int size(Collection collection) {
28 | if (collection == null) {
29 | return 0;
30 | }
31 | return collection.size();
32 | }
33 |
34 | public static String convertUnit(long size) {
35 | if (size <= 0) {
36 | return "0";
37 | }
38 | final String[] units = new String[]{"B", "KB", "MB", "GB", "TB"};
39 | int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
40 | return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/frameanimation/src/main/java/com/hewuzhao/frameanimation/utils/FrameParseUtil.java:
--------------------------------------------------------------------------------
1 | package com.hewuzhao.frameanimation.utils;
2 |
3 | import android.content.Context;
4 | import android.content.res.AssetManager;
5 | import android.content.res.Resources;
6 | import android.content.res.XmlResourceParser;
7 | import android.text.TextUtils;
8 | import android.util.Log;
9 | import android.util.TypedValue;
10 |
11 | import androidx.annotation.DrawableRes;
12 |
13 | import com.hewuzhao.frameanimation.FrameApplication;
14 | import com.hewuzhao.frameanimation.frameview.FrameItem;
15 | import com.hewuzhao.frameanimation.frameview.FrameList;
16 |
17 | import org.xmlpull.v1.XmlPullParser;
18 | import org.xmlpull.v1.XmlPullParserException;
19 |
20 | import java.util.ArrayList;
21 | import java.util.List;
22 |
23 | /**
24 | * @author hewuzhao
25 | * @date 2020/4/19
26 | */
27 | public class FrameParseUtil {
28 | private static final String TAG = "FrameParseUtil";
29 |
30 | /**
31 | * 帧动画文件解析
32 | *
33 | * @param resId 帧动画文件id
34 | */
35 | public static FrameList parse(@DrawableRes int resId) {
36 | FrameList frameList = new FrameList();
37 | List itemList = new ArrayList<>();
38 | Context context = FrameApplication.sApplication;
39 | Resources res = context.getResources();
40 | TypedValue value = new TypedValue();
41 |
42 | res.getValueForDensity(resId, 0, value, true);
43 | String file = value.string.toString();
44 | if (TextUtils.isEmpty(file)) {
45 | return frameList;
46 | }
47 |
48 | try {
49 | AssetManager assetManager = context.getAssets();
50 | XmlResourceParser parser = assetManager.openXmlResourceParser(0, file);
51 | int event = parser.getEventType();
52 | while (event != XmlPullParser.END_DOCUMENT) {
53 | switch (event) {
54 | case XmlPullParser.START_DOCUMENT: {
55 | Log.i(TAG, "xml 解析开始.");
56 | break;
57 | }
58 | case XmlPullParser.START_TAG: {
59 | String name = parser.getName();
60 | if ("animation-list".equals(name)) {
61 | int count = parser.getAttributeCount();
62 | for (int i = 0; i < count; i++) {
63 | String attributeName = parser.getAttributeName(i);
64 | if (attributeName != null) {
65 | switch (attributeName) {
66 | case "oneshot": {
67 | boolean oneShot = parser.getAttributeBooleanValue(i, false);
68 | frameList.setOneShot(oneShot);
69 | break;
70 | }
71 | case "maxBytes": {
72 | // default 500M
73 | int maxBytes = parser.getAttributeIntValue(i, 524288000);
74 | frameList.setMaxBytes(maxBytes);
75 | break;
76 | }
77 | case "maxEntries": {
78 | int maxEntries = parser.getAttributeIntValue(i, 100);
79 | frameList.setMaxEntries(maxEntries);
80 | break;
81 | }
82 | case "version": {
83 | int version = parser.getAttributeIntValue(i, 1);
84 | frameList.setVersion(version);
85 | break;
86 | }
87 | }
88 | }
89 | }
90 | } else if ("item".equals(name)) {
91 | FrameItem frameItem = new FrameItem();
92 | int count = parser.getAttributeCount();
93 | for (int i = 0; i < count; i++) {
94 | String attributeName = parser.getAttributeName(i);
95 |
96 | if (attributeName != null) {
97 | switch (attributeName) {
98 | case "drawable": {
99 | // @2131099732
100 | String drawable = parser.getAttributeValue(i);
101 | if (TextUtils.isEmpty(drawable)) {
102 | throw new XmlPullParserException("the drawable is empty, need a drawable.");
103 | }
104 | drawable = drawable.replace("@", "");
105 | String path = res.getResourceName(Integer.parseInt(drawable));
106 | String[] dr = path.split("/");
107 | drawable = dr[dr.length - 1];
108 | frameItem.setDrawableName(drawable);
109 | break;
110 | }
111 | case "duration": {
112 | int duration = parser.getAttributeIntValue(i, 60);
113 | frameItem.setDuration(duration);
114 | break;
115 | }
116 | }
117 | }
118 | }
119 | itemList.add(frameItem);
120 | }
121 | break;
122 | }
123 | default:
124 | break;
125 | }
126 |
127 | event = parser.next();
128 | }
129 | frameList.setFrameItemList(itemList);
130 | } catch (Exception ex) {
131 | ex.printStackTrace();
132 | Log.e(TAG, "FrameParseUtil, ex=" + ex);
133 | }
134 | String[] str = file.split("/");
135 | file = str[str.length - 1];
136 | file = file.split("\\.")[0];
137 | frameList.setFileName(file);
138 | return frameList;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/frameanimation/src/main/java/com/hewuzhao/frameanimation/utils/MatrixUtil.java:
--------------------------------------------------------------------------------
1 | package com.hewuzhao.frameanimation.utils;
2 |
3 | import android.graphics.Bitmap;
4 | import android.graphics.Matrix;
5 | import android.graphics.RectF;
6 |
7 | import com.hewuzhao.frameanimation.frameview.FrameScaleType;
8 |
9 | import java.util.Arrays;
10 | import java.util.List;
11 |
12 | /**
13 | * @author : hewuzhao
14 | * @date : 2020/5/5
15 | * @email : hewuzhao@baidu.com
16 | */
17 | public class MatrixUtil {
18 | private static final List MATRIX_SCALE_ARRAY = Arrays.asList(
19 | Matrix.ScaleToFit.FILL,
20 | Matrix.ScaleToFit.START,
21 | Matrix.ScaleToFit.CENTER,
22 | Matrix.ScaleToFit.END
23 | );
24 |
25 | /**
26 | * 根据ScaleType配置绘制bitmap的Matrix
27 | *
28 | * 参考ImageView的配置规则
29 | *
30 | * @param width view width
31 | * @param height view height
32 | */
33 | public static void configureDrawMatrix(Bitmap bitmap, int width, int height, Matrix matrix, @FrameScaleType int scaleType) {
34 | int srcWidth = bitmap.getWidth();
35 | int dstWidth = width;
36 | int srcHeight = bitmap.getHeight();
37 | int dstHeight = height;
38 | switch (scaleType) {
39 | case FrameScaleType.MATRIX: {
40 | return;
41 | }
42 | case FrameScaleType.CENTER: {
43 | matrix.setTranslate(Math.round((dstWidth - srcWidth) * 0.5f), Math.round((dstHeight - srcHeight) * 0.5f));
44 | break;
45 | }
46 | case FrameScaleType.CENTER_CROP: {
47 | float scale;
48 | float dx = 0f;
49 | float dy = 0f;
50 | //按照高缩放
51 | if (dstHeight * srcWidth > dstWidth * srcHeight) {
52 | scale = (float) dstHeight / (float) srcHeight;
53 | dx = (dstWidth - srcWidth * scale) * 0.5f;
54 | } else {
55 | scale = (float) dstWidth / (float) srcWidth;
56 | dy = (dstHeight - srcHeight * scale) * 0.5f;
57 | }
58 | matrix.setScale(scale, scale);
59 | matrix.postTranslate(dx, dy);
60 | break;
61 | }
62 | case FrameScaleType.CENTER_INSIDE: {
63 | float scale;
64 | //小于dst时不缩放
65 | if (srcWidth <= dstWidth && srcHeight <= dstHeight) {
66 | scale = 1.0f;
67 | } else {
68 | scale = Math.min((float) dstWidth / (float) srcWidth, (float) dstHeight / (float) srcHeight);
69 | }
70 | float dx = Math.round((dstWidth - srcWidth * scale) * 0.5f);
71 | float dy = Math.round((dstHeight - srcHeight * scale) * 0.5f);
72 | matrix.setScale(scale, scale);
73 | matrix.postTranslate(dx, dy);
74 | break;
75 | }
76 | default: {
77 | RectF srcRect = new RectF(0f, 0f, bitmap.getWidth(), bitmap.getHeight());
78 | RectF dstRect = new RectF(0f, 0f, width, height);
79 | matrix.setRectToRect(srcRect, dstRect, MATRIX_SCALE_ARRAY.get(scaleType - 1));
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/frameanimation/src/main/java/com/hewuzhao/frameanimation/utils/ResourceUtil.java:
--------------------------------------------------------------------------------
1 | package com.hewuzhao.frameanimation.utils;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.graphics.BitmapFactory;
6 |
7 | import com.hewuzhao.frameanimation.FrameApplication;
8 |
9 | /**
10 | * @author hewuzhao
11 | * @date 2020-02-02
12 | */
13 | public class ResourceUtil {
14 |
15 | /**
16 | * int --> byte[] 整形转byte[]
17 | *
18 | * @param res
19 | * @return
20 | */
21 | public static byte[] int2byte(int res) {
22 | byte[] targets = new byte[4];
23 |
24 | // 最低位
25 | targets[0] = (byte) (res & 0xff);
26 | // 次低位
27 | targets[1] = (byte) ((res >> 8) & 0xff);
28 | // 次高位
29 | targets[2] = (byte) ((res >> 16) & 0xff);
30 | // 最高位,无符号右移
31 | targets[3] = (byte) (res >>> 24);
32 | return targets;
33 | }
34 |
35 | /**
36 | * byte[] -->int byte[]转整形
37 | *
38 | * @param res
39 | * @return
40 | */
41 | public static int byte2int(byte[] res) {
42 | // 一个byte数据左移24位变成0x??000000,再右移8位变成0x00??0000
43 | // | 表示安位或
44 |
45 | return (res[0] & 0xff) | ((res[1] << 8) & 0xff00)
46 | | ((res[2] << 24) >>> 8) | (res[3] << 24);
47 | }
48 |
49 |
50 | public static Bitmap getBitmap(String drawableName, BitmapFactory.Options options) {
51 | return BitmapFactory.decodeResource(FrameApplication.sApplication.getResources(),
52 | getDrawableId(FrameApplication.sApplication, drawableName), options);
53 | }
54 |
55 | private static int getDrawableId(Context context, String resName) {
56 | return context.getResources().getIdentifier(resName, "drawable", context.getPackageName());
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/frameanimation/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/frameanimation/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/frameanimation/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frameanimation/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frameanimation/src/test/java/com/hewuzhao/frameanimation/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.hewuzhao.frameanimation;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 |
21 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hewuzhao/FrameAnimation/f739fd6ff261693a61c0057a19fd295ee0c7c024/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Feb 01 17:31:03 CST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/image/gif-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hewuzhao/FrameAnimation/f739fd6ff261693a61c0057a19fd295ee0c7c024/image/gif-demo.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | include ':frameanimation'
3 |
--------------------------------------------------------------------------------