Lottie支持复杂动画(json)使用笔记
基础信息
- Lottie Git开源地址(都给出只是方便大家找,其实我本人只用到Android)
- JSON文件需要Bodymovin导出的json文件
- Bodymovin:地址
- 官方说明:地址
- 官方Demo(Google市场):地址
Lottie相关信息
- 官方案例丢在Google应用市场了,国内不好下载。所以干脆自己打包Demo 百度云下载
- 如果是要研究代码,又不想用Git,可以自己反编译看看
- 我下载它是因为json动画文件不好弄:说说我怎么取的资源吧,尽管大多数都知道。
- 直接apk扩展名改成zip
- 使用压缩文件打开
- assets目录里面全部是需要的文件
- 我也准备了百度云链接
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"
展示动画
-
布局文件
/** * 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" />
-
代码实现
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);
-
切换动画
// 最简单的,但是需要注意,只适用于小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(); } });
-
设置监听
// 播放的文件更新的时候,也可以理解每一帧都调用,没想到应用场景,反正更一个动画就不停的调用。 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的滑动监听,他之所以用那个是为了美观。如果谁有兴趣,可以使用一下试试看。
-
布局文件使用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方法可以根据自己喜欢修改速度。
// 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();
-
源码地址:http://download.csdn.net/detail/qq_24429935/9833500
-
demo地址:http://download.csdn.net/detail/qq_24429935/9833496
-
JSON资源下载地址:http://www.lottiefiles.com/popular