关于一次在react-native中使用svg制作边框动画的探索
围绕边框旋转?svg!
众所周知react-native
是前端猿们喜闻乐见的一门技术栈,因其高效的跨平台能力,以及低成本的技术迁移(相对react开发者而言),深受前端猿的喜爱,只需要你懂一点点原生开发知识和亿点点react开发知识就可以轻松玩转,所以看见这个需求的时候,首先想到的就是svg
!
有小伙伴可能会问为什么不用react自己art
类库呢?不要问,问就是不想用,react-native有art的类库可以用,但是相对svg,api有所不一样,包括库本身也还有一些api未实现,比如安卓端shape的fill属性:
/**
* Sets up {@link #mPaint} according to the props set on a shadow view. Returns {@code true}
* if the fill should be drawn, {@code false} if not.
*/
protected boolean setupFillPaint(Paint paint, float opacity) {
if (mBrushData != null && mBrushData.length > 0) {
paint.reset();
paint.setFlags(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.FILL);
int colorType = (int) mBrushData[0];
switch (colorType) {
case COLOR_TYPE_SOLID_COLOR:
paint.setARGB(
(int) (mBrushData.length > 4 ? mBrushData[4] * opacity * 255 : opacity * 255),
(int) (mBrushData[1] * 255),
(int) (mBrushData[2] * 255),
(int) (mBrushData[3] * 255));
break;
case COLOR_TYPE_LINEAR_GRADIENT:
// For mBrushData format refer to LinearGradient and insertColorStopsIntoArray functions in ReactNativeART.js
if (mBrushData.length < 5) {
FLog.w(ReactConstants.TAG,
"[ARTShapeShadowNode setupFillPaint] expects 5 elements, received "
+ mBrushData.length);
return false;
}
float gradientStartX = mBrushData[1] * mScale;
float gradientStartY = mBrushData[2] * mScale;
float gradientEndX = mBrushData[3] * mScale;
float gradientEndY = mBrushData[4] * mScale;
int stops = (mBrushData.length - 5) / 5;
int[] colors = null;
float[] positions = null;
if (stops > 0) {
colors = new int[stops];
positions = new float[stops];
for (int i=0; i<stops; i++) {
positions[i] = mBrushData[5 + 4*stops + i];
int r = (int) (255 * mBrushData[5 + 4*i + 0]);
int g = (int) (255 * mBrushData[5 + 4*i + 1]);
int b = (int) (255 * mBrushData[5 + 4*i + 2]);
int a = (int) (255 * mBrushData[5 + 4*i + 3]);
colors[i] = Color.argb(a, r, g, b);
}
}
paint.setShader(
new LinearGradient(
gradientStartX, gradientStartY,
gradientEndX, gradientEndY,
colors, positions,
Shader.TileMode.CLAMP
)
);
break;
case COLOR_TYPE_RADIAL_GRADIENT:
// TODO(6352048): Support radial gradient etc.
case COLOR_TYPE_PATTERN:
// TODO(6352048): Support patterns etc.
default:
FLog.w(ReactConstants.TAG, "ART: Color type " + colorType + " not supported!");
}
if (mShadowOpacity > 0) {
paint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mShadowColor);
}
return true;
}
return false;
}
未实现patterns填充底色,也就是用图像填充,看见这么多TODO你还敢用吗?而react-native-svg这个库兼容了web端svg的很多标签属性,用起来更舒服,不会有附加的学习负担,懒人当然更喜欢这个。
那么该从何下手呢?首先看看svg如何画边框!
既然兼容web标签属性,那么看看css怎么制作一个会动的标签吧。打开w3c的svg章节,翻到 SVG rect这一章节,rect属性包括:
- x:绘制起点x轴坐标
- y:绘制起点y轴坐标
- rx,ry:rx 和 ry 属性可使矩形产生圆角
- width,height:宽度和高度
- style:
- fill 属性定义矩形的填充颜色(rgb 值、颜色名或者十六进制值)
- stroke-width 属性定义矩形边框的宽度
- stroke 属性定义矩形边框的颜色
- stroke-dasharray 文档没写,用于画虚线 [虚线的长度,虚线之间的间隔长度]
- stroke-dashoffset 文档依然没写,用于设置 虚线起点的偏移
看了属性之后大概猜一下,估计就是用stroke属性来画边框了,stroke-width定义边框的宽度,
stroke-dasharray
定义边框的长度和间隔,stroke-dashoffset
定义起点的偏移,这样就能把边框给画出来了。
但是怎么才能让他转起来呢?动画!
那么怎么才能让固定长度的边框转起来呢,这里又是个小细节,我们利用stroke-dashoffset属性给边框添加线性变化的偏移,这样边框就能转起来了。
在css中实现svg标签的动画比较简单,只需要定义一个animation动画给stroke-dashoffset就可以了,那么在没有animation标签支持的react-native下,如何才能动画呢?答案是使用react-native内部提供的Animated方法,由于Rect属于三方组件,所以需要用Animated.createAnimatedComponent(Rect)
包装一下才能使用。
做到这里,已经大概知道需要怎么实现了,需要确定strokeDasharray虚线的长度和虚线的间隔,以及通过用动画算法计算出偏移量,从而实现动画,那么这里需要注意的是,如果要让动画循环播放,并且可以设置多久转满一圈,也就是duration,这个偏移量就不能随便设置,得算出矩形的周长,以周长为一个offset,算周长就得翻翻初中数学了:
init = () => {
// 计算周长
const { width, height } = this.state;
const { borderRadius } = this.props;
let rx = borderRadius;
let ry = borderRadius;
const maxBorderRadius = width < height ? width : height;
if (borderRadius <= 0) {
rx = 0;
ry = 0;
}
if (borderRadius >= maxBorderRadius / 2) {
rx = maxBorderRadius / 2;
ry = maxBorderRadius / 2;
}
this.rx = rx;
this.ry = ry;
const borderRadiusLength = Math.PI * rx * 2; // 圆角的周长
const lineLength = 2 * width + 2 * height - 8 * rx;
this.setState({
allLength: borderRadiusLength + lineLength,
});
};
然后自然而然的咱们就写出了这样的布局代码:
<Svg
viewBox={`0 0 ${containerWidth} ${containerHeight}`}
width={containerWidth}
height={containerHeight}
style={{
position: 'absolute',
zIndex: 9999,
elevation: 1,
}}
>
<AnimatedRect
ref={ref => (this._myRect = ref)}
x={borderWith}
y={borderWith}
rx={this.rx}
ry={this.ry}
width={width}
height={height}
fill="none"
stroke={color}
strokeWidth={borderWith}
strokeDasharray={[
borderLength,
allLength - borderLength,
]}
/>
</Svg>
解释一下为什么strokeDasharray的虚线间隔需要设置成周长减去边框长度,因为咱们需要循环动画,当一个duration跑完一个周长的offset后,需要回到初始值0继续跑下一个offset,这样咱们的间隔加上边框长度就得刚好等于一个周长才行,也可以继续跑下一个offset的倍数,方法很多,懂的自然懂,其中viewBox以及fill什么的感兴趣可以自己翻一翻文档看看。
最后看一下动画api的调用方式:
···
constructor(props) {
super(props);
this.state = {
rectDasharray: new Animated.Value(0),
};
// 监听值变化,并设置
this.state.rectDasharray.addListener(rectDasharray => {
this._myRect &&
this._myRect.setNativeProps({
strokeDashoffset: rectDasharray.value,
});
});
}
···
animate = () => {
// 循环动画
this.state.rectDasharray.setValue(0);
Animated.timing(this.state.rectDasharray, {
toValue:
(this.props.direction == 'right' ? -1 : 1) *
this.state.allLength,
duration: this.props.duration,
easing: Easing.linear,
useNativeDriver: true,
}).start(this.animate);
};
setNativeProps
可以想象你正在用原生js
的方式设置dom标签的style
属性,类比过来原生js就是你正在写的react-native
,dom就是Rect
标签,当然你也可以使用Animated.interpolate
差值计算的方式来设置值,因为我是懒人所以用了比较笨的方法,完事儿差不多就酱紫
,