表达式(Expression)
表达式,是由数字、运算符、变量等以能求得数值的有意义排列方法所得的组合。譬如, x\*3+10
就是一个表达式,当x被赋值时,整个表达式就会有一个明确的结果。通过表达式,我们就可以描述一个具体的交互行为,比如我们希望x从0变化到100时,透明度能从1变化到0.5,那么表达式可以描述为: f(alpha) = 1-(x/100)*0.5。
在 BindingX
中,我们实现了一个轻量的表达式解析引擎用于执行表达式,除了基本的四则运算外,还支持三元运算符、数学函数等高级语法,能够满足绝大部分的场景。要查看 BindingX
中支持的表达式语法,请参考文档: 《支持的表达式语法》。
事件类型(Event Type)
现在你已经知道了 BindingX
是通过表达式来描述交互行为的,那么表达式中的变量究竟是什么呢?
答案是”不同的事件类型拥有不同的表达式变量”!那什么是事件类型呢?在 BindingX
中,事件是指能够驱动表达式数值变化的数据产生者,比如”用户的手势”、”列表的滚动” 甚至是”陀螺仪感知到的方向变化”,每一种这样的事件都对应着唯一的事件类型,比如”手势”对应的事件类型就是 pan
,要查看 BindingX
支持的所有事件类型,请参考文档: 《支持的事件类型》。
另外还需要说明的是,每一种事件类型都对应着不同的表达式变量。比如,当事件类型为 pan
时,表达式变量就是x和y,分别代表手势过程中横向和纵向的偏移量。同样地,要查看每一种事件类型对应的表达式变量,请参考文档: 《支持的事件类型》。
属性变换(Transformation Properties)
表达式的执行结果最终会驱动UI变化,比如透明度、位移、背景色等, 属性变换
就是用来描述这些属性的。在 BindingX
中,支持常用的transform属性变换,如translation
、scale
,另外还包括透明度、宽高等属性。要查看所有支持的属性变换,请参考文档:《支持的属性》
本文将向您介绍如何使用 BindingX
。如果您还不了解 BindingX
的工作原理,强烈建议先阅读文档 《简介》以及 《核心概念》。
前置条件
-
学习如何使用
weex
或者ReactNative
搭建页面; -
在您的Android或者iOS工程中,添加
BindingX
组件依赖,并注册组件。
第一步: 安装依赖
-
安装npm依赖
$ npm install weex-bindingx --save
-
在JS代码中引入BindingX模块
import BindingX from weex-bindingx;
第二步: 编写表达式
-
根据业务场景,选择您需要的EventType。 比如,要监听手势,evenType值为pan,监听滚动容器scrollOffset变化,eventType值为scroll。
-
根据交互行为,选择要改变的属性,并编写相应的表达式。比如,交互行为是”用户横滑100单位,透明度从1变化到0″。则属性为”opacity”,表达式为”1-x/100″。
第三步: 绑定表达式
根据第二步得到的eventType、Expression以及Property,调用 BindingX
模块的 bind
方法,完成绑定。
var result = BindingX.bind({
eventType: 'pan', ==> 事件类型
anchor: 'foo', ==> anchor指的是事件的触发者,如果是eventType是"orientation"或"timing",则不用填
props: [
{
element: view.ref, ==> 要改变的视图的引用或者id
expression: "1-x/100", ==> 表达式
property: "opacity" ==> 要改变的属性
}
]
})
当调用bind
方法之后,Native会启动监听,当目标事件(比如手指滑动、设备方向变化等)发生的时候,便会执行您先前绑定的一组或者多组表达式。 bind
方法会返回一个JS对象,其中包含了一个 token
属性,可以使用这个token取消绑定。
第四步: 取消绑定
在合适的时机调用 BindingX
的unbind方法取消绑定。比如,页面不可见或者即将销毁的时候。
BindingX.unbind({
token: result.token,
eventType: 'pan'
})
到这里,您已经学会基本的API使用了,更多内容可以参考其他教程。
手指移动事件监听
本文将向您介绍如何使用 BindingX
提供的手势能力来完成流畅的交互效果。如果您还不了解 BindingX
的工作原理,强烈建议先阅读文档 《简介》以及 《核心概念》。
特性介绍
BindingX
目前仅支持 pan
手势,您只需要在 bind
方法中将 eventType
的值设置为 pan
即可使用。 bindingX
提供了x和y两个预置变量,可以参与表达式运算,这两个变量的含义分别是手指移动过程中横向和纵向的偏移量,具体可以参考文档 《支持的事件类型》。
如何使用
下面通过一个简单的例子来介绍如何使用。我们的任务是实现一个”卡片侧滑删除”的效果。
注意: 我们的例子使用weex实现,上层DSL选用Rax。
第一步: 使用Rax编写卡片布局
Rax
提供了类似 ReactJS
的语法,如果您还不了解,可以移步他们的 官方网站 进行学习。
首先,您需要使用Rax-CLI工具创建一个新的Rax工程。然后就可以开始编写页面模板了。创建一个文件,命名为index.js,增加如下代码:
import {createElement, Component, render} from 'rax';
import Text from 'rax-text';
import View from 'rax-view';
class App extends Component {
render() {
}
}
render(<App />);
然后在当前文件夹下创建一个新文件,命名为index.css,用于编写样式。然后在index.js中引入该样式文件。
import {createElement, Component, render} from 'rax';
import Text from 'rax-text';
import View from 'rax-view';
import './index.css';
随后,我们在index.js中基于JSX语法编写卡片的布局,并在index.css中编写相关样式。
index.js
class App extends Component {
render() {
return (
<div className="container" >
<div className="border">
<div class="box">
<div className="head">
<div className="avatar"></div>
<text className="username">Foo</text>
</div>
<div className="content">
<text className="desc">Weex is a framework for building Mobile cross-platform UI. Rax is a universal JavaScript library with a largely React-compatible API.</text>
</div>
<div className="footer">
<text className="action">SHARE</text>
<text className="action" style={{color:'#7C4DFF'}}>EXPLORE</text>
</div>
</div>
</div>
</div>
);
}
}
index.css
.container {
flex: 1;
background-color:#eeeeee;
}
.border{
height:1000;
padding-left:35;
padding-right:35;
padding-top:100;
}
.box {
width: 680;
height: 450;
background-color:#651FFF;
}
.head {
background-color:#651FFF;
width:680;
height:120;
flex-direction:row;
align-items:center;
}
.content{
width:680;
height:240;
background-color:#651FFF;
padding-left:24;
padding-top:24;
padding-right:24;
box-sizing:border-box;
}
.footer {
width:680;
height:90;
background-color: #fff;
align-items:center;
justify-content:flex-end;
padding-right:25;
flex-direction:row;
box-sizing:border-box;
}
.action {
font-size:35;
padding-right:20;
}
.desc {
font-size:32;
color:#fff;
padding-left:24;
}
.avatar {
width:96;
height:96;
border-radius:48;
background-color:#CDDC39;
margin-left:36;
margin-right:48;
}
.username {
color:#fff;
font-size:32;
}
现在,我们的卡片布局已经编写完毕,您可以编译、预览效果。如果没有问题,就可以进行下一步了!
第二步: 引入BindingX组件
-
首先通过npm安装依赖。
$ npm install weex-bindingx --save
-
在index.js的头部引入
BindingX
组件。
import {createElement, Component, render} from 'rax';
import Text from 'rax-text';
import View from 'rax-view';
import BindingX from weex-bindingx;
import './index.css';
第三步: 注册手势事件
-
为卡片增加ref属性,用于唯一标识该组件,随后
BindingX
就可以基于ref找到该组件。render() { return ( <div className="container" > <div className="border"> <div ref='my' class="box" > <div className="head"> <div className="avatar"></div> <text className="username">Foo</text> </div> ... </div> </div> </div> ); }
-
为卡片注册
onTouchStart
事件,用于绑定表达式。onTouchStart = (event)=>{ } render() { return ( <div className="container" > <div className="border"> <div class="box" onTouchStart={this.onTouchStart}> <div className="head"> <div className="avatar"></div> <text className="username">Foo</text> </div> ... </div> </div> </div> ); }
第四步: 实现卡片拖拽效果
很显然,横滑的表达式应该是 x+0
,要改变的属性是 transform.translateX
。手势绑定的代码如下:
gesToken=0;
onTouchStart = (event)=>{
var my = this.refs.my;
var gesTokenObj = BindingX.bind({
anchor:my,
eventType:'pan',
props: [
{
element:my,
property:'transform.translateX',
expression:'x+0'
}
]
}, function(e) {
// nope
});
this.gesToken = gesTokenObj.token;
}
重新编译、执行代码,您应该就能横向拖拽卡片了!到这里,我们已经完成了大部分的效果,使用 BindingX
就是这么简单。
第五步: 增加透明度变化
现
在,我们在原来的基础上,再增加一个需求。手指在横滑的过程中,我们希望卡片的透明度也跟着变化。卡片的透明度对应的属性是opacity,值的范围是0
到1,因此,我们表达式的计算结果应该是0到1之间。在这个例子里,我们假设希望手指横向滑动600个单位,透明度变为0,那么表达式应该是 1-abs(x)/600
。
我们在props中再增加一段配置:
gesToken=0;
onTouchStart = (event)=>{
var my = this.refs.my;
var gesTokenObj = BindingX.bind({
anchor:my,
eventType:'pan',
props: [
{
element:my,
property:'transform.translateX',
expression:'x+0'
},
{
element:my,
property:'opacity',
expression:'1-abs(x)/600'
}
]
}, function(e) {
// nope
});
this.gesToken = gesTokenObj.token;
}
重新编译、执行,您应该就能看到期望的效果啦!
最后
回
顾这个例子,我们给卡片绑定了两个表达式,分别用于控制卡片的位移以及透明度,当手指在卡片上横向移动时,就会触发对应的表达式,进而改变卡片的位置以及
透明度。然而,这个例子仍然不完善,因为当我们的手指释放时,卡片也停住不动了,而我们希望卡片能够根据结束时的位置自动移除到屏幕外或者是回到原点。查
看下一篇教程 《动画》,我们来解决这个问题。
动画、时间轴事件监听
本文将向您介绍如何使用 BindingX
来实现动画。如果您还不了解 BindingX
的工作原理,强烈建议先阅读文档 《简介》以及 《核心概念》。
特性介绍
要在 BindingX
中使用动画,需要将eventType的值设置为 timing
。在timing模式下, BindingX
提供了一个预置变量叫 t
,可以参与表达式运算,它代表的含义是动画流逝的时间,从0开始,与手势不同的是, timing
中是不需要指定锚点(anchor)的。具体可以参考文档《支持的事件类型》。
如何使用
下面通过一个简单的例子来介绍如何使用。在教程 《手势》中,我们实现了通过手势横滑卡片的效果。但是当手指离开屏幕时,卡片并不能移除或者复位。这篇教程将解决这个问题。下面的动图是最终的效果。
注意: 我们的例子使用weex实现,上层DSL选用Rax。
第一步: 项目准备
-
使用
Rax
编写卡片布局; -
安装
BindingX
组件并引入; -
给卡片注册onTouchStart事件;
-
在onTouchStart中通过
BindingX
绑定表达式,实现拖拽卡片和更新透明度的效果。
-
以上步骤在教程 《手势》中已经介绍过了,这里不再展开。
第二步: 记录状态
现在我们希望在手势结束时根据位置判断卡片是复位还是删除。那么,首先我们需要监听手势结束的事件,然后记录卡片的透明度以及位置。代码如下:
gesToken=0;
x = 0; // 卡片偏移量
isInAnimation = false; // 卡片是否在动画当中
opacity = 1; // 卡片透明度
onTouchStart = (event)=>{
var my = this.refs.my;
var self = this;
var gesTokenObj = bindingx.bind({
anchor:my,
eventType:'pan',
props: [
{
element:my,
property:'transform.translateX',
expression:'x+0'
}
]
}, function(e) {
if(e.state === 'end') {
self.x = e.deltaX;
self.opacity = 1-Math.abs(e.deltaX)/600;
}
});
this.gesToken = gesTokenObj.token;
}
同时,在手势结束的时候,我们需要绑定一个动画。因此,我们增加一个新的方法 bindTimeing
。代码如下:
if(e.state === 'end') {
self.x = e.deltaX;
self.opacity = 1-Math.abs(e.deltaX)/600;
self.bindTiming();
}
...
bindTiming = ()=>{
//TODO
}
第三步:使用表达式描述动画
回顾您之前编写动画的经历,我们在声明动画的时候,通常需要设置动画的时长(duration)、需要改变的属性(property)以及动画的插值器如fadeIn等。但是,在使用 BindingX
的时候,您必须要要通过”表达式”的形式来描述。事实上,一个匀速动画可以用以下表达式来表示:
start + (end-start)*(min(t,duration)/duration)
start和end分别是起始值和结束值,duration就是动画时长。举个例子,假设,你希望改变一个view的translateX
属性,让其在2s内从0变化到650。那么你可以这样写:
props: [
{
element:some_ref,
property:'transform.translateX',
expression: '0+(650-0)*min(t,2000)/2000'
}
]
以上是匀速动画,那么,如果我们希望实现一个非匀速动画应该怎么办呢? 答案依然是编写表达式。 前面提到,匀速动画的表达式是start+(end-start)\*(min(t, duration) / duration)
,那么在这个基础上增加插值器,就可以实现非匀速动画,而插值器实际上是一个函数,形如:
input = min(t, duration)/duration
f(interpolator) = f(input)
考虑到插值器算法比较通用,因此, BindingX
内置了30多种插值器供您使用! 您甚至还可以使用贝塞尔曲线来定制您的插值效果。具体使用可以参考文档 《插值器/缓动函数》。
现在我们希望在手势结束时根据位置判断卡片是复位还是删除。那么根据卡片的偏移量就可以进行判断了。有下面三种情况:
-
复位
-
向左移除
-
向右移除
同时,我们假设动画的duration是1000ms。向左和向右移除的动画插值器选择 easeOutExpo
,复位的动画插值器选择 easeOutElastic
。(如果不了解插值器的用法,请参考文档《插值器/缓动函数》)。
代码如下:
bindTiming = ()=> {
this.isInAnimation = true;
var my = this.refs.my;
var self = this;
//should equal with timing duration
var exit = "t>1000";
var changed_x;
var final_x;
var final_opacity;
var translate_x_expression;
var shouldDismiss = false;
if(self.x>=-750/2 && self.x<=750/2) { // 复位
shouldDismiss = false;
final_x = 0;
changed_x = 0-self.x;
final_opacity = 1;
translate_x_origin = "easeOutElastic(t,"+self.x+","+changed_x+",1000)";
} else if(self.x < -750/2) { // 向左移除
shouldDismiss = true;
final_x = -750;
changed_x = -750-self.x;
final_opacity = 0;
translate_x_origin = "easeOutExpo(t,"+self.x+","+changed_x+",1000)";
} else {//x > 750 向右移除
final_x = 750;
shouldDismiss = true;
changed_x = 750-self.x;
final_opacity = 0;
translate_x_origin = "easeOutExpo(t,"+self.x+","+changed_x+",1000)";
}
var changed_opacity = final_opacity - self.opacity;
var opacity_expression = "linear(t,"+self.opacity+","+changed_opacity+",1000)";
var result = bindingx.bind(
{
eventType:'timing',
exitExpression: exit,
props: [
{
element:my,
property:'transform.translateX',
expression: translate_x_expression
},
{
element:my,
property:'opacity',
expression:opacity_expression
}
]
},function(e){
if(e.state === 'end' || e.state === 'exit') {
// reset x
self.x = final_x;
self.isInAnimation = false;
if(shouldDismiss) {
//remove card from hierarchy
}
}
});
}
第四步: 效果完善
如果在动画执行过程中,应该禁止手势,防止状态发生错乱。因此,在onTouchStart方法中需要增加如下代码:
onTouchStart = (event)=>{
var self = this;
if(this.isInAnimation === true) {
if(this.gesToken != 0) {
bindingx.unbind({
eventType:'pan',
token:self.gesToken
})
this.gesToken = 0;
}
return;
}
...
}
最后
到这里,我们已经实现了”卡片侧滑删除”的效果。