Lottie 复杂动画实现

Lottie支持复杂动画(json)使用笔记

基础信息

  • Lottie Git开源地址(都给出只是方便大家找,其实我本人只用到Android) 
  • JSON文件需要Bodymovin导出的json文件 
  • 官方说明:地址
  • 官方Demo(Google市场):地址

Lottie相关信息

  • 官方案例丢在Google应用市场了,国内不好下载。所以干脆自己打包Demo 百度云下载 
    • 如果是要研究代码,又不想用Git,可以自己反编译看看
    • 我下载它是因为json动画文件不好弄:说说我怎么取的资源吧,尽管大多数都知道。 
      1. 直接apk扩展名改成zip
      2. 使用压缩文件打开
      3. assets目录里面全部是需要的文件
      4. 我也准备了百度云链接

Lottie使用笔记

  • 设置动画文件,优先匹配代码,代码没设置,显示的才会是布局文件的配置。

初始化配置

  • Lottie要求最低编辑版本是16(Android4.1)

    minSdkVersion 16
    

  • Gradle注册添加支持

    dependencies {
        compile 'com.airbnb.android:lottie:1.0.1'
    }
    

  • 添加json动画文件到资产目录(app/src/main/assets)
  • 给使用到该控件的布局文件根标签添加(如果你在布局文件设置的话,如果没有,请忽略)

    xmlns:app="http://schemas.android.com/apk/res-auto"
    
    • 1
    • 2

展示动画

  • 布局文件

    /**
     * lottie_fileName json文件名
     * lottie_loop 是否循环播放
     * lottie_autoPlay 是否自动播放
    <com.airbnb.lottie.LottieAnimationView
            android:id="@+id/animation_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:lottie_fileName="hello-world.json"
            app:lottie_loop="true"
            app:lottie_autoPlay="true" />
    
    • 1代码实现
  • LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
    // 设置json文件
    animationView.setAnimation("helloworld.json");
    // 设置是否循环播放
    animationView.loop(true);
    // 播放动画
    animationView.playAnimation();
    // 暂停动画:貌似有点不同
    animationView.cancelAnimation();
    // 停止动画:我感觉两个效果顺序是颠倒的,使用到时候请测试看看吧
    animationView.pauseAnimation();
    // 跳转进度(0.0-1.1)
    animationView.setProgress(float f);
    // 在监听中可以添加代码设置动画时长
    animator.setDuration(1000L);
    
    • 1切换动画
  • // 最简单的,但是需要注意,只适用于小Json文件,大的Json加载时间过长,中间可能空出来。
    // animationView.setAnimation("LottieLogo2.json");
    // animationView.playAnimation();
    // 官方还给出另外一种标准的切换方式
    LottieComposition.fromAssetFileName(act, "LottieLogo2.json",
            new LottieComposition.OnCompositionLoadedListener() {
                @Override
                public void onCompositionLoaded(LottieComposition composition) {
                    animationView.setComposition(composition);
                    animationView.playAnimation();
                }
            });
    
    • 1
  • 设置监听

    // 播放的文件更新的时候,也可以理解每一帧都调用,没想到应用场景,反正更一个动画就不停的调用。
    animationView.addAnimatorUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
    
        }
    });
    
    // 常用的监听,很多都很有用处。
    animationView.addAnimatorListener(new Animator.AnimatorListener() {
        // 动画开始调用
        @Override
        public void onAnimationStart(Animator animator) {
    
        }
    
        // 如果设置loop为true,永远不会调用
        @Override
        public void onAnimationEnd(Animator animator) {
    
        }
    
        // 动画取消监听,监听的是Cancel方法,可是还是进度条暂停的状态。
        @Override
        public void onAnimationCancel(Animator animator) {
    
        }
    
        // 动画重复,第一次播放不是重复,不包含在内,切换动画也一样。
        @Override
        public void onAnimationRepeat(Animator animator) {
    
        }
    });
    

  • 本地文件展示

    • 这个可以直接打开系统的文件管理器

      Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
      intent.setType("*/*");
      intent.addCategory(Intent.CATEGORY_OPENABLE);
      
      try {
          startActivityForResult(Intent.createChooser(intent, "请选择一个JSON文件"), PLAYER_BY_FILE);
      } catch (android.content.ActivityNotFoundException ex) {
          Toast.makeText(act, "请安装一个文件管理器。", Toast.LENGTH_SHORT).show();
      }
    • 在这里接收选择的文件

      @Override
      protected void onActivityResult(int requestCode, int resultCode, Intent data) {
          if(requestCode == PLAYER_BY_FILE){
              Uri uri = data.getData();
              InputStream fis;
      
              try {
                  switch (uri.getScheme()) {
                      case "file":
                          fis = new FileInputStream(uri.getPath());
                          break;
                      case "content":
                          fis = act.getContentResolver().openInputStream(uri);
                          break;
                      default:
                          Toast.makeText(act, "加载失败!", Toast.LENGTH_SHORT).show();
                          return;
                  }
              } catch (FileNotFoundException e) {
                  Toast.makeText(act, "请安装一个文件管理器。", Toast.LENGTH_SHORT).show();
                  return;
              }
          }
      }
      

    • 根据返回的 输入流 InputStream 来展示Json动画

      LottieComposition
              .fromInputStream(act, fis, new LottieComposition.OnCompositionLoadedListener() {
                  @Override
                  public void onCompositionLoaded(LottieComposition composition) {
                      animationView.setComposition(composition);
                      animationView.playAnimation();
                  }
              });
      

  • 根据网络展示

    // str 就是联网请求到的json字符串
    JSONObject jsonObject = null;
    try {
        jsonObject = new JSONObject(str);
    } catch (JSONException e) {
        e.printStackTrace();
    }
    LottieComposition
            .fromJson(getResources(), jsonObject, new LottieComposition.OnCompositionLoadedListener() {
                @Override
                public void onCompositionLoaded(LottieComposition composition) {
                    animationView.setComposition(composition);
                    animationView.playAnimation();
                }
            });
    
    • 引导界面动画
  • 这个,建议别看官方的Demo,引用第三方的工具类,反正我没用过那个类,只能一行一行的分析。最后,我发现实际上只是做了一个ViewPager的滑动监听,他之所以用那个是为了美观。如果谁有兴趣,可以使用一下试试看。

    1. 布局文件使用RelativeLayout,在 LottieAnimationView 上面添加一个ViewPager

      <?xml version="1.0" encoding="utf-8"?>
      <RelativeLayout
          xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          android:id="@+id/activity_change_pager"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          tools:context="ll.withwings.testlottieanimation.lottie.ChangePagerActivity">
      
          <com.airbnb.lottie.LottieAnimationView
              android:id="@+id/animation_view"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content" />
      
          <android.support.v4.view.ViewPager
              android:id="@+id/vp_show_animation"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
      
          </android.support.v4.view.ViewPager>
      
      </RelativeLayout>
      
      • 代码方面设置监听ViewPager滑动。lerp方法可以根据自己喜欢修改速度。
    2. // ViewPager 使用透明的Fragment填充
      // 设置 LottieAnimationView 动画的进度与ViewPager联动
      
      /**
       * 这里之所以多一个1f,是为了ViewPager最后一个item不能滑动准备的(值是根据EmptyFragment数量计算的)
       */
      private static final float[] ANIMATION_TIMES = new float[]{
              0f,
              0.3333f,
              0.6666f,
              1f,
              1f
      };
      
      /**
       * 为了ViewPager联动效果准备的空Fragment。
       */
      private List<EmptyFragment> emptyFragments;
      
      ……
      
      mVpShowAnimation.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
          @Override
          public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
              setAnimationProgress(position, positionOffset);
          }
      
          @Override
          public void onPageSelected(int position) {
      
          }
      
          @Override
          public void onPageScrollStateChanged(int state) {
      
          }
      });
      
      private void setAnimationProgress(int position, float positionOffset) {
          float startProgress = ANIMATION_TIMES[position];
          float endProgress = ANIMATION_TIMES[position + 1];
          // 更新动画进度
          animationView.setProgress(lerp(startProgress, endProgress, positionOffset));
      }
      
      // 根据ViewPager拖动偏移比例来计算位置
      private float lerp(float startValue, float endValue, float f) {
          return startValue + f * (endValue - startValue);
      }
      
      • 字母特效动画

如果谁用到的话,我建议是用我这个代码,官方代码为了兼容性删减了很多功能。当然,如果用官方的,只需要复制官方Git里面 LottieFontViewGroup 这个文件即可

*   首先复制文件 LottieFontViewGroup.java 到自己工程

*   需要记得加上监听,onDestroy 时移除监听。可以让控件根据输入内容自动滚动。

        @Override
        protected void initListener() {
            // 这个监听可以根据换行自动滑动
            fontView.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener);
        }

        @Override
        protected void onDestroy() {
            // add的监听还要删除
            fontView.getViewTreeObserver().removeOnGlobalLayoutListener(layoutListener);
            super.onDestroy();
        }

        // 监听操作,建议直接复制走。
        private final ViewTreeObserver.OnGlobalLayoutListener layoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                scrollView.fullScroll(View.FOCUS_DOWN);
            }
        };

  • 附上我改的:

    public class LottieFontViewGroup extends FrameLayout {
        private final Map<String, LottieComposition> compositionMap = new HashMap<>();
        private final List<View> views = new ArrayList<>();
    
        @Nullable
        private LottieAnimationView cursorView;
    
        public LottieFontViewGroup(Context context) {
            super(context);
            init();
        }
    
        public LottieFontViewGroup(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public LottieFontViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        private void init() {
            setFocusableInTouchMode(true);
            LottieComposition.fromAssetFileName(getContext(), "Mobilo/BlinkingCursor.json",
                    new LottieComposition.OnCompositionLoadedListener() {
                        @Override
                        public void onCompositionLoaded(LottieComposition composition) {
                            cursorView = new LottieAnimationView(getContext());
                            cursorView.setLayoutParams(new LayoutParams(
                                    ViewGroup.LayoutParams.WRAP_CONTENT,
                                    ViewGroup.LayoutParams.WRAP_CONTENT
                            ));
                            cursorView.setComposition(composition);
                            cursorView.loop(true);
                            cursorView.playAnimation();
                            addView(cursorView);
                        }
                    });
        }
    
        /**
         * 根据当前状态更新软键盘状态
         */
        public void changeInputType() {
            InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
            imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);
        }
    
        /**
         * 判断软键盘状态
         *
         * @return true代表打开,false代表隐藏
         */
        public boolean getInputType() {
            InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
            boolean isOpen = imm.isActive();//isOpen若返回true,则表示输入法打开
            return isOpen;
        }
    
        /**
         * 更改软键盘显示
         *
         * @param isOpen
         */
        public void setInputType(boolean isOpen) {
            InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
            if (isOpen) {
                imm.showSoftInput(this, InputMethodManager.SHOW_FORCED);
            } else {
                // 强制隐藏键盘
                imm.hideSoftInputFromWindow(this.getWindowToken(), 0);
            }
        }
    
        private String string = "";
    
        /**
         * 获取当前字符串。
         */
        public String getString() {
            return string;
        }
    
        /**
         * ASCII 码转字符串
         *
         * @param ascii
         * @return 文件字符串
         */
        public String asciiToString(int ascii) {
            StringBuffer sbu = new StringBuffer(string);
            sbu.append((char) ascii);
            return new String(sbu);
        }
    
        private float downX;
        private float downY;
    
        /**
         * 点击控件,切换软键盘显示
         * @param event
         * @return
         */
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if(event.getAction()==MotionEvent.ACTION_DOWN){
                downX = event.getX();
                downY = event.getY();
            }else if(event.getAction() == MotionEvent.ACTION_UP && event.getX()== downX && event.getY() == downY){
                changeInputType();
            }
            return true;
        }
    
        private void addSpace() {
            int index = indexOfChild(cursorView);
            addView(createSpaceView(), index);
        }
    
        @Override
        public void addView(View child, int index) {
            super.addView(child, index);
            if (index == -1) {
                views.add(child);
            } else {
                views.add(index, child);
            }
        }
    
        private void removeLastView() {
            if (views.size() > 1) {
                int position = views.size() - 2;
                removeView(views.get(position));
                views.remove(position);
            }
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
            if (views.isEmpty()) {
                return;
            }
            int currentX = getPaddingTop();
            int currentY = getPaddingLeft();
    
            for (int i = 0; i < views.size(); i++) {
                View view = views.get(i);
                if (!fitsOnCurrentLine(currentX, view)) {
                    if (view.getTag() != null && view.getTag().equals("Space")) {
                        continue;
                    }
                    currentX = getPaddingLeft();
                    currentY += view.getMeasuredHeight();
                }
                currentX += view.getWidth();
            }
    
            setMeasuredDimension(getMeasuredWidth(),
                    currentY + views.get(views.size() - 1).getMeasuredHeight() * 2);
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            if (views.isEmpty()) {
                return;
            }
            int currentX = getPaddingTop();
            int currentY = getPaddingLeft();
    
            for (int i = 0; i < views.size(); i++) {
                View view = views.get(i);
                if (!fitsOnCurrentLine(currentX, view)) {
                    if (view.getTag() != null && view.getTag().equals("Space")) {
                        continue;
                    }
                    currentX = getPaddingLeft();
                    currentY += view.getMeasuredHeight();
                }
                view.layout(currentX, currentY, currentX + view.getMeasuredWidth(),
                        currentY + view.getMeasuredHeight());
                currentX += view.getWidth();
            }
        }
    
        @Override
        public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
            BaseInputConnection fic = new BaseInputConnection(this, false);
            outAttrs.actionLabel = null;
            outAttrs.inputType = InputType.TYPE_NULL;
            outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT;
            return fic;
        }
    
        @Override
        public boolean onCheckIsTextEditor() {
            return true;
        }
    
        @Override
        public boolean onKeyUp(int keyCode, KeyEvent event) {
            if (keyCode == KeyEvent.KEYCODE_SPACE) {
                string += " ";
                addSpace();
                return true;
            }
    
            if (keyCode == KeyEvent.KEYCODE_DEL) {
                if(string.length()>0){
                    string = string.substring(0, string.length() - 1);
                }else{
                    string = "";
                }
                removeLastView();
                return true;
            }
    
            if (!isValidKey(event)) {
                return super.onKeyUp(keyCode, event);
            }
    
    
            String letter = "" + Character.toUpperCase((char) event.getUnicodeChar());
            // switch (letter) {
            //     case ",":
            //         letter = "Comma";
            //         break;
            //     case "'":
            //         letter = "Apostrophe";
            //         break;
            //     case ";":
            //     case ":":
            //         letter = "Colon";
            //         break;
            // }
            final String fileName = "Mobilo/" + letter + ".json";
            if (compositionMap.containsKey(fileName)) {
                addComposition(compositionMap.get(fileName));
            } else {
                LottieComposition.fromAssetFileName(getContext(), fileName,
                        new LottieComposition.OnCompositionLoadedListener() {
                            @Override
                            public void onCompositionLoaded(LottieComposition composition) {
                                compositionMap.put(fileName, composition);
                                addComposition(composition);
                            }
                        });
            }
    
            return true;
        }
    
        private boolean isValidKey(KeyEvent event) {
            if (!event.hasNoModifiers()) {
                return false;
            }
            if (event.getKeyCode() >= KeyEvent.KEYCODE_A && event.getKeyCode() <= KeyEvent.KEYCODE_Z) {
                string = asciiToString(event.getKeyCode() + 36);
                return true;
            }
    
            // switch (keyCode) {
            //     case KeyEvent.KEYCODE_COMMA:
            //     case KeyEvent.KEYCODE_APOSTROPHE:
            //     case KeyEvent.KEYCODE_SEMICOLON:
            //         return true;
            // }
            return false;
        }
    
        private void addComposition(LottieComposition composition) {
            LottieAnimationView lottieAnimationView = new LottieAnimationView(getContext());
            lottieAnimationView.setLayoutParams(new LayoutParams(
                    ViewGroup.LayoutParams.WRAP_CONTENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT
            ));
            lottieAnimationView.setComposition(composition);
            lottieAnimationView.playAnimation();
            if (cursorView == null) {
                addView(lottieAnimationView);
            } else {
                int index = indexOfChild(cursorView);
                addView(lottieAnimationView, index);
            }
        }
    
        private boolean fitsOnCurrentLine(int currentX, View view) {
            return currentX + view.getMeasuredWidth() < getWidth() - getPaddingRight();
        }
    
        private View createSpaceView() {
            View spaceView = new View(getContext());
            spaceView.setLayoutParams(new LayoutParams(
                    getResources().getDimensionPixelSize(R.dimen.font_space_width),
                    ViewGroup.LayoutParams.WRAP_CONTENT
            ));
            spaceView.setTag("Space");
            return spaceView;
        }
    }
    

Lottie使用出现问题

  • JSON文件不播放,比如:代码设置文件应用崩溃,布局文件设置了无效。

    • JSON文件有有格式要求的,这点我重复一下,Lottie支持的Json文件来自于Bodymovin,该项目是Adobe公司的动画制作软件After Effects的插件,用来将动画导出成 svg/canvas/html + js ,方便在浏览器上展示。
  • 打开界面就崩:json文件错误。路径是直接跟目录下,直接文件名;根目录的文件夹,是文件夹名/文件名,如:

    • Logo/LogoSmall.json
    • LogoSmall.json
  • loop设置为false 之后,播放结束无法再次开始,请在监听的结束监听中添加:

    animationView.pauseAnimation();

猜你喜欢

转载自blog.csdn.net/qq_32666413/article/details/80537972
今日推荐