taro3 仿android原生的SwipeRefreshLayout组件
一、android原生效果图
二、代码
import React from 'react'
import classNames from 'classnames'
import styles from './index.module.scss'
import { CommonPage } from 'tq-pat-ui'
import { Canvas, ITouchEvent, ScrollView, View } from '@tarojs/components'
interface IProps { }
interface IState {
/**
* 列表顶部刷新标志位移Y
*/
changeY: number
/**
* 开启列表刷新返回时的动画
*/
openHeaderBack: boolean
/**
* 是否正在刷新
*/
isRefreshing: boolean
/**
* 隐藏刷新图标
*/
hiddenLoading: boolean
}
/**
* 不能达到的Y值
*/
const UNTOUCHEDY = -9999
export default class AATest extends React.Component<IProps, IState> {
/**
* 下拉刷新初始触摸点Y
*/
touchStartY = UNTOUCHEDY
/**
* 是否已滑动到顶部
*/
isTop = true
/**
* 是否正在进行下拉刷新动作
*/
isMovingRefresh = false
/**
* onReady的init
*/
init = false
constructor(props: IProps) {
super(props)
this.state = {
changeY: UNTOUCHEDY,
openHeaderBack: false,
isRefreshing: false,
hiddenLoading: false,
}
}
onReady() {
if (this.init) {
this.drawRefresh(0.9)
} else {
this.init = true
}
}
componentWillUnmount(){
clearInterval(this.refreshInterval)
this.refreshInterval = ''
}
ctx: any
devicePixelRatio = 1
canvasMaxX = 150
canvasMaxY = 150
color = '#1492FF' // '#333333'
arcX = this.canvasMaxX / 2 // 圆中心点X
arcY = this.canvasMaxY / 2 // 圆中心点Y
arcR = (this.canvasMaxX / 2) * 0.5 // 圆半径
startAngle = 1.5 * Math.PI // 开始角度,从圆顶部开始
/**
* 绘制refresh图片
*/
drawRefresh(ratio: number, hiddenArrow?: boolean) {
if (!this.ctx) {
this.ctx = Taro.createCanvasContext('canvas', this)
this.devicePixelRatio = this.getPixelRatio(this.ctx) // 设备像素比率,解决画图模糊(其实也没啥用)
this.ctx.scale(2 * this.devicePixelRatio, 1 * this.devicePixelRatio) // canvas长宽比例有问题,所以这里宽度放大为2倍,xy轴数值不变
this.ctx.translate(0.5, 0.5)
// canvas 左上角 (0, 0)
// 向左,向下为正方向
this.canvasMaxX = 150 / this.devicePixelRatio // canvasX轴最大150
this.canvasMaxY = 150 / this.devicePixelRatio // canvasY轴最大150
// this.color = '#333333'
this.arcX = this.canvasMaxX / 2 // 圆中心点X
this.arcY = this.canvasMaxY / 2 // 圆中心点Y
this.arcR = (this.canvasMaxX / 2) * 0.5 // 圆半径
}
this.ctx.clearRect(0, 0, 150, 150)
// 画圆
this.ctx.beginPath()
const endAngle = (1.5 + (2 - 0.4) * ratio) * Math.PI // 开始角度
this.ctx.arc(this.arcX, this.arcY, this.arcR, this.startAngle, endAngle, false)
this.ctx.setStrokeStyle(this.color)
this.ctx.setLineWidth(10 / this.devicePixelRatio)
this.ctx.setLineCap('round')
this.ctx.stroke()
if(hiddenArrow !== true){
// 画等腰三角形
this.ctx.beginPath()
const triangleHeight = (20 / this.devicePixelRatio) * ratio
const AX = this.arcX + (this.arcR + triangleHeight) * Math.cos(endAngle)
const AY = this.arcX + (this.arcR + triangleHeight) * Math.sin(endAngle)
const BX = this.arcX + (this.arcR - triangleHeight) * Math.cos(endAngle)
const BY = this.arcX + (this.arcR - triangleHeight) * Math.sin(endAngle)
const CX = AX - Math.sqrt(2) * triangleHeight * Math.cos(Math.PI / 4 - endAngle)
const CY = AY + Math.sqrt(2) * triangleHeight * Math.sin(Math.PI / 4 - endAngle)
this.ctx.moveTo(AX, AY)
this.ctx.lineTo(BX, BY)
this.ctx.lineTo(CX, CY)
this.ctx.fillStyle = this.color
this.ctx.fill()
}
this.ctx.draw()
}
/**
* 获取设备像素比率
*/
getPixelRatio = (context: any) => {
const backingStore = context.backingStorePixelRatio ||
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1
return (window.devicePixelRatio || 1) / backingStore
}
onScrollToUpper() {
this.isTop = true
}
onScrollToLower() {
}
onTouchStart(e: ITouchEvent) {
// this.touchStartY = e.touches[0].pageY
}
onTouchMove(e: ITouchEvent) {
let { openHeaderBack, isRefreshing } = this.state
if(isRefreshing){
e.preventDefault()
}
if(!openHeaderBack && !isRefreshing){
const currentTouchY = e.touches[0].pageY
let changeY = UNTOUCHEDY
if(this.touchStartY !== UNTOUCHEDY){
changeY = currentTouchY - this.touchStartY
}
if(changeY > 0){
e.preventDefault()
}
if (this.isTop && this.touchStartY === UNTOUCHEDY) {
this.touchStartY = currentTouchY
}
if(changeY !== UNTOUCHEDY){
this.isMovingRefresh = true
if (changeY > 150) {
changeY = 150
} else if (changeY < 0) {
changeY = 0
}
this.setState({
changeY
})
const ratio = changeY / 75 > 1 ? 1 : changeY / 75
this.drawRefresh(ratio)
}
}
}
onTouchEnd() {
let { changeY, openHeaderBack, isRefreshing } = this.state
if(!openHeaderBack && !isRefreshing){
if(changeY >= 75){
this.setState({
changeY: 75,
openHeaderBack: true,
})
this.drawRefresh(1, true)
setTimeout(() => {
this.setState({
isRefreshing: true,
}, this.refreshList.bind(this))
}, 0.3 * 1000)
}else{
this.setState({
changeY: 0,
openHeaderBack: true,
})
setTimeout(() => {
this.setState({
changeY: UNTOUCHEDY,
openHeaderBack: false,
})
}, 300)
}
this.touchStartY = UNTOUCHEDY
}
}
refreshInterval: any
intervalRatio = 1.00
isDecrease = true
refreshList() {
this.refreshInterval = setInterval(() => {
this.drawRefresh(this.intervalRatio, true)
if(this.isDecrease){
this.intervalRatio = Number((this.intervalRatio - 0.01).toFixed(2))
if(this.intervalRatio === 0){
this.isDecrease = false
}
}else{
this.intervalRatio = Number((this.intervalRatio + 0.01).toFixed(2))
if(this.intervalRatio === 1){
this.isDecrease = true
}
}
}, 10)
setTimeout(() => { // 模拟请求时间
this.setState({
hiddenLoading: true,
openHeaderBack: false,
}, () => {
setTimeout(() => {
this.setState({
changeY: UNTOUCHEDY,
hiddenLoading: false,
isRefreshing: false,
})
clearInterval(this.refreshInterval)
this.refreshInterval = ''
this.intervalRatio = 1.00
this.isDecrease = true
}, 0.3 * 1000)
})
}, 3 * 1000)
}
render() {
const dataList = [
'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
// 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
// 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
// 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
// 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
// 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
// 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
// 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
]
const { changeY, openHeaderBack, isRefreshing, hiddenLoading } = this.state
return (
<CommonPage>
<View
id='list'
className={styles['list']}
onTouchStart={this.onTouchStart.bind(this)}
onTouchMove={this.onTouchMove.bind(this)}
onTouchEnd={this.onTouchEnd.bind(this)}
>
<View
className={classNames({
[styles['refresh']]: true,
[styles['refresh-back']]: openHeaderBack,
[styles['refresh-hidden']]: hiddenLoading,
})}
style={{ transform: `translate(-50%, ${changeY}px)` }}
>
<View
className={classNames({
[styles['canvas']]: true,
[styles['canvas-loading']]: isRefreshing,
})}
>
<Canvas
canvasId={'canvas'}
className={classNames({
[styles['canvas']]: true,
[styles['canvas-can']]: changeY / 75 >= 1,
[styles['canvas-not']]: changeY / 75 < 1,
})}
style={(openHeaderBack || isRefreshing) ? {} : { transform: `rotate(${360 * (changeY / 150)}deg)` }}
/>
</View>
</View>
<ScrollView
id='list'
className={styles['list-scroll']}
scrollY={true}
upperThreshold={0}
lowerThreshold={200}
onScroll={() => this.isTop = false}
onScrollToUpper={this.onScrollToUpper.bind(this)}
onScrollToLower={this.onScrollToLower.bind(this)}
>
{dataList.map((item, itemIndex) => {
return (
<View
key={`tqpatlist-item-${itemIndex}`}
onClick={() => console.error('aaa')}
>
{itemIndex}
</View>
)
})}
{changeY}
{dataList.map((item, itemIndex) => {
return (
<View key={`tqpatlist-item-${itemIndex}`}>
{itemIndex}
</View>
)
})}
{dataList.map((item, itemIndex) => {
return (
<View key={`tqpatlist-item-${itemIndex}`}>
{itemIndex}
</View>
)
})}
{dataList.map((item, itemIndex) => {
return (
<View key={`tqpatlist-item-${itemIndex}`}>
{itemIndex}
</View>
)
})}
</ScrollView>
</View>
</CommonPage>
)
}
}
scss:
.list{
height: 100vh;
width: 100vw;
overflow: hidden;
position: relative;
&-scroll{
height: 100vh;
width: 100vw;
}
}
.refresh{
position: absolute;
bottom: 100%;
left: 50%;
transform: translate(-50%, 0%);
display: flex;
justify-content: center;
align-items: center;
height: 10vw;
width: 10vw;
border-radius: 50%;
background-color: white;
box-shadow: 0px 0px 8px 4px whitesmoke;
display: flex;
align-items: center;
justify-content: center;
&-back{
transition: transform 0.1s linear;
}
&-hidden{
animation: hid 0.3s linear;
animation-iteration-count: 1;
animation-fill-mode: forwards
}
}
@keyframes hid {
0% {
height: 10vw;
width: 10vw;
}
100% {
height: 0vw;
width: 0vw;
}
}
@keyframes loading {
0% {
transform: rotate(0deg)
}
100% {
transform: rotate(360deg)
}
}
.canvas{
height: 100%;
width: 100%;
transition: opacity 0.3s linear;
&-not{
opacity: 0.5;
}
&-can{
opacity: 1;
}
&-loading{
animation: loading 1s linear infinite;
}
// background-color: yellow;
// transform: rotate(40deg);
// border-radius: 50%;
// opacity: 0.5;
}
效果图:
三、问题
1、canvas画的一个下拉刷新显示太模糊,虽然对真机做了适配,但还是有些模糊;
2、onTouchMove在上拉过后又下拉的情况下非常卡顿,导致不流畅,好像是多个组件同时触发的原因,用了preventDefault()也不太管用;
3、为了与原生效果一样,做了多层setTImeOut,后续看看能不能简化;