本文首发于微信公众号——世界上有意思的事,搬运转载请注明出处,否则将追究版权责任。交流qq群:859640274

大家好久不见,又有一个多月没有发文章了,所以今天发一篇来刷刷存在感。最近 Flutter 非常火,我这一个月也不断的找资料来学习 Flutter。经过一段时间的摸索,我发现现在很多资料都非常”水“。各种 Dart 入门、Flutter 入门、Flutter 资料收集,完全没有任何有趣的东西。我不想去写重复而无聊的文章,所以本篇文章会抛转引玉的探讨一些在学习和开发 Flutter 的过程中遇见的问题和解决方案。

阅读须知:

  • 1.WE——>WsElement、ECWS——>ElementContainerWidgetState、EAL——>ElementActionListener

本文分为以下章节,读者可按需阅读:

  • 1.Flutter之问——以 QA 的形式来阐述我对 Flutter 的看法和学习经验。
  • 2.移植一个Flutter控件——将仿写抖音的贴纸控件移植到 Flutter 中。
  • 3.Flutter探究——聊一聊 Flutter 的原理。
  • 4.尾巴

一、Flutter之问

天下事有难易乎?为之,则难者亦易已!

Q:Flutter 怎么学?

A:这是老生常谈的问题了。随便打开一个 Flutter 系列文章,都会为你铺平接下来几周的路。但是几周之后呢?似乎很少文章会接着写下去,**毕竟大脑最喜欢简单的东西(我也不例外),一件事情的难度与受欢迎程度成反比。**所以 Flutter 怎么学?所谓:取乎其上,得乎其中。我只有一句话:以让 Flutter 成为你最拿手技能为目标去学。

Q:能给一些 Flutter 的学习资料吗?

A:我列举一下我学习 Flutter 过程中用到的资料:

  • 1.Dart官网,啃完官方文档,Dart 你就入门了。

  • 2.Flutter实战,这本开源书的例子很多,全部敲一遍 flutter 你就入门了。特别是最后的 Flutter 原理分析可以仔细看看。

  • 3.Flutter github 仓库,现在网络上 Flutter 原理分析的文章真的非常少,所以真想要成为 Flutter 专家,你必须作为开拓者去阅读 Flutter 在各种层级下的源码。

Q:Flutter 会干掉 Native?

A:Flutter 是 Native 的子集。在手机被”革命“之前,但凡业务比较复杂的公司,只会要求 Native 工程师掌握 Flutter。而不会出现抛弃 Native 只做 Flutter 的工程师,因为 Flutter 说一千道一万只是一个 ui 框架。毕竟它自身的复杂度很难支撑起比它还复杂的业务。以上只是个人观点,有分歧可以在评论区探讨

Q:Flutter 哪些地方做的比 Native 好?

A:下面是我总结出来的 Flutter 比 Native 好的地方:

  • 1.ios、android 一把抓,还可能带上 web、mac、pc。
  • 2.Dart 语言非常现代,比 java、oc 好上太多。
  • 3.新兴框架没有历史包袱。
  • 4.热更技术非常诱人。
  • 5.入门很简单。

二、移植一个FluTter控件

经常读我的文章的读者应该看过我上一篇文章:抖音、ins、微信功能大比拼——Story的贴纸文字,这篇文章中详细比较了各家 Story 的贴纸文字的功能,然后在 Android 端实现了一个贴纸框架。而这一章我就打算将这个贴纸框架移植到 Flutter,相信最后的还原度会超过你的想象。接下来建议配合源码阅读文章。注意这一章的大部分内容和上一篇文章中讲解 Android 端实现控件的章节是差不多的。

github 地址

使用方式:sticker_framework: ^0.0.1

1.架构方式

我们第一节先讲讲文字贴纸控件的架构实现,我会基于下面的 图1 和 github 上的代码进行讲解。建议大家把代码 clone 下来,当然别忘了给个 star。

我们先来根据图1来讲讲整个控件的架构

  • 1.我们先从整体来看:

    • 1.我们需要选择一个 StatefulWidget 作为基本的容器。所以图中的 ElementContainerWidgetState 就是一个构造这样的容器的 State,简单概括一下它有这些功能:

      • 1.处理各种手势事件,这里的手势包括单指和双指。
      • 2.添加和删除一些子 Widget。这里的子 Widget 用于绘制各种元素。
      • 3.提供一些 api 让外部能操控元素。
      • 4.提供一个 listener,让外部能够监听内部的各种流程。
    • 2.有了绘制容器,我们需要向绘制容器里面添加 Widget。而 Widget 在用户操作的过程中需要有各种数据,所以这里我用了 WE 来封装需要展示的 Widget,其内部有下面这些东西:
      • 1.各种用户操作过程中需要的数据例如:scale、rotate、x、y等等。
      • 2.有一些方法能够通过数据来更新 Widget。
      • 3.提供一些 api 让 ECWS 能更新 WE 里面的数据 。
    • 3.由 ECWS 和 WE 就能继续继承出各种各样的扩展控件。
  • 2.整体讲完了,我们就可以来仔细的讲讲图中的流程
    • 1.先讲横着的箭头:外部/内部调用,外部需要调用 ECWS 来进行对 WE 的增删改查等操作时会进入这个路径,这个路径里可以有下面这些操作:

      • 1.addElement:向 ECWS 中添加一个元素。
      • 2.deleteElement:从 ECWS 中删除一个元素。
      • 3.update:让 WE 根据当前数构建出一个 Widget。
      • 4.findElementByPosition:找到传入的坐标下的最顶层的 WE。
      • 5.selectElement:选中一个 WE 且将其调到最顶层。
      • 6.unSelectElement:取消选中一个 WE。
    • 2.再来讲竖着的箭头:手势事件流,这里中间会经历一些内部逻辑我们后面来讲,最终事件流会触发下面的一系列行为:
      • 1.单指移动的整个流程:当我们选中了一个 WE 的时候就可以对它进行移动。这里移动可以分为开始、进行中、结束。每个事件都会调用 WE 的对应方法以更新其内部数据。
      • 2.双指旋转缩放的整个流程:当我们选中了一个 WE 的时候可以用双指对它进行缩放和旋转。这里可以分为开始、进行中、结束。这里也会调用 WE 的对应方法更新数据。
      • 3.选中元素再次点击:当我们选中了一个 WE 的时候,可以对其再次点击。
      • 4.点击空白区域:当我们没有点击任意 WE 的时候可以进行一些操作,例如清除当前 WE 的选中状态。这个行为是可以继承的,可以交由子类来覆写。
      • 5.子类事件:我们看上面其实感觉触发的事件比较少。所以在 down、move、up 的时候会优先调用三个方法 downSelectTapOtherAction、scrollSelectTapOtherAction、upSelectTapOtherAction。这三个方法可以被子类覆写,如果返回 true 的话表示事件已经消耗了,ECWS 就不会再触发其他事件。这样一来子类也可以对手势进行扩展,例如按住某个地方单指缩放等等。
      • 7.我图中 ECWS 也实现了一个子类 DECWS,这个类简单的加两个手势:
        • 1.单指移动缩放:类似抖音的随拍,按住元素的右下角的时候可以用拖动来对元素进行缩放和旋转。
        • 2.删除:类似抖音的随拍,点击元素左上角的时候可以直接删除元素。
    • 3.图1中有一个特性其实没有画出来因为画不下了,那就是:ECWS 在1和2中的几乎所有行为都能被外部监听,ElementActionListener 就是负责监听的接口。ECWS 中存有一个 EAL 的 set 集合所以监听器可以添加多个。

2.技术点实现

我在开发整个控件的时候遇到过比较多的技术实现上的难点,所以这一节就选一些来讲讲,让读者在看源码的时候不会特别困惑。

(1).定义数据结构与绘制坐标系

-----代码块1----- ws_element.dartint mZIndex = -1; // 图像的层级double mMoveX = 0.0; // 初始化后相对 ElementContainerWidget 中心的移动距离double mMoveY = 0.0; // 初始化后相对 ElementContainerWidget 中心的移动距离double mOriginWidth; // 初始化时内容的宽度double mOriginHeight; // 初始化时内容的高度Rect mEditRect; // 可绘制的区域double mRotate = 0.0; // 图像顺时针旋转的角度,以 π 为基准double mScale = 1.0; // 图像缩放的大小double mAlpha = 1.0; // 图像的透明度bool mIsSelected = false; // 是否处于选中状态bool mIsSingeFingerMove = false; // 是否处于单指移动的状态bool mIsDoubleFingerScaleAndRotate = false; // 是否处于双指旋转缩放的状态Widget mElementShowingWidget; // 展示内容的 widgetOffset mOffset; // ElementContainerWidget 相对屏幕的位移
复制代码

函数未动数据先行,数据结构是一个框架非常核心的东西,定义了一个好的数据结构可以省去很多不必要的代码。所以这一小节我们来根据代码块1定义一下数据结构和 Widget 绘制坐标系

  • 1.我们将 WE 所在的 ECWS 作为 WE 中 view 的可绘制区域,代码块1中的 mEditRect 就是这个区域代表的矩形。所以 mEditRect 一般为**[0, 0, ECWS.getWidth, ECWS.getHeight],mEditRect 的单位为px**。

  • 2.我们定义的坐标系原点在 mEditRect 的中心点,也就是 ECWS 的中心点。mMoveX、mMoveY 分别表示 view 距离坐标系原点的距离。因为它们俩默认为 0,所以一般 view 被添加到 ECWS 中的时候默认位置就在 ECWS 的中心。这两个参数的单位为px

  • 3.我们的坐标系具有 z 轴,mZIndex 就是 z 轴的坐标,z 轴表示 view 的层叠关系,mZIndex 为 0 时表示 view 在 ECWS 的顶层。mZindex 默认为 -1,表示 view 没有被添加到 ECWS 中。mZIndex 是整数

  • 4.我们定义 mRotate 为正时 view 顺时针转动,mRotate 的区间为[-360,360]。

    5.我们定义 view 没有缩放的时候 mScale 为 1,mScale 为 2 的时候表示 view 放大 2 倍,以此类推。

  • 6.mOriginWidth 和 mOriginHeight 为 view 的初始大小,单位是px

  • 7.mAlpha 为 view 的透明度,默认为 1 且小于等于1。

  • 8.剩下的参数就不用解释了,代码里面都有注释。

(2).WE是如何刷新元素的

-----代码块2----- ws_element.dartadd() {mElementShowingWidget = initWidget();}Widget initWidget();Widget buildTransform() {Matrix4 matrix4 = Matrix4.translationValues(mMoveX, mMoveY, 0);matrix4.rotateZ(mRotate);matrix4.scale(mScale, mScale, 1);return Transform(alignment: Alignment.center,transform: matrix4,child: Opacity(opacity: mAlpha,child: mElementShowingWidget,),);}
复制代码
  • 1.刷新元素的核心代码就是代码块2:

    • 1.首先在 ECWS 添加一个 WE 的时候,WE 的子类中可以通过实现 initWidget() 来初始化自己需要的元素内容
    • 2.然后每次数据更新时,我们会通过 buildTransform() 构建一个 Widget 给外部使用。
    • 3.而 buildTransfrom 内部则是通过 Matrix4 和 Transform 来实现移动旋转缩放,通过 Opacity 来进行 Alpha 变换。

(3).ECWS如何构建整个容器

-----代码块2----- element_container_widget.dart@overrideWidget build(BuildContext context) {RawGestureDetector gestureDetectorTwo = GestureDetector(child: GestureDetector(child: Stack(alignment: AlignmentDirectional.center,key: globalKey,children: mElementList.map((e) {return e.buildTransform();}).toList().reversed.toList()),onPanUpdate: onMove,behavior: HitTestBehavior.opaque,),).build(context);gestureDetectorTwo.gestures[RotateScaleGestureRecognizer] =GestureRecognizerFactoryWithHandlers<RotateScaleGestureRecognizer>(() => RotateScaleGestureRecognizer(debugOwner: this),(RotateScaleGestureRecognizer instance) {instance..onStart = onDoubleFingerScaleAndRotateStart..onUpdate = onDoubleFingerScaleAndRotateProcess..onEnd = onDoubleFingerScaleAndRotateEnd;},);return Listener(child: ConstrainedBox(constraints: BoxConstraints(minHeight: double.infinity,minWidth: double.infinity,),child: gestureDetectorTwo,),behavior: HitTestBehavior.opaque,onPointerDown: onDown,onPointerUp: onUp,);}复制代码
  • 1.我们都知道 State 中需要在 build() 中返回一个 Widget 给 StatefulWidget。
  • 2.为了装下多个有层叠关系的元素,我们使用 Stack 作为元素的容器。
  • 3.Stack 外面包装了 GestureDetector 来处理 move 事件。
  • 4.GestureDetector 外部包装了我自定义的 RotateScaleGestureRecognizer 来处理双指旋转缩放事件。
  • 5.最外层则是用 Listener 来监听手指 down 和 up 事件。
  • 6.上面这样的设计的原因我会在后面深入 Flutter 的时候讲解。

3.源码流程解析

这一节我主要会对项目中的测试 demo 进行源码流程分析,让读者对控件整体的运行方式有个简单的了解。这一节主要是讲解源码,所以读者一定要去 clone 源码,跟随文章的脚步前进。

(1).添加元素

  • 1.简单的初始化动作我就不赘述了,我们从 main.dart 的 add 按钮开始。点击后先会创建一个 StickerElement 这个是我测试用的元素,里面代码很简单也不说了。
  • 2.addSelectAndUpdateElement 是一个组合方法,里面调用了 addElementselectElementupdate,也就是添加元素,选中元素,更新元素。我们一个个来分析::
    • 1.addElement:这个方法里主要做了下面这些事情:

      • 1.进行数据检查,如果被添加的 WE 为空或者该 WE 已经在 ECWS 中,那么添加失败。
      • 2.在 ECWS 中我维持了一个 WE 的 List,所有的 WE 都存于其中,每次 add 的时候 WE 都会被添加到 list 的最前面 ,其他 WE 的 mZIndex 也会顺势更新。
      • 3.调用 WE.add 方法,里面使用 initWidget 初始化了 mElementShowingView,前面我们说过了 initWidget 的逻辑由子类定义。
      • 4.调用监听器的对应方法,且调用自动取消选中的方法(ECWS 可以被外部决定是否自动取消选中)。
    • 2.selectElement:WE 被 add 了之后,我们这里直接将其选中,代码里面主要做了下面这些事情:
      • 1.进行数据检查,如果需要选中的 WE 没有被添加到 ECWS 中则选中失败。
      • 2.将需要选中的 WE 从 list 中移除然后添加到 list 的顶部,然后顺便更新其他 WE 的 mZIndex。
      • 3.调用 WE 的 select 方法,里面主要就是更新要选中的 WE 的数据。
      • 4.调用监听器对应的方法。
    • 3.update:前面都做好了,就需要将 WE 调整到其应该的状态,这里我想大家都猜到了就是调用 setState 然后其会触发我们在第二节中说的 build 方法,然后调用每个 WE 的 buildTransform 返回数据被更新后的 Widget。

(2).元素单指手势

元素手势不像添加元素那样需要外部调用,元素手势是通过事件分发触发的,我们这里不讲 Flutter 的事件分发机制,只讲我们基于其上的逻辑。

  • 1.对于元素单指手势的处理,主要看三个触摸事件:down、move、up。所以我们直接看 ECWS.build 中设置的三个回调方法。

    • 1.onDown 里面的逻辑如下:

      • 1.通过 findElementByPosition 根据 down 的位置找到当前位置下最顶层的 WE。
      • 2.如果当前有选中的 WE 且与当前触摸 WE 是同一个的话,那么先调用 downSelectTapOtherAction,这个函数可以被子类覆写,默认返回 false。也就是说子类可以优先处理当前事件,如果子类处理了这个事件,那么 return。如果子类不处理,那么将 mMode 标记为 SELECTED_CLICK_OR_MOVE,表示最终的手势可能是点击元素,也可能是移动元素。具体的行为需要 move 或者 up 的时候才能判定。
      • 3.如果当前有选中的 WE 但与当前触摸的 WE 不是同一个的时候也分两种情况:一种情况是触摸的 WE 不存在,此时表示将 mMode 标记为 SINGLE_TAP_BLANK_SCREEN 表示点击了 ECWS 的空白区域。另一种情况是触摸的 WE 存在,此时表示重新选中了一个 WE。
      • 4.如果当前没有选中的 WE,也会有两种情况:一个是触摸的 WE 也不存在,那么和前面一样表示点击空白区域。否则的话就是选中一个 WE。
    • 2.onMove 中会优先将 move 事件交给 scrollSelectTapOtherAction,该方法也可以被子类覆写,同样默认返回 false,如果子类处理了这个事件,那么就直接 return 了。否则当 mModeSELECTED_CLICK_OR_MOVE(已经选中了 WE 开始移动)、SELECT(没有选中 WE 开始移动)、MOVE(WE 移动过程中) 三种情况中的一种的时候,都可以触发移动手势。具体的逻辑在 singleFingerMove 中:
      • 1.先根据 mMode 的状态,调用 singleFingerMoveStartsingleFingerMoveProcess。singleFingerMoveStart 中调用了监听器和 WE 的对应方法,里面基本没什么逻辑。 singleFingerMoveProcess 中也调用了监听和 WE 的对应方法,但是 WE 的对应方法中更新了 mMoveX 和 mMoveY 的数据。
      • 2.调用 update 更新 WE 中的 view。将 mMode 设置为 MOVE,表示处于移动中。
    • 3.onUp 方法:
      • 1.mModeSELECTED_CLICK_OR_MOVE,到这里的时候才能确认,用户的行为是选中了元素之后的点击,我们在前面分析过了这里面的事件分发的机制,这里也不赘述了。
      • 2.mModeSINGLE_TAP_BLANK_SCREEN,表示点击 ECWS 的空白处,这里调用的 onClickBlank 也是可以被子类覆写的,可以实现一些自己的逻辑。
      • 3.mModeMOVE,结束调用单指移动结束。

三、Flutter探究

这一章我会从一个 Android 工程师的角度来研究一下 Flutter,讲一讲我在移植控件时遇见的问题们。

1.Flutter与Android对比

先看看 Flutter 与 Android 写的 App 实际的比较吧

  • 1.我在将代码从 Android 移植到 Flutter 上花费了大概 10 个小时。整个控件在 Android 上开始设计到开发完成则是花费了 100 多个小时。所以整个库的移植成本并不算太高。
  • 2.看上面 gif 的比较,可以发现流畅度上面并没有区别。我找了几个朋友实际体验了一下,大家都同样没有发现使用起来有差异。
  • 3.图3、图4分别是 Flutter 和 Android 的性能图。我们发现的确像很多测评文章里面说到的。Flutter 的内存消耗要比 Native 多。在实验比较的时候我添加了几十个元素。最后两端都稳定在了一个内存数值上面。Flutter 是 256MB 左右,Android 是 128MB 左右。
  • 4.在移植代码的过程中,我总结了下面这些写 Java 和 Dart 之间的区别:

    • 1.Dart 有非常多的语法糖,代码比起 java 来说有比较多的精简。
    • 2.Dart 的传参方式使得写 Flutter 控件的时候更像是在写属性配置表。

2.Flutter原理

以一个 Android 工程师的眼光来看 Flutter

(1).Flutter的事件简单总结

  • 1.LIstener 是手势的基础:GestureDetector 是基于 Listener 开发的。

  • 2.事件自底向上,事件不可截断

    • 1.先定义一下:自底向上表示从子 view 到父 view。自顶向下表示从父 view 到子 view。
    • 2.做过 Android 的同学知道 Android 中的事件**是一个自顶向下再自底向上的过程。**在中间的任意一环我们都可以进行拦截,从而让事件不再继续传递。
    • 3.Flutter 的事件模型则是:自底向上,而且目前来看没有任何操作能阻断这个流程。
    • 4.也就是说,如果我们使用 Listener 对任意一个 Widget 进行监听,那么我们在事件传递的过程中阻止 Listener 获取事件。
    • 5.事件不可截断的特性在开发中最有用的地方就是:如果我们使用 tapUp,tapDown,这类手势想要监听手指的抬起和放下,那么这些手势可能会被其他手势给冲掉。此时我们就能使用 Listener 来通过监听具体的 down 和 up 事件,因为这个是不可截断的。
  • 3.开发中我们使用 GestureDetector 封装 Widget,我们定义的一个个手势回调会让 GestureDetector 生成多个 GestureRecognizer 附着在当前的 Widget 上以处理 Widget 接收到的事件。

  • 4.每根手指的 down、move、up 都是一个事件流,当 down 事件自底向上确立了一个 Widget 链的时候,附着在链中各个 Widget 上的 GestureRecognizer 们就会去竞争这个事件流的归属。

  • 5.一个事件流的胜出 GestureRecognizer 只有一个,胜出后整个事件流都属于这个 GestureRecognizer 。

  • 6.GestureRecognizer 的胜出机制,就是 Flutter 在事件不可截断这个 feature 上的补充的灵活性,可以使得某个 Widget 上的手势被截断,推荐优先使用 Gesture

  • 7.Gesture 的胜出机制是怎么样的?

    • 1.如果一次竞争中只有一个 GestureRecognizer,那么他就直接胜出。
    • 2.如果一次竞争中有多个相同的 GestureRecognizer,那么越底层的越胜出。
    • 3.如果一次竞争中有不同的 GestureRecognizer:
      • 1.GestureRecognizer 中定义了一个超时机制,有些 GestureRecognizer 定义了某个事件进行了一个时间阈值后如果没有其他 GestureRecognizer 申请延长阈值那么本 GestureRecognizer 就直接胜出。例如:TapGestureRecognizer 定义了 down 事件进行了 100 ms 之后,如果没有其他 GestureRecognizer 延长阈值,那么自己就获得事件流。
      • 2.而 LongPressGestureRecognizer 定义的时间阈值是 500ms,如果 500ms 后没有其他 GestureRecognizer 申请延长阈值则自己获得事件流。
      • 3.那么 TapGestureRecognizer 和 LongPressGestureRecognizer 都在的时候,通过 down 事件的长短来判断谁胜出。

(2).Flutter的绘制逻辑

Flutter 核心原理

四、尾巴

啊!感觉这篇文章有点虎头蛇尾的感觉,文章从开始到结束跨了好几周。中间又是加班又是搬家,把我的热血都消磨了。本来多加一些 Flutter 的深入探究的,但是感觉会越写越久,所以先就这样。接下来我会写一系列文章来分析 Flutter 的原理和 Flutter Sdk。所以更多内容敬请期待!ps:一鼓作气,再而竭,三而衰。真是完美的表现了我写这篇文章的过程,希望读者们不要学我。

连载文章

  • 1.从零开始仿写一个抖音app——开始
  • 4.从零开始仿写一个抖音App——日志和埋点以及后端初步架构
  • 5.从零开始仿写一个抖音App——app架构更新与网络层定制
  • 6.从零开始仿写一个抖音App——音视频开篇
  • 7.从零开始仿写一个抖音App——基于FFmpeg的极简视频播放器
  • 8.从零开始仿写一个抖音App——跨平台视频编辑SDK项目搭建
  • 9.从零开始仿写一个抖音App——Android绘制机制以及Surface家族源码全解析

不贩卖焦虑,也不标题党。分享一些这个世界上有意思的事情。题材包括且不限于:科幻、科学、科技、互联网、程序员、计算机编程。下面是我的微信公众号:世界上有意思的事,干货多多等你来看。

转载于:https://juejin.im/post/5d087bad5188257de35fd911

移植一个抖音贴纸组件到Flutter相关推荐

  1. 用Flutter手撸一个抖音国际版 看看有多炫

    肉眼品世界导读: 本文为启明星技术架构师社群作者投稿,字节跳动选择了flutter作为混合开发的语言,Flutter "一出生"就以"UI 漂亮.像素级可控.性能流畅.可 ...

  2. 第二篇-用Flutter手撸一个抖音国内版,看看有多炫

    前言 继上一篇使用Flutter开发的抖音国际版 后再次撸一个国内版抖音,大部分功能已完成,主要是Flutter开发APP速度很爽,  先看下图 项目主要结构介绍 这次主要的改动在api.dart 及 ...

  3. android sdk build-tools_从零开始仿写一个抖音App——视频编辑SDK开发(一)

    本文首发于微信公众号--世界上有意思的事,搬运转载请注明出处,否则将追究版权责任.交流qq群:859640274. 大家好久不见,又有一个多月没有发文章了.不知道还有哪些读者记得我的 从零开始仿写抖音 ...

  4. 从零开始写一个抖音App——Apt代码生成技术、gradle插件开发与protocol协议

    1.讨论--总结前两周评论中有意义的讨论并给予我的解答 2.mvps代码生成原理--将上周的 mvps 架构的代码生成原理进行解析 3.开发一款gradle插件--从 mvps 的代码引出 gradl ...

  5. 从零开始仿写一个抖音App——基于FFmpeg的极简视频播放器

    本文首发于微信公众号--世界上有意思的事,搬运转载请注明出处,否则将追究版权责任.微信号:a1018998632,交流qq群:859640274 1.从零开始仿写一个抖音app--开始 4.从零开始仿 ...

  6. 一人一天,如何开发一个抖音级的短视频?

    7 月,抖音全球日活跃用户 1.5 亿,月活跃用户突破 5 亿,其活跃程度及用户粘性概括为「抖音五分钟,人间两小时」.毫无疑问抖音是 2018 年最火应用之一.抖音的火爆,不仅意味着垂直短视频可以获得 ...

  7. 从零开始仿写一个抖音App

    点击上方"何俊林",马上关注,每天早上8:50准时推送 真爱,请置顶或星标 本文转载自公号开发者技术前线,原文:https://juejin.im/post/5b9e9bf1e51 ...

  8. 手写一个抖音视频去水印工具,千万别刚一个程序员

    百因必有果 说一下我为什么要做个抖音视频去水印工具,其实是因为我的沙雕女友,她居然刚我~ 有天晚上她在抖音看见一个非常具有 教育意义 的视频,"男人疼媳妇就该承包全部家务活",然后 ...

  9. 音乐、游戏、教育,谁是字节跳动的下一个“抖音”?

    务实且浪漫的字节跳动,不知何时才能找到下一个"抖音". 全文5809字,阅读约需11分钟 来源 | 亿欧网 文|王乙淇 编辑|张宇喆 7月1日,字节跳动被曝已将音乐升级为P1优先级 ...

最新文章

  1. python打印换行符_Python换行符以及如何在不使用换行符的情况下进行Python打印
  2. 企业级 CICD 工具部署 Serverless 应用的落地实践
  3. python布尔系列_python数据分析类库系列-Numpy之布尔型索引
  4. boost::core模块default_allocator
  5. VTK:Filtering之ProgrammableFilter
  6. Codeforces 558E A Simple Task
  7. Apache Ant 的安装
  8. 安全漏洞——如何查找和修复它们
  9. 我想和iOS大牛们交流的问题
  10. grDevices | R语言中的配色方法汇总(Ⅱ-2)
  11. dotnet 入门到放弃 使用 .NET Core 卸载工具
  12. STM32F1--FreeRTOS系统移植运行报错:L6218E:Undefined symbol xTaskGetSchedulerState (referred from delay.o)
  13. chroma8000使用_台湾致茂MES系统Chroma8000上使用更安全敏捷
  14. 凤凰汽车登陆纳斯达克:累计亏损1820万美元,持续经营能力存疑问
  15. 学习KNN算法重点目标
  16. 为什么ad域打开失败_【AD】域环境常见错误集
  17. 趁年轻要学会打破思维
  18. Kubernetes新近kubectl及CNI漏洞修复,Rancher 2.2.1发布
  19. 无胁科技-TVD每日漏洞情报-2022-9-26
  20. Linux学习-40-格式化分区mkfs、mke2fs命令用法

热门文章

  1. 捷信达集团办设置(windows7 或win 8.1)
  2. 3个iPhone设置让你成为“人类高质量iPhone用户”
  3. HDU-动态规划题集【转】
  4. 图片返回base64数据渲染为图片的处理
  5. 设计模式之七大原则——里氏替换原则(LSP)(三)
  6. 4399移动产品经理:海量关键词优化策略
  7. conda删除虚拟环境
  8. jquery:当内容发生改变时触发事件
  9. 对接eBay流程(demo可直接运行)
  10. 知识图谱偏重于信息还是计算机,基于知识图谱的计算机领域胜任力研究与应用...