您好,欢迎访问代理记账网站
  • 价格透明
  • 信息保密
  • 进度掌控
  • 售后无忧

taro3 仿android原生的SwipeRefreshLayout下拉刷新组件

taro3 仿android原生的SwipeRefreshLayout组件

    • 一、android原生效果图
    • 二、代码
    • 三、问题

一、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,后续看看能不能简化;


分享:

低价透明

统一报价,无隐形消费

金牌服务

一对一专属顾问7*24小时金牌服务

信息保密

个人信息安全有保障

售后无忧

服务出问题客服经理全程跟进