React Native安卓原生下拉刷新 (兼容7.0版本以上)
起始
本文意在补充这些年React Native 安卓下拉刷新资料
本文将使用安卓下拉刷新组件库 SmartRefreshLayout,展示如何引入原生组件库到 React Native
里面进行封装
初始化项目
📖目录如下
├── assets
│ └── bouncing-fruits.json
├── components
│ ├── refresh-control (刷新组件)
│ │ └── PullToRefresh.android.tsx
│ ├── smart-refresh(下拉刷新组件)
│ └── ...
├── screens
│ └── Home.tsx
└── ...
创建一个列表
🍵我们使用 FlatList
作为本次的🌰
import React from 'react';
import {View, Text, StyleSheet, FlatList} from 'react-native';
type Item = {
title: string;
id?: string;
};
// item高度
const ITEM_HEIGHT: number = 70;
// 数据
let DATA = Array.from({length: 20}, (_, index) => {
return {
title: `Vue真好用👍-${index}`,
id: `${index}`,
};
});
// item
const Item = ({title}: Item) => {
return (
<View style={styles.itemWrapper}>
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
</View>
);
};
// 渲染item
const renderItem = ({item}: {item: Item}) => <Item title={item.title} />;
const Home: React.FC<{}> = () => {
return (
<View style={styles.wrapper}>
<FlatList
data={DATA}
renderItem={renderItem}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
keyExtractor={item => item.id}
/>
</View>
);
};
// styles
const styles = StyleSheet.create({
wrapper: {
flex: 1,
},
itemWrapper: {
padding: 10,
height: ITEM_HEIGHT,
},
item: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#5B8EFF',
borderRadius: 5,
},
title: {
color: '#ffffff',
fontSize: 16,
},
});
export default Home;
🧐预览
PullToRefreshAndroid
创建下拉组件 PullToRefreshAndroid
该组件是下拉刷新组件的最外层,里面包含 SmartRefresh
下拉刷新组件,Header
用户自定义的刷新头部组件。
🪵代码结构🌰如下
<SmartRefresh
renderHeader={
<Header
style={{
...styles.header,
height: HEADER_HEIGHT,
}}>
<LottieView
style={{
height: HEADER_HEIGHT,
}}
source={fruitsAnimation}
autoPlay
/>
</Header>
}
/>
🔧修改 Home.tsx
代码
- 引入
PullToRefreshAndroid
组件 FlatList
中添加refreshControl
自定义的下拉刷新组件- 我们的
PullToRefreshAndroid
组件需要获取两个方法 - 一个是
onRefresh
用于监听是否开始刷新,另外一个是handleFinishRefresh
用于设置刷新是否完成
....
import PullToRefreshAndroid from '../components/refresh-control/PullToRefresh.android';
...
...
const Home: React.FC<{}> = () => {
// 安卓下拉刷新组件
const pullToRefreshAndroid = useRef(null);
return (
<View style={styles.wrapper}>
<FlatList
data={DATA}
renderItem={renderItem}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
keyExtractor={item => item.id}
refreshControl={
<PullToRefreshAndroid
// onRefresh 必填因为需要监听是否需要刷新
onRefresh={() => {
console.log('开始刷新');
// 组件内部有一个 handleFinishRefresh 方法用于设置刷新完成
setTimeout(() => {
pullToRefreshAndroid.current?.handleFinishRefresh();
}, 1000);
}}
ref={pullToRefreshAndroid}
/>
}
/>
</View>
);
};
🥘编写 PullToRefreshAndroid.tsx
组件
首先我们先安装 lottie-react-native 动画库,这是一个 Lottie 的生态库,用于解析使用bodymovin导出为 JSON 的Adobe After Effects动画并在本地渲染它们
接下来我将使用该动画来制作下拉刷新的动画
✏️创建 PullToRefresh
函数
其中我们使用了 forwardRef
函数暴露 SmartRefresh
原生组件组件里面的方法 ,使最外层 Home.tsx
获取到 SmartRefresh
内部的方法函数
- 引入
SmartRefresh
安卓原生下拉刷新组件 - 引入
SmartRefreshHeader
安卓原生拉下组件的刷新头,具体请看编写下拉刷新头部代码 - 引入
LottieView
下拉刷新动画
const PullToRefresh = forwardRef((props: Props, forwardedRef: any) => {
return (
<SmartRefresh
ref={forwardedRef}
// 外部传入的事件监听器-> 用于监听下拉刷新组件是否刷新完成
onRefresh={props.onRefresh}
// 该 children 是 FlatList 组件
children={props.children}
// 该高度是必填项,因为我们需要设置下拉刷新头部的高度是多少
headerHeight={HEADER_HEIGHT}
// 定义刷新下拉刷新头部
renderHeader={
<SmartRefreshHeader
style={{
...styles.header,
height: HEADER_HEIGHT,
}}>
<LottieView
style={{
height: HEADER_HEIGHT,
}}
source={fruitsAnimation}
autoPlay
/>
</SmartRefreshHeader>
}
/>
);
});
✏️创建 SmartRefresh
下拉刷新组件
用于包裹 FlatList
组件和下拉刷新头部 SmartRefreshHeader
组件
import React, {forwardRef, memo} from 'react';
import {ScrollViewProps, View} from 'react-native';
type Props = {
children?: ScrollViewProps;
headerHeight: number;
renderHeader: object;
onRefresh: () => void;
};
const SmartRefresh = forwardRef((props: Props, forwardedRef: any) => {
return (
<View ref={forwardedRef}>
{/* 下拉头部 */}
{props.renderHeader}
{/* 滚动列表 */}
{props.children}
</View>
);
});
export default memo(SmartRefresh);
🧐预览
🍡完整代码
📃PullToRefresh.android.tsx
import React, {forwardRef, memo} from 'react';
import {StyleSheet} from 'react-native';
// 动画组件
import LottieView from 'lottie-react-native';
// 安卓原生下拉刷新
import SmartRefresh from '../smart-refresh/SmartRefresh';
// 安卓原生下拉刷新头部
import SmartRefreshHeader from '../smart-refresh/SmartRefreshHeader';
// types
import type {ScrollViewProps} from 'react-native';
type Props = {
children?: ScrollViewProps;
onRefresh: () => void;
};
// 下拉动画
const fruitsAnimation = require('../../assets/bouncing-fruits.json');
// 下拉刷新头部高度
const HEADER_HEIGHT = 70;
const PullToRefresh = forwardRef((props: Props, forwardedRef: any) => {
return (
<SmartRefresh
ref={forwardedRef}
// 外部传入的事件监听器-> 用于监听下拉刷新组件是否刷新完成
onRefresh={props.onRefresh}
// FlatList 滚动列表
children={props.children}
// 该高度是必填项,因为我们需要设置下拉刷新头部的高度是多少
headerHeight={HEADER_HEIGHT}
// 渲染下拉刷新头部的方法
renderHeader={
// 安卓原生下拉刷新头部 -> 请看编写下拉刷新头部的代码
<SmartRefreshHeader
style={{
...styles.header,
height: HEADER_HEIGHT,
}}>
<LottieView
style={{
height: HEADER_HEIGHT,
}}
source={fruitsAnimation}
autoPlay
/>
</SmartRefreshHeader>
}
/>
);
});
const styles = StyleSheet.create({
header: {
height: 100,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#5A66F1',
},
});
export default memo(PullToRefresh);
📃SmartRefresh.tsx
import React, {forwardRef, memo} from 'react';
import {ScrollViewProps, View} from 'react-native';
type Props = {
children?: ScrollViewProps;
headerHeight: number;
renderHeader: object;
onRefresh: () => void;
};
const SmartRefresh = forwardRef((props: Props, forwardedRef: any) => {
return (
<View ref={forwardedRef}>
{/* 下拉头部 */}
{props.renderHeader}
{/* 滚动列表 */}
{props.children}
</View>
);
});
export default memo(SmartRefresh);
引入SmartRefreshHeader
原生组件
🥘编写下拉刷新头部
- 创建完上面的组件之后让我们一起看一下下拉刷新头部如何编写
- 下面我们引入了原生的安卓组件,所以现在我需要开始编写我们的下拉刷新头部原生组件
import React, {type PropsWithChildren} from 'react';
import {requireNativeComponent} from 'react-native';
// 引入安卓原生下拉头部
export const SmartRefreshHeader = requireNativeComponent('SmartRefreshHeader');
const _SmartRefreshHeader: React.FC<
PropsWithChildren<{
style: object;
}>
> = props => {
return <SmartRefreshHeader {...props} />;
};
export default _SmartRefreshHeader;
🥘编写安卓原生下拉刷新头部组件 SmartRefreshHeader
🚗前往android/build.gradle
文件
由于我们引用的是 SmartRefreshLayout 安卓组件库, 所以需要先添加引用
dependencies
中添加implementation 'io.github.scwang90:refresh-layout-kernel:2.0.5'
下拉刷新 核心必须依赖implementation 'io.github.scwang90:refresh-header-classics:2.0.5'
ClassicsHeader 按需引入经典标头效果,可以不引入
经典标头效果
代码如下
dependencies {
....
// 下拉刷新 核心必须依赖
implementation 'io.github.scwang90:refresh-layout-kernel:2.0.5'
// 经典标头效果
implementation 'io.github.scwang90:refresh-header-classics:2.0.5'
....
}
🚗前往android/app/src/main/java/com/<your name>/
目录
创建目录文件夹
├── smart-refresh-header
│ ├── SmartRefreshHeader.java (下拉组件头部)
│ └── SmartRefreshHeaderManager.java (下拉组件头部入口文件)
├── smart-refresh-layout
│ ├── SmartRefreshLayoutPackage.java (下拉组件包入口)
│ └── SpinnerStyleConstants.java (变换方式常量)
└── ...
✏️编写 SmartRefreshHeader.java
下拉组件头部
- 在
smart-refresh-header
目录下添加 - 引入组件库的
RefreshHeader
api,对其进行自定义修改 - 关于自定义头部的参考可以看这里
// 此处请修改为你的包名称
package com.smartrefreshlayout;
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import com.facebook.react.views.view.ReactViewGroup;
// 下拉刷新库
import com.scwang.smart.refresh.layout.api.RefreshHeader;
import com.scwang.smart.refresh.layout.api.RefreshKernel;
import com.scwang.smart.refresh.layout.api.RefreshLayout;
import com.scwang.smart.refresh.layout.constant.RefreshState;
import com.scwang.smart.refresh.layout.constant.SpinnerStyle;
import com.scwang.smart.refresh.layout.util.SmartUtil;
public class SmartRefreshHeader
extends ReactViewGroup
implements RefreshHeader {
// RefreshKernel 核心接口(用于完成高级Header功能)
private RefreshKernel mRefreshKernel;
// 背景颜色
private int mBackgroundColor;
// 主题颜色
private Integer mPrimaryColor;
// 下拉刷新头部
protected RefreshHeader mRefreshHeader;
// 获取变换方式(必须指定一个:平移、拉伸、固定、全屏)-> 默认指定为平移即可
private SpinnerStyle mSpinnerStyle = SpinnerStyle.Translate;
public SmartRefreshHeader(Context context) {
super(context);
initView(context);
}
/**
* 尺寸定义初始化完成 (如果高度不改变(代码修改:setHeader),只调用一次, 在RefreshLayout#onMeasure中调用)
* @param kernel RefreshKernel 核心接口(用于完成高级Header功能)
* @param height HeaderHeight or FooterHeight
* @param maxDragHeight 最大拖动高度
*/
// @Override
public void onInitialized(
@NonNull RefreshKernel kernel,
int height,
int extendHeight
) {
mRefreshKernel = kernel;
/**
* 指定在下拉时候为 Header 或 Footer 绘制背景
* @param internal Header Footer 调用时传 this
* @param backgroundColor 背景颜色
* @return RefreshKernel
*/
mRefreshKernel.requestDrawBackgroundFor(this, mBackgroundColor);
}
// 此处最好和外部 react native 设置的头部 headerHeight
private void initView(Context context) {
// 设置最小高度
setMinimumHeight(30);
}
// 设置视图
public void setView(View v) {
addView(v);
}
/**
* 获取真实视图(必须返回,不能为null)
*/
@NonNull
public View getView() {
return this;
}
/**
* 获取变换方式(必须指定一个:平移、拉伸、固定、全屏)
*/
@Override
public SpinnerStyle getSpinnerStyle() {
return this.mSpinnerStyle;
}
/**
* 设置主题颜色 (如果自定义的Header没有颜色,本方法可以什么都不处理)
* @param colors 对应Xml中配置的 srlPrimaryColor srlAccentColor
*/
@Override
public void setPrimaryColors(@ColorInt int... colors) {}
/**
* 指定在下拉时候为 Header 或 Footer 绘制背景
* @param internal Header Footer 调用时传 this
* @param backgroundColor 背景颜色
* @return RefreshKernel
*/
public SmartRefreshHeader setPrimaryColor(@ColorInt int primaryColor) {
mBackgroundColor = mPrimaryColor = primaryColor;
if (mRefreshKernel != null) {
mRefreshKernel.requestDrawBackgroundFor(this, mPrimaryColor);
}
return this;
}
/**
* 获取变换方式(必须指定一个:平移、拉伸、固定、全屏)
*
* @param style
* @return
*/
public SmartRefreshHeader setSpinnerStyle(SpinnerStyle style) {
this.mSpinnerStyle = style;
return this;
}
/**
* onMoving
*
* @param isDragging
* @param percent
* @param offset
* @param height
* @param maxDragHeight
*/
@Override
public void onMoving(
boolean isDragging,
float percent,
int offset,
int height,
int maxDragHeight
) {
if(mRefreshHeader != null) {
mRefreshHeader.onMoving(isDragging, percent, offset, height, maxDragHeight);
}
}
/**
* 完成
*
* @param refreshLayout
* @param height
* @param extendHeight
*/
@Override
public void onReleased(
RefreshLayout refreshLayout,
int height,
int extendHeight
) {}
/**
* 开始动画(开始刷新或者开始加载动画)
* @param layout RefreshLayout
* @param height HeaderHeight or FooterHeight
* @param maxDragHeight 最大拖动高度
*/
@Override
public void onStartAnimator(
RefreshLayout layout,
int headHeight,
int maxDragHeight
) {}
/**
* 动画结束
* @param layout RefreshLayout
* @param success 数据是否成功刷新或加载
* @return 完成动画所需时间 如果返回 Integer.MAX_VALUE 将取消本次完成事件,继续保持原有状态
*/
@Override
public int onFinish(@NonNull RefreshLayout refreshLayout, boolean success) {
// 延迟500毫秒之后再弹回
return 500;
}
/**
* 是否支持水平拖动
*
* @return
*/
@Override
public boolean isSupportHorizontalDrag() {
return false;
}
/**
* 水平拖动
*
* @param percentX
* @param offsetX
* @param offsetMax
*/
@Override
public void onHorizontalDrag(float percentX, int offsetX, int offsetMax) {}
/**
* 手指拖动下拉(会连续多次调用,用于实时控制动画关键帧)
* @param percent 下拉的百分比 值 = offset/headerHeight (0 - percent - (headerHeight+maxDragHeight) / headerHeight )
* @param offset 下拉的像素偏移量 0 - offset - (headerHeight+maxDragHeight)
* @param headerHeight Header的高度
* @param maxDragHeight 最大拖动高度
*/
public void onPulling(
float percent,
int offset,
int headHeight,
int maxDragHeight
) {}
/**
* 手指释放之后的持续动画(会连续多次调用,用于实时控制动画关键帧)
* @param percent 下拉的百分比 值 = offset/headerHeight (0 - percent - (headerHeight+maxDragHeight) / headerHeight )
* @param offset 下拉的像素偏移量 0 - offset - (headerHeight+maxDragHeight)
* @param headerHeight Header的高度
* @param maxDragHeight 最大拖动高度
*/
public void onReleasing(
float percent,
int offset,
int headHeight,
int maxDragHeight
) {}
/**
* onRefreshReleased
*
* @param layout
* @param headerHeight
* @param maxDragHeight
*/
public void onRefreshReleased(
RefreshLayout layout,
int headerHeight,
int maxDragHeight
) {}
/**
* onStateChanged
*
* @param refreshLayout
* @param oldState
* @param newState
*/
@Override
public void onStateChanged(
RefreshLayout refreshLayout,
RefreshState oldState,
RefreshState newState
) {}
}
✏️编写 SmartRefreshHeader
的入口文件 SmartRefreshHeaderManager
- 在
smart-refresh-header
目录下添加 - 接下来我们还需要编写下拉刷新头部的入口文件暴露给
React Native
调用
// 此处请修改为你的包名称
package com.smartrefreshlayout;
import android.graphics.Color;
import androidx.annotation.ColorInt;
// 自定义头部
import com.smartrefreshlayout.SmartRefreshHeader;
// 变换方式样式常量
import com.smartrefreshlayout.SpinnerStyleConstants;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
// 下拉刷新库
import com.scwang.smart.refresh.layout.constant.SpinnerStyle;
import java.util.HashMap;
public class SmartRefreshHeaderManager
extends ViewGroupManager<SmartRefreshHeader> {
/**
* getName方法返回的名字会用于在 JavaScript 端引用
*
* @return
*/
@Override
public String getName() {
return "SmartRefreshHeader";
}
/**
* 视图在createViewInstance中创建, 且应当把自己初始化为默认的状态。所有属性的设置都通过后续的updateView来进行
*
* @param reactContext
* @return
*/
@Override
protected SmartRefreshHeader createViewInstance(
ThemedReactContext reactContext
) {
return new SmartRefreshHeader(reactContext);
}
/**
* 设置主调色
*
* @param view
* @param primaryColor
*/
@ReactProp(name = "primaryColor")
public void setPrimaryColor(SmartRefreshHeader view, String primaryColor) {
view.setPrimaryColor(Color.parseColor(primaryColor));
}
/**
* 获取变换方式(必须指定一个:平移、拉伸、固定、全屏)
*
* @param view
* @param spinnerStyle
*/
@ReactProp(name = "spinnerStyle")
public void setSpinnerStyle(SmartRefreshHeader view, String spinnerStyle) {
view.setSpinnerStyle(
SpinnerStyleConstants.SpinnerStyleMap.get(spinnerStyle)
);
}
}
✏️编写 SpinnerStyleConstants.java
用于判断变换方式常量文件
在 smart-refresh-layout
目录下添加
// 此处请修改为你的包名称
package com.smartrefreshlayout;
import com.scwang.smart.refresh.layout.constant.SpinnerStyle;
import java.util.HashMap;
public class SpinnerStyleConstants {
public static final String TRANSLATE = "translate";
public static final String FIX_BEHIND = "fixBehind";
public static final String SCALE = "scale";
public static final String FIX_FRONT = "fixFront";
public static final String MATCH_LAYOUT = "matchLayout";
public static final HashMap<String, SpinnerStyle> SpinnerStyleMap = new HashMap<String, SpinnerStyle>() {
{
put(TRANSLATE, SpinnerStyle.Translate);
put(FIX_BEHIND, SpinnerStyle.FixedBehind);
put(SCALE, SpinnerStyle.Scale);
put(MATCH_LAYOUT, SpinnerStyle.MatchLayout);
put(FIX_FRONT, SpinnerStyle.FixedFront);
}
};
}
✏️编写 SmartRefreshLayoutPackage.java
用于注册下拉刷新组件
在 smart-refresh-layout
目录下添加
// 此处请修改为你的包名称
package com.smartrefreshlayout;
// 自定义刷新下拉头部
import com.smartrefreshlayout.SmartRefreshHeaderManager;
// react
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class SmartRefreshLayoutPackage implements ReactPackage {
// UI Components 在此注册
@Override
public List<ViewManager> createViewManagers(
ReactApplicationContext reactContext
) {
return Arrays.<ViewManager>asList(
new SmartRefreshHeaderManager()
);
}
// Native Modules 在此注册
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext
) {
return Arrays.<NativeModule>asList();
}
}
🐖注册下拉刷新原生组件 SmartRefreshLayoutPackage
在 MainApplication.java
中添加
...
// 下拉刷新头部
import com.smartrefreshlayout.SmartRefreshLayoutPackage;
...
...
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
// 拉下刷新组件
packages.add(new SmartRefreshLayoutPackage());
return packages;
}
...
⚠注意:以上代码编写完后请重新编译
资料:ReactNative官网Android 原生UI组件
🧐预览
完成以上代码后的效果图😊
🔧编写外层📦 SmartRefreshLayout
组件
到这里我们已经基本完成了下拉刷新头部的创建,接下来我们还只需要一个外层的包裹📦组件,包裹住我们的下拉刷新头部和滚动列表 FlatList
即可
回到React Native
中一开始的 SmartRefresh.tsx
组件
const SmartRefresh = forwardRef((props: Props, forwardedRef: any) => {
return (
<View ref={forwardedRef}>
{/* 下拉头部 */}
{props.renderHeader}
{/* 滚动列表 */}
{props.children}
</View>
);
});
我们看上面的代码是不是还少了一个包裹住下拉头部和滚动列表的外层,现在我们就开始编写这个外层组件
SmartRefreshLayout
里面需要获取我们在外层组件PullToRefresh.android.tsx
里面传进来的值
所以一共有4个参数
type Props = {
// 滚动列表
children?: ScrollViewProps;
// 下拉刷新头部高度
headerHeight: number;
// 下拉刷新头部渲染函数
renderHeader: object;
// 刷新中回调
onRefresh: () => void;
};
引入原生外层组件
const SmartRefreshLayout = requireNativeComponent('SmartRefreshLayout');
此时SmartRefresh
函数代码
...
const SmartRefreshLayout = requireNativeComponent('SmartRefreshLayout');
...
<SmartRefreshLayout
// 下拉刷新头部高度
headerHeight={props.headerHeight}
// 刷新中
onSmartRefresh={() => {
props.onRefresh();
}}
>
{/* 下拉头部 */}
{props.renderHeader}
{/* 滚动列表 */}
{props.children}
</SmartRefreshLayout>
✏️SmartRefresh.tsx
代码
参数设置完成后完整代码如下:
import React, {forwardRef, memo, useRef, useEffect} from 'react';
import {
ScrollViewProps,
StyleSheet,
requireNativeComponent,
} from 'react-native';
type Props = {
// 滚动列表
children?: ScrollViewProps;
// 下拉刷新头部高度
headerHeight: number;
// 下拉刷新头部渲染函数
renderHeader: object;
// 刷新中回调
onRefresh: () => void;
};
// 引入原生外层组件
const SmartRefreshLayout = requireNativeComponent('SmartRefreshLayout');
const SmartRefresh = forwardRef((props: Props, forwardedRef: any) => {
// ref
const refreshLayout = useRef(null);
// mounted
useEffect(() => {
forwardedRef.current = {};
}, [forwardedRef]);
return (
<SmartRefreshLayout
style={styles.wrapper}
// 下拉刷新头部高度
headerHeight={props.headerHeight}
// 刷新中
onSmartRefresh={() => {
props.onRefresh();
}}
ref={refreshLayout}>
{/* 下拉头部 */}
{props.renderHeader}
{/* 滚动列表 */}
{props.children}
</SmartRefreshLayout>
);
});
const styles = StyleSheet.create({
wrapper: {
flex: 1,
},
});
export default memo(SmartRefresh);
🥘编写原生外层组件 SmartRefreshLayout
上面代码参数设置完成后接下来开始制作我们的原生外层组件SmartRefreshLayout
✏️编写 ReactSmartRefreshLayout.java
前往路径 android/app/src/main/java/com/smart-refresh-layout
下创建外层组件 ReactSmartRefreshLayout
// 此处请修改为你的包名称
package com.smartrefreshlayout;
import android.content.Context;
import android.view.MotionEvent;
import com.facebook.react.uimanager.events.NativeGestureUtil;
// 下拉刷新库
import com.scwang.smart.refresh.layout.SmartRefreshLayout;
import com.scwang.smart.refresh.layout.api.RefreshLayout;
import com.scwang.smart.refresh.layout.api.RefreshKernel;
public class ReactSmartRefreshLayout extends SmartRefreshLayout{
// 按下位置
private float mPrevTouchX = 0;
// 是否要拦截
private boolean mIntercepted = false;
protected RefreshKernel mKernel = new RefreshKernelImpl();
public ReactSmartRefreshLayout(Context context) {
super(context);
}
/**
* 布局 Header Footer Content
* 1.布局代码看起来相对简单,时因为测量的时候,已经做了复杂的计算,布局的时候,直接按照测量结果,布局就可以了
* @param changed 是否改变
* @param l 左
* @param t 上
* @param r 右
* @param b 下
*/
@Override
public void onLayout(
boolean changed,
int left,
int top,
int right,
int bottom
) {
super.onLayout(true, left, top, right, bottom);
}
/**
* 拦截触摸事件->拦截x轴移动
*
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (shouldInterceptTouchEvent(ev) && super.onInterceptTouchEvent(ev)) {
// 通知原生手势开始
NativeGestureUtil.notifyNativeGestureStarted(this, ev);
return true;
}
return false;
}
/**
* 应该拦截触摸事件
*
* @param ev
* @return
*/
private boolean shouldInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
// 手指按下
case MotionEvent.ACTION_DOWN:
// 按下位置
mPrevTouchX = ev.getX();
mIntercepted = false;
break;
// 手指移动
case MotionEvent.ACTION_MOVE:
// x轴移动位置
final float eventX = ev.getX();
// 拦截x轴移动
final float xDiff = Math.abs(eventX - mPrevTouchX);
// 拦截
if (mIntercepted || xDiff > mTouchSlop) {
mIntercepted = true;
return false;
}
}
return true;
}
}
✏️编写入口文件 SmartRefreshLayoutManager.java
接下来编写我们的入口文件 SmartRefreshLayoutManager.java
引入 ReactSmartRefreshLayout
对其进行扩展
📃Events.java
由于我们需要对其进行扩展,所以还需要一个Events.java
文件用于设置我们有哪些事件可以触发
// 此处请修改为你的包名称
package com.smartrefreshlayout;
public enum Events {
// 刷新触发
REFRESH("onSmartRefresh"),
// 下拉触发
HEADER_MOVING("onHeaderMoving"),
// 下拉开始刷新
PULL_DOWN_TO_REFRESH("onPullDownToRefresh"),
// 释放刷新
RELEASE_TO_REFRESH("onReleaseToRefresh"),
// 释放时进行刷新
HEADER_RELEASED("onHeaderReleased");
private final String mName;
Events(final String name) {
mName = name;
}
@Override
public String toString() {
return mName;
}
}
📃SmartRefreshLayoutManager.java
上面事件编写完后,现在来编写我们的入口文件,入口文件里面包含了,对原生组件SmartRefreshLayout
的配置(即设置开启那些功能),还有对事件进行外部传出
// 此处请修改为你的包名称
package com.smartrefreshlayout;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import android.graphics.Color;
import android.util.Log;
import android.view.View;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.events.RCTEventEmitter;
// 下拉刷新库
import com.scwang.smart.refresh.header.ClassicsHeader;
import com.scwang.smart.refresh.layout.api.RefreshFooter;
import com.scwang.smart.refresh.layout.api.RefreshHeader;
import com.scwang.smart.refresh.layout.api.RefreshLayout;
import com.scwang.smart.refresh.layout.constant.RefreshState;
import com.scwang.smart.refresh.layout.listener.OnRefreshListener;
import com.scwang.smart.refresh.layout.simple.SimpleMultiListener;
import com.scwang.smart.refresh.layout.util.SmartUtil;
// 组件可以触发的事件
import com.smartrefreshlayout.Events;
// 外层组件
import com.smartrefreshlayout.ReactSmartRefreshLayout;
// 下拉刷新头部
import com.smartrefreshlayout.SmartRefreshHeader;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
public class SmartRefreshLayoutManager
extends ViewGroupManager<ReactSmartRefreshLayout> {
// 外层组件
private ReactSmartRefreshLayout smartRefreshLayout;
// 事件发射器
private RCTEventEmitter mEventEmitter;
private ThemedReactContext themedReactContext;
// 完成刷新并标记没有更多数据
private static final String COMMAND_FINISH_REFRESH_NAME = "finishRefresh";
// 事件id
private static final int COMMAND_FINISH_REFRESH_ID = 0;
private int getTargetId() {
return smartRefreshLayout.getId();
}
@Override
public String getName() {
return "SmartRefreshLayout";
}
@Override
protected ReactSmartRefreshLayout createViewInstance(
ThemedReactContext reactContext
) {
smartRefreshLayout = new ReactSmartRefreshLayout(reactContext);
// 禁止上拉加载
smartRefreshLayout.setEnableLoadMore(false);
// 最大显示下拉高度/Header标准高度
smartRefreshLayout.setHeaderMaxDragRate(1);
//触发刷新距离 与 HeaderHeight 的比率
smartRefreshLayout.setHeaderTriggerRate(0.5f);
// 是否启用越界回弹
smartRefreshLayout.setEnableOverScrollBounce(true);
// 是否启用越界拖动(仿苹果效果)
smartRefreshLayout.setEnableOverScrollDrag(true);
themedReactContext = reactContext;
// 注册事件发射器
mEventEmitter = reactContext.getJSModule(RCTEventEmitter.class);
return smartRefreshLayout;
}
/**
* 获取导出的自定义直接事件类型常量
*
* @return
*/
@Override
public Map getExportedCustomDirectEventTypeConstants() {
MapBuilder.Builder builder = MapBuilder.builder();
for (Events event : Events.values()) {
builder.put(
event.toString(),
MapBuilder.of("registrationName", event.toString())
);
}
return builder.build();
}
/**
* 获取命令映射
*
* @return
*/
@Nullable
@Override
public Map<String, Integer> getCommandsMap() {
return MapBuilder.of(
COMMAND_FINISH_REFRESH_NAME,
COMMAND_FINISH_REFRESH_ID
);
}
/**
* Set the ratio of the maximum height to drag header.
* 设置下拉最大高度和Header高度的比率(将会影响可以下拉的最大高度)
* @param rate ratio = (the maximum height to drag header)/(the height of header)
* 比率 = 下拉最大高度 / Header的高度
* @return RefreshLayout
*/
@ReactProp(name = "maxDragRate", defaultFloat = 2.0f)
public void setMaxDragRate(ReactSmartRefreshLayout view, float maxDragRate) {
view.setHeaderMaxDragRate(maxDragRate);
}
/**
* Set the damping effect.
* 显示拖动高度/真实拖动高度 比率(默认0.5,阻尼效果)
* @param rate ratio = (The drag height of the view)/(The actual drag height of the finger)
* 比率 = 视图拖动高度 / 手指拖动高度
* @return RefreshLayout
*/
@ReactProp(name = "dragRate", defaultFloat = 0.5f)
public void setDragRate(ReactSmartRefreshLayout view, float dragRate) {
view.setDragRate(dragRate);
}
/**
* Set whether to enable cross-border drag (imitation iphone effect).
* 设置是否启用越界拖动(仿苹果效果)
* @param enabled 是否启用
* @return RefreshLayout
*/
@ReactProp(name = "overScrollDrag", defaultBoolean = false)
public void setOverScrollDrag(
ReactSmartRefreshLayout view,
boolean overScrollDrag
) {
view.setEnableOverScrollDrag(overScrollDrag);
}
/**
* Set whether to enable cross-border rebound function.
* 设置是否启用越界回弹
* @param enabled 是否启用
* @return RefreshLayout
*/
@ReactProp(name = "overScrollBounce", defaultBoolean = false)
public void setOverScrollBounce(
ReactSmartRefreshLayout view,
boolean overScrollBounce
) {
view.setEnableOverScrollBounce(overScrollBounce);
}
/**
* Set whether to enable the pure scroll mode.
* 设置是否开启纯滚动模式
* @param enabled 是否启用
* @return RefreshLayout
*/
@ReactProp(name = "pureScroll", defaultBoolean = false)
public void setPureScroll(ReactSmartRefreshLayout view, boolean pureScroll) {
view.setEnablePureScrollMode(pureScroll);
}
/**
* Set theme color id (primaryColor and accentColor).
* 设置主题颜色
*
* @param primaryColorId ColorRes 主题颜色ID
* @return RefreshLayout
*/
@ReactProp(name = "primaryColor", defaultInt = Color.TRANSPARENT)
public void setPrimaryColor(ReactSmartRefreshLayout view, int primaryColor) {
view.setPrimaryColors(primaryColor);
}
/**
* Set the Header's height.
* 设置 Header 高度
*
* @param heightDp Density-independent Pixels 虚拟像素(px需要调用px2dp转换)
* @return RefreshLayout
*/
@ReactProp(name = "headerHeight")
public void setHeaderHeight(
ReactSmartRefreshLayout view,
float headerHeight
) {
if (headerHeight != 0.0f) {
view.setHeaderHeight(headerHeight);
}
}
/**
* 是否启用下拉刷新(默认启用)
*
* @param enabled 是否启用
* @return SmartRefreshLayout
*/
@ReactProp(name = "enableRefresh", defaultBoolean = true)
public void setEnableRefresh(
ReactSmartRefreshLayout view,
boolean enableRefresh
) {
view.setEnableRefresh(enableRefresh);
}
/**
* 是否启用自动刷新
*
* @param view
* @param autoRefresh
*/
@ReactProp(name = "autoRefresh", defaultBoolean = false)
public void setAutoRefresh(
ReactSmartRefreshLayout view,
ReadableMap autoRefresh
) {
boolean isAutoRefresh = false;
Integer time = null;
if (autoRefresh.hasKey("refresh")) {
isAutoRefresh = autoRefresh.getBoolean("refresh");
}
if (autoRefresh.hasKey("time")) {
time = autoRefresh.getInt("time");
}
/**
* Display refresh animation and trigger refresh event.
* 显示刷新动画并且触发刷新事件
* @return true or false, Status non-compliance will fail.
* 是否成功(状态不符合会失败)
*/
if (isAutoRefresh == true) {
if (time != null && time > 0) {
view.autoRefresh(time);
} else {
view.autoRefresh();
}
}
}
/**
* finish refresh.
* 完成刷新
*
* @param delayed 开始延时
* @return RefreshLayout
*/
@Override
public void receiveCommand(
ReactSmartRefreshLayout root,
int commandId,
@Nullable ReadableArray args
) {
switch (commandId) {
case COMMAND_FINISH_REFRESH_ID:
int delayed = args.getInt(0);
boolean success = args.getBoolean(1);
if (delayed >= 0) {
root.finishRefresh(delayed, success, null);
} else {
root.finishRefresh(success);
}
break;
default:
break;
}
}
/**
* Set the header of RefreshLayout.
* 设置指定的 Header
*
* @param header RefreshHeader 刷新头
* @return RefreshLayout
*/
@Override
public void addView(ReactSmartRefreshLayout view, View child, int index) {
switch (index) {
case 0:
RefreshHeader header;
if (child instanceof RefreshHeader) {
header = (RefreshHeader) child;
} else {
header = new SmartRefreshHeader(themedReactContext);
((SmartRefreshHeader) header).setView(child);
}
/**
* Set the header of RefreshLayout.
* 设置指定的 Header
* @param header RefreshHeader 刷新头
* @param width the width in px, can use MATCH_PARENT and WRAP_CONTENT.
* 宽度 可以使用 MATCH_PARENT, WRAP_CONTENT
* @param height the height in px, can use MATCH_PARENT and WRAP_CONTENT.
* 高度 可以使用 MATCH_PARENT, WRAP_CONTENT
* @return RefreshLayout
*/
view.setRefreshHeader(
header,
// 匹配父母
MATCH_PARENT,
// 匹配父母
MATCH_PARENT
);
break;
case 1:
/**
* Set the content of RefreshLayout(Suitable for non-XML pages, not suitable for replacing empty layouts)。
* 设置指定的 Content(适用于非XML页面,不适合用替换空布局)
* @param content View 内容视图
* @return RefreshLayout
*/
view.setRefreshContent(child);
break;
case 2:
break;
default:
break;
}
}
/**
* 添加视图
*
* @param parent
* @param views
*/
@Override
public void addViews(ReactSmartRefreshLayout parent, List<View> views) {
super.addViews(parent, views);
}
/**
* 添加事件发射器
*
* @param reactContext
* @param view
*/
@Override
protected void addEventEmitters(
ThemedReactContext reactContext,
final ReactSmartRefreshLayout view
) {
/**
* 必须设置OnRefreshListener,如果没有设置,
* 则会自动触发finishRefresh
*
* OnRefreshListener和OnSimpleMultiPurposeListener
* 中的onRefresh都会触发刷新,只需写一个即可
*/
view.setOnRefreshListener(
new OnRefreshListener() {
@Override
public void onRefresh(RefreshLayout refreshLayout) {}
}
);
// 事件监听
view.setOnMultiListener(
// 多功能监听器
new SimpleMultiListener() {
private int getTargetId() {
return view.getId();
}
// 头部移动
public void onHeaderMoving(
RefreshHeader header,
boolean isDragging,
float percent,
int offset,
int headerHeight,
int maxDragHeight
) {
WritableMap writableMap = Arguments.createMap();
// 百分比
writableMap.putDouble("percent", percent);
// 偏移位置
writableMap.putDouble("offset", SmartUtil.px2dp(offset));
// 头部高度
writableMap.putDouble("headerHeight", SmartUtil.px2dp(headerHeight));
// 下拉触发
mEventEmitter.receiveEvent(
getTargetId(),
Events.HEADER_MOVING.toString(),
writableMap
);
}
// 释放时进行刷新
@Override
public void onHeaderReleased(
RefreshHeader header,
int headerHeight,
int extendHeight
) {
WritableMap writableMap = Arguments.createMap();
// 头部高度
writableMap.putDouble("headerHeight", SmartUtil.px2dp(headerHeight));
// 释放时进行刷新
mEventEmitter.receiveEvent(
getTargetId(),
Events.HEADER_RELEASED.toString(),
writableMap
);
}
// 头部开始动画
@Override
public void onHeaderStartAnimator(
RefreshHeader header,
int headerHeight,
int extendHeight
) {}
// 头部结束
@Override
public void onHeaderFinish(RefreshHeader header, boolean success) {}
// 刷新触发
@Override
public void onRefresh(RefreshLayout refreshLayout) {
mEventEmitter.receiveEvent(
getTargetId(),
Events.REFRESH.toString(),
null
);
}
// 开始改变
@Override
public void onStateChanged(
RefreshLayout refreshLayout,
RefreshState oldState,
RefreshState newState
) {
switch (newState) {
// 下拉开始刷新
case None:
case PullDownToRefresh:
mEventEmitter.receiveEvent(
getTargetId(),
Events.PULL_DOWN_TO_REFRESH.toString(),
null
);
break;
case Refreshing:
break;
// 释放刷新
case ReleaseToRefresh:
mEventEmitter.receiveEvent(
getTargetId(),
Events.RELEASE_TO_REFRESH.toString(),
null
);
break;
}
}
}
);
}
}
✏️修改 SmartRefreshLayoutPackage.java
文件
接下来我们还需要去注册外层组件
// 此处请修改为你的包名称
package com.smartrefreshlayout;
// 自定义刷新下拉头部
import com.smartrefreshlayout.SmartRefreshHeaderManager;
// 外层组件
import com.smartrefreshlayout.SmartRefreshLayoutManager;
// react
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class SmartRefreshLayoutPackage implements ReactPackage {
// UI Components 在此注册
@Override
public List<ViewManager> createViewManagers(
ReactApplicationContext reactContext
) {
return Arrays.<ViewManager>asList(
// 外层组件
new SmartRefreshLayoutManager(),
// 自定义刷新下拉头部
new SmartRefreshHeaderManager()
);
}
// Native Modules 在此注册
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext
) {
return Arrays.<NativeModule>asList();
}
}
🧐预览
完成以上代码后的效果如下
🥘通过 finishRefresh
函数触发刷新结束
通过上面预览可以看到刷新后下拉刷新头部并没有消失,是因为我们还没有调用刷新结束的方法
刷新方法其实我们在刚才的 SmartRefreshLayoutManager.java
中里面已经编写完成了如下:
...
/**
* finish refresh.
* 完成刷新
*
* @param delayed 开始延时
* @return RefreshLayout
*/
@Override
public void receiveCommand(
ReactSmartRefreshLayout root,
int commandId,
@Nullable ReadableArray args
) {
switch (commandId) {
case COMMAND_FINISH_REFRESH_ID:
int delayed = args.getInt(0);
boolean success = args.getBoolean(1);
if (delayed >= 0) {
root.finishRefresh(delayed, success, null);
} else {
root.finishRefresh(success);
}
break;
default:
break;
}
}
...
然后我们需要在React Native
组件的 #### Home.tsx
里面调用 finishRefresh
方法来吧下拉刷新头部隐藏
📃Home.tsx
调用SmartRefresh.tsx
里面的handleFinishRefresh
方法隐藏头部
type PullToRefreshAndroid = {
handleFinishRefresh: () => void;
};
const Home: React.FC<{}> = () => {
// 安卓下拉刷新组件
const pullToRefreshAndroid = useRef<PullToRefreshAndroid>(null);
return (
<View style={styles.wrapper}>
<FlatList
...
refreshControl={
<PullToRefreshAndroid
onRefresh={() => {
setTimeout(() => {
pullToRefreshAndroid.current?.handleFinishRefresh();
}, 1000);
}}
ref={pullToRefreshAndroid}
/>
}
/>
</View>
);
};
然后在SmartRefresh.tsx
暴露 handleFinishRefresh
方法给父级组件
📃SmartRefresh.tsx
使用 UIManager.dispatchViewManagerCommand
获取原生外层组件 SmartRefreshLayout
内部方法
// 找到当前节点
const findNode = () => {
return findNodeHandle(refreshLayout.current);
};
// 触发事件
const dispatchCommand = (commandName: string, params: any) => {
UIManager.dispatchViewManagerCommand(
findNode(),
(UIManager.getViewManagerConfig
? UIManager.getViewManagerConfig('SmartRefreshLayout')
: UIManager.SmartRefreshLayout
).Commands[commandName],
params,
);
};
// 停止刷新
const handleFinishRefresh = () => {
// -1为马上刷新完成
// >0 为延迟刷新完成时间(毫秒)
dispatchCommand('finishRefresh', [-1, true]);
};
🍡完整代码
import React, {forwardRef, memo, useRef, useEffect} from 'react';
import {
ScrollViewProps,
StyleSheet,
requireNativeComponent,
findNodeHandle,
UIManager as NotTypedUIManager,
UIManagerStatic,
} from 'react-native';
type Props = {
// 滚动列表
children?: ScrollViewProps;
// 下拉刷新头部高度
headerHeight: number;
// 下拉刷新头部渲染函数
renderHeader: object;
// 刷新中回调
onRefresh: () => void;
};
interface CustomUIManager extends UIManagerStatic {
SmartRefreshLayout: any;
}
const UIManager = NotTypedUIManager as CustomUIManager;
// 引入原生外层组件
const SmartRefreshLayout: any = requireNativeComponent('SmartRefreshLayout');
const SmartRefresh = forwardRef((props: Props, forwardedRef: any) => {
// ref
const refreshLayout = useRef(null);
// 初始化forwardedRef
useEffect(() => {
forwardedRef.current = {
handleFinishRefresh,
};
});
// 找到当前节点
const findNode = () => {
return findNodeHandle(refreshLayout.current);
};
// 触发事件
const dispatchCommand = (commandName: string, params: any) => {
UIManager.dispatchViewManagerCommand(
findNode(),
(UIManager.getViewManagerConfig
? UIManager.getViewManagerConfig('SmartRefreshLayout')
: UIManager.SmartRefreshLayout
).Commands[commandName],
params,
);
};
// 停止刷新
const handleFinishRefresh = () => {
// -1为马上刷新完成
// >0 为延迟刷新完成时间(毫秒)
dispatchCommand('finishRefresh', [-1, true]);
};
return (
<SmartRefreshLayout
style={styles.wrapper}
// 下拉刷新头部高度
headerHeight={props.headerHeight}
// 刷新中
onSmartRefresh={() => {
props.onRefresh();
}}
ref={refreshLayout}>
{/* 下拉头部 */}
{props.renderHeader}
{/* 滚动列表 */}
{props.children}
</SmartRefreshLayout>
);
});
const styles = StyleSheet.create({
wrapper: {
flex: 1,
},
});
export default memo(SmartRefresh);
🧐最终预览
以下效果图就是完成以上代码之后的效果🚪
- 各位小伙伴有没有很赞👍呢
- 如果觉得很👍的话,请在评论区打出你的👍
❌运行时各种问题
yarn ios 时错误
can't find gem cocoapods (>= 0.a) with executable pod
rvm 安装 ruby 3.1.2版本
rvm install ruby-3.1.2
重新运行
pod install
错误:Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP
运行 export HOMEBREW_NO_INSTALL_CLEANUP=TRUE
源码地址
参考文献:
转载自:https://juejin.cn/post/7167359594736402469