# WandAndroidKotlin
**Repository Path**: trydamer/wand-android-kotlin
## Basic Information
- **Project Name**: WandAndroidKotlin
- **Description**: WanAndroidKotlin版
- **Primary Language**: Unknown
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 2
- **Forks**: 0
- **Created**: 2022-06-10
- **Last Updated**: 2023-11-14
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
## WanAndroid—Kotlin
  
### 一、前言
本项目主要是为了学习Kotlin和Android而开发,无商业用途。代码和UI设计参考了[iceCola7/WanAndroid](https://github.com/iceCola7/WanAndroid)和[goweii/WanAndroid](https://github.com/goweii/WanAndroid)项目,所用API均来自[WanAndroid](https://www.wanandroid.com/)。我的[WanAndroid-Java版本点我跳转](https://gitee.com/trydamer/wan-android-java),由于Android趋势于Kotlin,Java版本的可能更新不那么频繁了。下文主要记录一些功能的实现原理以及遇到的问题和解决方案。
### 二、功能实现原理
#### 2.1 注册登录背景
这里主要是图中弯进去的部分,通过自定义View来绘制的。弯进去的部分并不是一个圆弧,而是[贝塞尔曲线](https://blog.csdn.net/xiaozhangcsdn/article/details/98963937)。在onDraw方法中,找到控件的左右两个点的坐标,然后再根据参数找到控制点的坐标。用[Path](https://blog.csdn.net/w_t_y_y/article/details/59542548)来绘制一条二阶贝塞尔曲线,然后再首位相连即可。对应代码如下:
```kotlin
// 起点
path.moveTo(0f, height.toFloat())
// 控制点和终止点
path.quadTo(x.toFloat(), (height - y).toFloat(), width.toFloat(), height.toFloat())
// path.close()方法是让最后一个点和起点连接起来,形成一个
// 封闭的区域,也可以调用lineTo(以直线连接,等价与close)或其他方法来
// 让其形成封闭区域
path.close()
// 填充
paint.style = Paint.Style.FILL
// 绘制
canvas.drawPath(path, paint)
```
#### 2.2 使用ViewBinding
使用ViewBinding可以提高我们的开发效率,它帮我们省掉了findViewById的过程,也不需要在Activity或Fragment等中定义许多View。使用方式如下:
- 1. 在app下的build.gradle的android节点中声明使用ViewBinding
```gradle
viewBinding {
enabled = true
}
```
- 2. 在Activity(或Fragment等)中创建ViewBinding
```kotlin
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(LayoutInflater.from(this))
setContentView(binding.root)
binding.tvTitle.text = "ViewBinding"
}
```
binding.tvTitle中的tvTitle就是layout中id为“tv_title”的控件。
为了更加方便,把ViewBinding加入到BaseActivity中,这样就不用写那么多重复的代码了,如下:
```kotlin
abstract class BaseActivity(
private val inflate: (layout: LayoutInflater) -> V
) : AppCompatActivity() {
protected lateinit var binding: V
override fun onCreate(saveInstanceState: Bundle?) {
super.onCreate(saveInstanceState)
binding = inflate(layoutInflater)
setContentView(binding.root)
initView()
}
protected abstract fun initView()
}
```
其他Activity使用时,代码如下:
```kotlin
class OpenSourceActivity:
BaseActivity(ActivityOpenSourceBinding::inflate) {
private lateinit var openSourceAdapter: OpenSourceAdapter
override fun initView() {
openSourceAdapter = OpenSourceAdapter(this, getOpenSourceDatas())
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.isNestedScrollingEnabled = false
binding.recyclerView.adapter = openSourceAdapter
}
}
```
Kotlin的一个好处之一就是可以像C语言一样把函数当作参数传递,如下:
```kotlin
private val inflate: (layout: LayoutInflater) -> V
```
上面这行代码的意思:inflate是一个方法,接收一个类型为LayoutInflater的参数,返回类型是V。
当然了,ViewBinding其实是通过生成中间代码为我们省去了findViewById等这些操作,本质上是没有变的。如下图所示,在OpenSourceActivity中的”ActivityOpenSourceBinding::inflate“就是传递了一个方法过去。

对于Fragment,也是如此。
#### 2.3 在Adapter中使用ViewBinding
在Adapter中使用ViewBinding要稍微复杂一些(仅仅复杂一些而已)。同样地,为了减少代码的重复性,定义一个基类Adapter,其他使用ViewBinding的Adapter继承该基类Adapter即可。
基类Adapter:
```kotlin
/**
* 使用ViewBinding的基类RecyclerView适配器
*
* 数据类型
* ViewBiding
* ViewHolder
*
* @param data 数据
* @param inflate ViewBinding的inflate
* */
abstract class RecyclerViewBindingAdapter>(
protected val context: Context,
protected val data: List,
private val inflate: (layout: LayoutInflater) -> VB
) : RecyclerView.Adapter() {
/**
* 重写创建ViewHolder方法,改变创建布局的方法
*
* @param parent 父布局
* @param viewType 布局类型
* @return ViewHolder
* */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
return inflate(LayoutInflater.from(parent.context)).run { onCreateViewHolder(this) }
}
override fun onBindViewHolder(holder: VH, position: Int) { onBindViewHolder(holder.binding, data[position], position)
}
override fun getItemCount(): Int {
return data.size
}
abstract fun onCreateViewHolder(binding: VB): VH
/**
* 直接把binding和data传给实现者,使用方便
*
* @param binding ViewBinding
* @param item 数据
* @param position 第几项数据
* */
abstract fun onBindViewHolder(binding: VB, item: T, position: Int)
}
```
因为基类Adapter是继承RecyclerView.Adapter,因此还需要一个ViewHolder,如下:
基类ViewHolder:
```kotlin
/**
* RecyclerView使用ViewBinding的ViewHolder
* */
open class ViewBindingViewHolder(val binding: V) : RecyclerView.ViewHolder(binding.root)
```
“我的”页面下的选项适配器:
```kotlin
class MineSetAdapter(
context: Context,
data: List
) : RecyclerViewBindingAdapter>(
context,
data,
MineSetItemBinding::inflate
) {
private var onMineSetItemClickListener: OnMineSetItemClickListener? = null
fun setOnMineSetItemClickListener(onMineSetItemClickListener: OnMineSetItemClickListener) {
this.onMineSetItemClickListener = onMineSetItemClickListener
}
override fun onCreateViewHolder(binding: MineSetItemBinding): ViewBindingViewHolder {
return ViewBindingViewHolder(binding)
}
override fun onBindViewHolder(binding: MineSetItemBinding, item: MineSetDTO, position: Int) {
binding.imgIcon.setImageResource(item.icon)
binding.tvTitle.text = item.title
binding.tvContent.text = item.content
binding.root.setOnClickListener {
onMineSetItemClickListener?.run {
onMineSetItemClick(item, position)
}
}
}
}
```
布局文件xml:
```xml
```
效果:
与不使用ViewBinding的Adapter相比,确实减少了许多工作量,这样我们便能够更集中于开发本身的事情了。
#### 2.4 流式布局——FlexboxLayout
先上效果图,不然可能说了半天都不知道流式布局是个什么玩意儿,如下图:
其实也就是这么个玩意儿,之前看到其他App的热搜/搜索历史时,还在想这是如何实现的,不过当时也只是想想罢了。关于流式布局,在[WanAndroid-Java](https://gitee.com/trydamer/wan-android)中已经详细说明了,这里提一下,就不再赘述。
#### 2.5 多选适配器
长按列表item进入多选状态,再次长按退出多选,效果图如下:
[多选适配器](app/src/main/java/com/lcj/wanandroidkotlin/base/adapter/CheckAdapter.kt)首先加载自己的布局,然后再把子类的布局加载到该布局中即可。对于子类来说,和使用[RecyclerViewBindingAdapter](app/src/main/java/com/lcj/wanandroidkotlin/base/adapter/RecyclerViewBindingAdapter.kt)差不多。
布局如下:
```xml
```
”选择框“默认隐藏,这样在显示的时候其实就和子布局一样,当进入选择模式后,把”选择框“显示出来即可达到上图的效果。创建ViewHolder的代码如下:
```kotlin
/**
* 重写{@link onCreateViewHolder}方法,屏蔽创建ViewHolder的方法。
* 先加载“多选”布局,然后再把子类的布局添加到“多选”布局中
* @param parent 父view
* @param viewType 没有用到viewType
* @return CheckViewHolder
* */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheckViewHolder {
return SelectRecyclerItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
).run {
CheckViewHolder(this)
}.run {
setSubViewBinding(
inflate(
LayoutInflater.from(binding.root.context),
binding.root,
true
)
)
}
}
```
其中CheckViewHolder如下:
```kotlin
open class CheckViewHolder(
binding: SelectRecyclerItemBinding
) : ViewBindingViewHolder(
binding
) {
lateinit var subBinding: V
fun setSubViewBinding(subBinding: V) = apply {
this.subBinding = subBinding
}
}
```
在绑定ViewHolder时,把subBinding传递给子类即可。
当然,该适配器并不适用于多列视图或者横向视图等,因为”选择框“总是出现在左边,而且该适配器还屏蔽了viewType,用于常规列表视图比较合适。
为什么不能把”选择框“放到右边?因为使用线性布局,如果把”选择框“放到右边,由于子布局是match_parent,所以”选择框“会被挤到屏幕之外了。当然也不是不可以把”选择框“放到右边,只是布局和逻辑要稍微复杂一点。把布局修改为:
```xml
```
创建ViewHolder的代码改为:
```kotlin
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheckViewHolder {
return SelectRecyclerItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
).run {
CheckViewHolder(this)
}.run {
setSubViewBinding(
inflate(
LayoutInflater.from(binding.fltBody.context),
binding.fltBody,
true
)
)
}
}
```
在onBindViewHolder中,需要做一些判断,关键代码如下:
```kotlin
if (checkModel) {
if (showOnRight) {
holder.binding.imgCheckedLeft.visibility = View.GONE
holder.binding.imgCheckedRight
} else {
holder.binding.imgCheckedRight.visibility = View.GONE
holder.binding.imgCheckedLeft
}.run {
setImageResource(
if (data[position].checked) {
R.drawable.ic_selected_red
} else {
R.drawable.ic_un_select
}
)
visibility = View.VISIBLE
}
} else {
holder.binding.imgCheckedRight.visibility = View.GONE
holder.binding.imgCheckedLeft.visibility = View.GONE
}
```
在创建适配器时,指定”选择框“在左边还是右边即可。效果图如下图:
事实上只要能够实现一种,其他样式都只需要如法炮制即可。
#### 2.6 侧滑适配器
侧滑使用的框架是[com.daimajia.swipelayout](https://github.com/daimajia/AndroidSwipeLayout),[Java版](https://gitee.com/trydamer/wan-android)的侧滑框架使用的是[mcxtzhang/SwipeDelMenuLayout](https://github.com/mcxtzhang/SwipeDelMenuLayout),使用daimajia的要稍微复杂一些,但相对来说也比较好用。效果图如下:
添加依赖:
```gradle
// 基础库
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'
// 动画库,可选
implementation 'com.daimajia.androidanimations:library:1.1.2@aar'
```
虽然框架提供了BaseSwipeAdapter,但该适配器是继承自ListView的,而我们使用的是RecyclerView,因此需要另一个BaseSwipeAdapter。先照着框架的抄一遍,然后再改改就行了,代码如下:
```kotlin
abstract class SwipeAdapter>(
context: Context,
data: List,
inflate: (layoutInflater: LayoutInflater, parent: ViewGroup, attachToParent: Boolean) -> VB
) : RecyclerViewBindingAdapter(
context = context,
data = data,
inflate = inflate
) {
private val swipeLayoutList = mutableListOf()
/**
* 关闭所有的SwipeLayout
* @param filterLayout 屏蔽的SwipeLayout
* */
fun closeAllSwipe(filterLayout: SwipeLayout?) {
for (swipeLayout in swipeLayoutList) {
if (filterLayout == swipeLayout) {
continue
}
if (swipeLayout.openStatus != SwipeLayout.Status.Open) {
continue
}
swipeLayout.close()
}
}
override fun onBindViewHolder(holder: VH, position: Int) {
super.onBindViewHolder(holder, position)
val swipeLayout = getSwipeLayout(holder.binding, position)
swipeLayout.addSwipeListener(object : SwipeLayout.SwipeListener {
override fun onStartOpen(layout: SwipeLayout?) {
closeAllSwipe(null)
}
override fun onOpen(layout: SwipeLayout?) {
layout?.let { swipeLayoutList.add(it) }
}
override fun onStartClose(layout: SwipeLayout?) {
}
override fun onClose(layout: SwipeLayout?) {
layout?.let { swipeLayoutList.remove(it) }
}
override fun onUpdate(layout: SwipeLayout?, leftOffset: Int, topOffset: Int) {
}
override fun onHandRelease(layout: SwipeLayout?, xvel: Float, yvel: Float) {
}
})
}
abstract fun getSwipeLayout(binding: VB, position: Int): SwipeLayout
}
```
也许你使用的效果是这样的:
不要慌,虽然效果很怪异,但你也发现了这个SwipeLayout的原理。只是在使用的时候没有给item加上背景。因为当时我并没有在意demo的背景,百度也没有找着原因,只得慢慢摸索。我按照demo加了一个背景然后就可以了,在正常显示的item中加入背景即可。
#### 2.7 ItemDecoration
考虑一个问题,在侧滑列表中还有分割线,为什么侧滑显示的“按钮”没有紧挨着分割线。以往在列表中加入分割线,都是直接在布局中的最下面加一个View,如首页的文章适配器的布局就是这样做的:
```xml
```
但是如果在侧滑的布局中也这样搞,那么侧滑时显示出来的“按钮”就和item不齐平。毕竟item的高度不一致,所以不能固定高度。
解决这个问题其实很简单,就是用[ItemDecoration](https://www.jianshu.com/p/8a51039d9e68),也许你知道它,但我是前两周才知道这个东西!!!
ItemDecoration有几个比较重要的方法:
```kotlin
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
)
```
在这个方法中设置Item的上下左右边距,具体说明请看我下面盗的这张图:

```kotlin
override fun onDraw(
c: Canvas,
parent: RecyclerView,
state: RecyclerView.State
)
```
这个方法和View的onDraw一样,画就完了。不同的是,在这个方法里面应该遍历parent的子view来画,即修饰每一个当前可见的item。
```kotlin
override fun onDrawOver(
c: Canvas,
parent: RecyclerView,
state: RecyclerView.State
)
```
这个方法也是绘制的,和onDraw唯一的区别就是调用顺序,简单来说:
- onDraw 绘制的东西如果与item重叠,则重叠区域被item覆盖;
- onDrawOver 绘制的东西如果与item重叠,则重叠区域被onDrawOver覆盖。
回到正题,我们在onDraw中绘制分割线即可:
```kotlin
class SwipeItemDecoration(context: Context): RecyclerView.ItemDecoration() {
private val paint: Paint = Paint()
private val MARGIN_TOP = DeviceUtil.dp2px(10)
private val MARGIN_BOTTOM = DeviceUtil.dp2px(10)
private val LINE_START_X = DeviceUtil.dp2px(50.0F)
private val LINE_END_X = DeviceUtil.dp2px(50.0F)
private val LINE_BASE_Y = DeviceUtil.dp2px(10.0F)
init {
paint.strokeWidth = DeviceUtil.dp2px(1.0F)
paint.color = if (ThemeUtils.isDarkMode(context)) {
ContextCompat.getColor(context, R.color.night_split_line)
} else {
ContextCompat.getColor(context, R.color.light_split_line)
}
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.set(0, MARGIN_TOP, 0, MARGIN_BOTTOM)
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
if (parent.childCount > 0) {
/**
* 最后一条数据不绘制分割线
* */
val count = if (parent.getChildAdapterPosition(parent.getChildAt(parent.childCount - 1)) ==
parent.adapter?.itemCount!! - 1) {
parent.childCount - 1
} else {
parent.childCount
}
for (index in 0 until count) {
val childView = parent.getChildAt(index)
val y = childView.bottom + LINE_BASE_Y
c.drawLine(LINE_START_X, y, childView.right - LINE_END_X, y, paint)
}
}
}
}
```
在onDraw中,通过
```kotlin
parent.getChildAdapterPosition(parent.getChildAt(parent.childCount - 1))
```
来判断当前view是不是最后一条数据的view,从而决定要不要画分割线,最后在页面中调用:
```kotlin
binding.ildBody.recyclerView.addItemDecoration(SwipeItemDecoration(this))
```
**关于ItemDecoration更多用法,可以参考我的另一个项目,[LogisticsDemo](https://gitee.com/trydamer/logistics-demo)**
### 三、问题记录与解决方案
#### 3.1 传递Parcelable问题
在Kotlin中,页面之间传递Parcelable尤为简单,在build.gradle中添加:
```gradle
/*序列化插件*/
apply plugin: 'kotlin-parcelize'
```
后即可直接使用注解的方式序列化,如下:
```kotlin
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class TestBean(
val name: String,
val test: List,
val id: Long,
val other: List,
val remark: String,
val detail: UserInfoBean
): Parcelable
```
这样即可使用,不必像Java那样还要实现Parcelable的方法。但是,据说这种方式如果是混合开发(Kotlin+Java)会出现问题。当然我本人是没有验证过的,只是头铁,就要通过手动实现Parcelable方法来传递。
当数据没有List时一切正常,但是如果数据中有List,则传递会失败(只是List会出错,其他数据依然正常)。百度上查了许久,仍然找不到解决方案。最终只能分开传递,非List数据正常传递,Parcelable不写入也不读取List数据,List数据再另传。如下:
```kotlin
KnowledgeActivity.start(requireContext(), parent as KnowledgeDTO, parent.children as ArrayList, childPosition)
```
然后再在接收页面分别读取两个数据,最终合到一起即可。虽然这种方式相对要复杂一点,但也能满足需求。
#### 3.2使用ViewBinding相关问题
使用ViewBinding提高了开发效率,如需使用,在build.gradle的android节点中加入:
```gradle
viewBinding {
enabled = true
}
```
即可。在Activity(或Fragment)中使用如下:
基类Activity:
```kotlin
abstract class BaseActivity(private val inflate: (layout: LayoutInflater) -> V) : AppCompatActivity(),
KeyBoardAction {
protected lateinit var binding: V
override fun onCreate(saveInstanceState: Bundle?) {
super.onCreate(saveInstanceState)
binding = inflate(layoutInflater)
setContentView(binding.root)
initView()
}
// 省略其他代码
}
```
其他Activity:
```kotlin
class WebActivity: BaseActivity(ActivityWebBinding::inflate) {
override fun initView() {
binding.webView.loadUrl(intent.getStringExtra(ActivityConstant.WEB_ACTIVITY_URL))
}
}
```
非常方便。但是ViewBinding出现问题时也不好排查,下面记录一下遇到的问题
##### 3.2.1 Adapter中的问题
基类Adapter:
```kotlin
/**
* 使用ViewBinding的RecyclerView适配器基类
*
* 数据类型
* ViewBiding
* ViewHolder
*
* @param data 数据
* @param inflate ViewBinding的inflate
* */
abstract class RecyclerViewBindingAdapter>(
protected val context: Context,
protected val data: List,
private val inflate: (layout: LayoutInflater) -> VB
) : RecyclerView.Adapter() {
/**
* 重写创建ViewHolder方法,改变创建布局的方法
*
* @param parent 父布局
* @param viewType 布局类型
* @return ViewHolder
* */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
return inflate(LayoutInflater.from(parent.context)).run { onCreateViewHolder(this) }
}
override fun onBindViewHolder(holder: VH, position: Int) {
onBindViewHolder(holder.binding, data[position], position)
}
override fun getItemCount(): Int {
return data.size
}
abstract fun onCreateViewHolder(binding: VB): VH
/**
* 直接把binding和data传给实现者,使用方便
*
* @param binding ViewBinding
* @param item 数据
* @param position 第几项数据
* */
abstract fun onBindViewHolder(binding: VB, item: T, position: Int)
}
```
基类ViewHolder:
```kotlin
/**
* RecyclerView使用ViewBinding的ViewHolder
* */
open class ViewBindingViewHolder(val binding: V) : RecyclerView.ViewHolder(binding.root)
```
“我的”页面下的选项适配器:
```kotlin
class MineSetAdapter(
context: Context,
data: List
) : RecyclerViewBindingAdapter>(context, data, MineSetItemBinding::inflate) {
private var onMineSetItemClickListener: OnMineSetItemClickListener? = null
fun setOnMineSetItemClickListener(onMineSetItemClickListener: OnMineSetItemClickListener) {
this.onMineSetItemClickListener = onMineSetItemClickListener
}
override fun onCreateViewHolder(binding: MineSetItemBinding): ViewBindingViewHolder {
return ViewBindingViewHolder(binding)
}
override fun onBindViewHolder(binding: MineSetItemBinding, item: MineSetDTO, position: Int) {
binding.imgIcon.setImageResource(item.icon)
binding.tvTitle.text = item.title
binding.tvContent.text = item.content
binding.root.setOnClickListener {
onMineSetItemClickListener?.run {
onMineSetItemClick(item, position)
}
}
}
}
```
布局文件xml:
```xml
```
实际效果:
布局文件中把item的高度设置为50dp,然而却达不到理想的效果。
**解决方案**
这个问题在BannerAdapter也出现了,因为只传递了LayoutInflater,没有传递parent等参数,所以在适配器中会出现问题,把参数
```kotlin
private val inflate: (layout: LayoutInflater) -> VB
```
改成
```kotlin
private val inflate: (layout: LayoutInflater, parent: ViewGroup, attachToParent: Boolean) -> VB
```
即可
#### 3.3 使用SwipeLayout的问题
**问题描述**
我想在侧滑的基础上加一个多选,让列表既可以侧滑,又可以多选。我在多选的布局中加入SwipeLayout。导致无法进入多选模式,点击事件也无效。即在2.5节中的布局中加入SwipeLayout,而设置长按和点击事件的控件是最外层的LinearLayout。
**解决方案**
把SwipeLayout放到外面,监听SwipeLayout的子View即可,父布局如下:
```xml
```
因为侧滑显示的控件不尽相同,所以在创建ViewHolder时再把具体的布局放到SwipeLayout中即可。
### 四、扩展
#### 4.1 ffmpeg
上文的动图都是通过ffmpeg生成的,这里不是为了炫技哈,只是实在不想安装那些S十三软件。这里作者是直接使用编译好的软件,[点我下载](https://github.com/BtbN/FFmpeg-Builds/releases)。因为是使用命令操作,顺便记录一下一些常用命令。
##### 4.1.1 视频生成gif
```shell
ffmpeg -ss 4 -t 21 -i "E:\\KuaiVideos\\2022-07-03 17-13-06.mp4" -vf "crop=iw/3:ih:iw/3:0" check_adapter.gif
```
上述命令的各个参数解释:
- -ss 4 表示从视频的第4秒开始。
- -t 21表示时长为21秒。
- -i "E:\\...."是视频路径。
- -vf "crop=..."表示对视频进行裁剪,格式为:宽度:高度:起始x坐标:起始y坐标。上述crop参数的意思是:宽度为视频宽度的1/3,高度为视频高度,起始x坐标为视频宽度1/3的位置,起始y坐标为0。
- 最后的”check_adapter.gif“则是输出的文件名。
还有许多参数,由于本次没有使用到,所以就不记录了,后面用到再更新。至于上述命令中,为什么是1/3,请看原视频的尺寸:

如果不对视频进行裁剪,那么生成的gif图片尺寸和原视频的一样,看起来不太方便。而我需要的部分大概在x轴1/3开始到2/3的位置。
生成gif时缩放操作:
```shell
-vf "scale=iw/3:ih/3"
```