React Native FlatList:Select 所有项目和 运行 'OnPress'

React Native FlatList : Select all Items and run 'OnPress'

我正在使用 react-native-bouncy-checkboxFlatlist

I have created an array object which has id, name, amount.

So far I have achieved:

  • User can select individual items from the Flatlist, and it will add the amount and display it as total amount.

  • User can also edit the amount they have selected using TextInput.


但是,我正在尝试创建“Select All”功能。

因此,当用户按下 'Select All' 或点击 'checkbox' 时,它应该:

  1. select FlatList
  2. 中的所有项目
  3. 加总金额
  4. 允许用户单独编辑所有 selected 数量
  5. 更新复选框以显示它是 selected。

到目前为止,我已尝试让所有 'checkbox' 显示当按下 'Select All' 文本或按下 'checkbox' 时(在 select所有文字)。

过去几个小时我一直在努力让它工作,但没能成功。因此,欢迎就此问题提供任何帮助。


下面提供的代码片段和应用程序屏幕截图:

代码示例:

import 'react-native-gesture-handler';
import React, { useState, useEffect, Component } from 'react';
import { StyleSheet, View, Text, TouchableOpacity, FlatList } from 'react-native';
import { Button, Divider, TextInput } from 'react-native-paper';
import BouncyCheckbox from 'react-native-bouncy-checkbox';
import TextInputMask from 'react-native-text-input-mask';

function AccountMultiplePayment({ navigation }) {
    const [apiData, setApiData] = useState([
        {
            id: 1,
            name: 'John',
            address: 'address 1',
            amount: '79.90',
        },
        {
            id: 2,
            name: 'Simon',
            address: 'address 2',
            amount: '35.50',
        },
        {
            id: 3,
            name: 'Tim',
            address: 'address 3',
            amount: '15.50',
        },
        {
            id: 4,
            name: 'Rob',
            address: 'address 4',
            amount: '33.33',
        },
        {
            id: 5,
            name: 'Sarah',
            address: 'address 5',
            amount: '77.77',
        },
    ])

    const [billPaymentAmount, setBillPaymentAmount] = useState({})
    const [selectedBill, setSelectedBill] = useState([]);
    const [totalPaymentAmount, setTotalPaymentAmount] = useState(0);

    const computeBillPaymentAmount = () => {
        let newBillPaymentAmount = {}

        apiData.forEach(({ id, amount }) => {
            newBillPaymentAmount[id] = amount
        })

        return newBillPaymentAmount
    }

    const computeTotalPaymentAmount = () => {
        let total = 0

        selectedBill.forEach(id => {
            total += parseFloat(billPaymentAmount[id])
        })

        // Prevent NaN issue, because once user delete amount will become empty string
        return total ? total : 0
    }

    useEffect(() => {
        setBillPaymentAmount(computeBillPaymentAmount())
    }, [apiData])

    useEffect(() => {
        setTotalPaymentAmount(computeTotalPaymentAmount())
    }, [selectedBill, billPaymentAmount])

    const [checked, setChecked] = useState(false);

    return (
        <>
            <View style={{flexDirection: 'row'}}>
                <TouchableOpacity 
                    style={{ alignItems: 'center', paddingVertical: 10 }}
                    onPress={() => setChecked(!checked)}
                >
                    <Text style={{ color: 'black', fontSize: 25 }}>Select All</Text>
                </TouchableOpacity>
                <BouncyCheckbox
                        isChecked={checked}
                        fillColor={'green'}
                        unfillColor={'#FFFFFF'}
                        onPress={() => {
                            setChecked(!checked)
                        }}
                    />
            </View>
            <FlatList
                data={apiData}
                renderItem={({ item }) => {
                    return (
                        <View style={{ flexDirection: 'row' }}>
                            <View style={[styles.subHeaderContainer, { flex: 1 }]}>
                                <Text
                                    style={[
                                        styles.defaultText,
                                        { fontWeight: 'bold', fontSize: 16 },
                                    ]}>
                                    {item.name}
                                </Text>
                                <Divider style={{ marginVertical: 5 }} />
                                <View style={{ flexDirection: 'row' }}>
                                    <Text
                                        style={[styles.defaultText, { fontWeight: 'bold', flex: 2 }]}>
                                        Total Payable Amount:
                                    </Text>
                                    <View style={{ flex: 1 }}>
                                        <TextInput
                                            value={billPaymentAmount[item.id]}
                                            onChangeText={value => setBillPaymentAmount({ ...billPaymentAmount, [item.id]: value })}
                                            keyboardType={'numeric'}
                                            mode={'outlined'}
                                            label={'RM'}
                                            dense={true}
                                            render={props =>
                                                <TextInputMask
                                                    {...props}
                                                    mask='[9990].[99]'
                                                />
                                            }
                                        />
                                    </View>
                                </View>
                            </View>
                            <BouncyCheckbox
                                isChecked={checked}
                                fillColor={'green'}
                                unfillColor={'#FFFFFF'}
                                onPress={() => {
                                    if (selectedBill.includes(item.id)) {
                                        setSelectedBill(selectedBill.filter(value => value !== item.id))
                                    } else {
                                        setSelectedBill([...selectedBill, item.id])
                                    }
                                }}
                            />
                        </View>
                    );
                }}
                keyExtractor={item => item.id}
                removeClippedSubviews={false}
            />
            {
                    <>
                        <View
                            style={{
                                paddingVertical: 10,
                                paddingHorizontal: 20,
                                flexDirection: 'row',
                                backgroundColor:'blue'
                            }}>
                            <Text
                                style={{ color: 'white', flex: 1, fontWeight: 'bold', fontSize: 18 }}>
                                Total Amount:{' '}
                            </Text>
                            <View>
                                <Text style={{ color: 'white', fontWeight: 'bold', fontSize: 24 }}>
                                    RM {totalPaymentAmount.toFixed(2)}
                                </Text>
                                {totalPaymentAmount <= 0 ? null : (
                                    <TouchableOpacity
                                        onPress={() => {
                                            //navigation.goBack();
                                            navigation.goBack();
                                            navigation.navigate('Account');
                                        }}>
                                        <Text>Reset</Text>
                                    </TouchableOpacity>
                                )}
                            </View>
                        </View>

                        <Button
                            mode={'contained'}
                            color={'limegreen'}
                            style={{
                                borderRadius: 5,
                                marginHorizontal: 20,
                                marginVertical: 10,
                                justifyContent: 'center',
                            }}
                            labelStyle={{ color: 'white', padding: 10 }}
                            uppercase={false}
                            onPress={() => { }}
                            disabled={totalPaymentAmount <= 0 ? true : false}>
                            <Text>Pay Bill</Text>
                        </Button>
                    </>
            }
        </>
    );
}
class Account extends Component {
    constructor(props) {
        super(props);
        this._isMounted = false;
        this.state = {

        };
    }

    render() {
        return (
            <>
                {<AccountMultiplePayment {...this.props} {...this.navigation} />}
            </>
        );
    }
}

export default Account;

const styles = StyleSheet.create({
    flex: {
        flex: 1,
    },
    headerTitle: {
        alignItems: 'center',
        borderBottomLeftRadius: 25,
        borderBottomRightRadius: 25,
        paddingHorizontal: 20,
        paddingVertical: 20,
    },
    subHeaderContainer: {
        paddingVertical: 10,
        paddingHorizontal: 20,
        backgroundColor: 'white',
        borderRadius: 10,
        elevation: 5,
        marginVertical: 5,
        marginHorizontal: 10,
    },
    subHeaderTitle: {
        color: 'white',
        fontWeight: 'bold',
        fontSize: 16,
        backgroundColor: '#2c1855',
        padding: 10,
        borderRadius: 10,
    },
    defaultText: {
        color: 'black',
    },
});

这是目前的样子。所有选中的项目都单独 selected:


这就是我想要实现的目标:

检查 documentation of react-native-bouncy-checkbox 后,以下内容很重要。

  1. isChecked确定默认的内部复选框状态。只评估一次。
  2. check 状态处理由库内部处理。
  3. 我们想自己处理这个问题,以便通过单个状态更改选中所有复选框。为此,我们需要将 disableBuiltInState 属性设置为 true “如果你想手动处理 isChecked 属性并禁用内置处理”。

因此,我建议采用以下工作流程。

  1. isChecked 创建一个状态数组,其中包含每个元素的布尔标志。
  2. 创建自定义 onPress-handler,它从 FlatList 获取索引作为参数。使用该索引,我们能够将状态数组中的正确布尔标志设置为 truefalse.
  3. 如果按下 Select All,我们的 onPress-处理程序会将我们状态数组的所有布尔标志设置为 true。这将导致我们的屏幕重新渲染,并且由于我们将 disableBuiltInState 属性设置为 true,react-native-bouncy-checkbox 将使用我们的自定义状态处理。

这是一个最小的工作示例。

// set inititial all to false, you might want some different initial state
  const [checks, setChecks] = useState(Array.from({ length: apiData.length }, () => false))

  const onCheck = React.useCallback(
    (index) => {
      let previous = [...checks]
      previous[index] = !previous[index]
      setChecks(previous)
    },
    [checks, setChecks]
  )

  const selectAll = React.useCallback(() => {
    let previous = [...checks]
    setChecks(previous.map(() => true))
  }, [checks, setChecks])

return (
    <View style={{ padding: 40, marginTop: 50 }}>
      <FlatList
        data={apiData}
        renderItem={({ item, index }) => (
          <View style={{ padding: 20 }}>
            <BouncyCheckbox
              isChecked={checks[index]}
              fillColor={"green"}
              unfillColor={"#FFFFFF"}
              onPress={() => onCheck(index)}
              disableBuiltInState={true}
            />
          </View>
        )}
        keyExtractor={(item) => "" + item.id}
        removeClippedSubviews={false}
      />
      <TouchableOpacity onPress={selectAll}>
        <Text style={{ fontSize: 20, color: "red" }}> Select All</Text>
      </TouchableOpacity>
    </View>
  )

看起来如下。

Select All 得到

由于您想计算附加值(每个选定项目的总应付金额),我建议只将 checks 状态添加到您已经实施的 useEffect。当 checks 状态改变时,这个 useEffect 将被调用。您可以计算 checks 中的布尔标志为真的所有字段,并设置输入字段的状态。

重构代码如下


import "react-native-gesture-handler";
import React, { useState, useEffect, Component } from "react";
import {
  StyleSheet,
  View,
  Text,
  TouchableOpacity,
  FlatList,
} from "react-native";
import { Button, Divider, TextInput } from "react-native-paper";
import BouncyCheckbox from "react-native-bouncy-checkbox";

function AccountMultiplePayment({ navigation }) {
  const [apiData, setApiData] = useState([
    {
      id: 1,
      name: "John",
      address: "address 1",
      amount: "79.90",
    },
    {
      id: 2,
      name: "Simon",
      address: "address 2",
      amount: "35.50",
    },
    {
      id: 3,
      name: "Tim",
      address: "address 3",
      amount: "15.50",
    },
    {
      id: 4,
      name: "Rob",
      address: "address 4",
      amount: "33.33",
    },
    {
      id: 5,
      name: "Sarah",
      address: "address 5",
      amount: "77.77",
    },
  ]);

  const [billPaymentAmount, setBillPaymentAmount] = useState({});
  const [selectedBill, setSelectedBill] = useState([]);
  const [totalPaymentAmount, setTotalPaymentAmount] = useState(0);

  const computeBillPaymentAmount = () => {
    let newBillPaymentAmount = {};

    apiData.forEach(({ id, amount }) => {
      newBillPaymentAmount[id] = amount;
    });

    return newBillPaymentAmount;
  };

  const computeTotalPaymentAmount = () => {
    let total = 0;

    selectedBill.forEach((id) => {
      total += parseFloat(billPaymentAmount[id]);
    });

    // Prevent NaN issue, because once user delete amount will become empty string
    return total ? total : 0;
  };

  useEffect(() => {
    setBillPaymentAmount(computeBillPaymentAmount());
  }, [selectedBill.length]);

  useEffect(() => {
    setTotalPaymentAmount(computeTotalPaymentAmount());
  }, [billPaymentAmount]);

  const selectAllBill = () => {
    if (selectedBill.length < apiData.length) {
      setSelectedBill([...new Set(apiData.map((item) => item.id))]);
    }

    if (selectedBill.length === apiData.length) {
      setSelectedBill([]);
    }
  };

  const isBillAdded = (id) => selectedBill.some((el) => el === id);

  const hasAllBillselected = apiData.length === selectedBill.length;

  return (
    <>
      <View style={{ flexDirection: "row" }}>
        <TouchableOpacity
          style={{ alignItems: "center", paddingVertical: 10 }}
          onPress={selectAllBill}
        >
          <Text style={{ color: "black", fontSize: 25 }}>Select All</Text>
        </TouchableOpacity>
        <BouncyCheckbox
          disableBuiltInState
          isChecked={hasAllBillselected}
          fillColor={"green"}
          unfillColor={"#FFFFFF"}
          onPress={selectAllBill}
        />
      </View>

      <FlatList
        data={apiData}
        renderItem={({ item }) => {
          return (
            <View style={{ flexDirection: "row" }}>
              <View style={[styles.subHeaderContainer, { flex: 1 }]}>
                <Text
                  style={[
                    styles.defaultText,
                    { fontWeight: "bold", fontSize: 16 },
                  ]}
                >
                  {item.name}
                </Text>
                <Divider style={{ marginVertical: 5 }} />
                <View style={{ flexDirection: "row" }}>
                  <Text
                    style={[
                      styles.defaultText,
                      { fontWeight: "bold", flex: 2 },
                    ]}
                  >
                    Total Payable Amount:
                  </Text>
                  <View style={{ flex: 1 }}>
                    <TextInput
                      value={billPaymentAmount[item.id]}
                      onChangeText={(value) =>
                        setBillPaymentAmount({
                          ...billPaymentAmount,
                          [item.id]: value,
                        })
                      }
                      keyboardType={"numeric"}
                      mode={"outlined"}
                      label={"RM"}
                      dense={true}
                    />
                  </View>
                </View>
              </View>
              <BouncyCheckbox
                disableBuiltInState
                isChecked={selectedBill.includes(item.id)}
                fillColor={"green"}
                unfillColor={"#FFFFFF"}
                onPress={() => {
                  if (selectedBill.includes(item.id)) {
                    setSelectedBill(
                      selectedBill.filter((value) => value !== item.id)
                    );
                  } else {
                    setSelectedBill([...new Set([...selectedBill, item.id])]);
                  }
                }}
              />
            </View>
          );
        }}
        keyExtractor={(item) => item.id}
        removeClippedSubviews={false}
      />
      {
        <>
          <View
            style={{
              paddingVertical: 10,
              paddingHorizontal: 20,
              flexDirection: "row",
              backgroundColor: "blue",
            }}
          >
            <Text
              style={{
                color: "white",
                flex: 1,
                fontWeight: "bold",
                fontSize: 18,
              }}
            >
              Total Amount:{" "}
            </Text>
            <View>
              <Text
                style={{ color: "white", fontWeight: "bold", fontSize: 24 }}
              >
                RM {totalPaymentAmount.toFixed(2)}
              </Text>
              {totalPaymentAmount <= 0 ? null : (
                <TouchableOpacity
                  onPress={() => {
                    //navigation.goBack();
                    navigation.goBack();
                    navigation.navigate("Account");
                  }}
                >
                  <Text>Reset</Text>
                </TouchableOpacity>
              )}
            </View>
          </View>

          <Button
            mode={"contained"}
            color={"limegreen"}
            style={{
              borderRadius: 5,
              marginHorizontal: 20,
              marginVertical: 10,
              justifyContent: "center",
            }}
            labelStyle={{ color: "white", padding: 10 }}
            uppercase={false}
            onPress={() => {}}
            disabled={totalPaymentAmount <= 0 ? true : false}
          >
            <Text>Pay Bill</Text>
          </Button>
        </>
      }
    </>
  );
}
class Account extends Component {
  constructor(props) {
    super(props);
    this._isMounted = false;
    this.state = {};
  }

  render() {
    return (
      <>{<AccountMultiplePayment {...this.props} {...this.navigation} />}</>
    );
  }
}

export default Account;

const styles = StyleSheet.create({
  flex: {
    flex: 1,
  },
  headerTitle: {
    alignItems: "center",
    borderBottomLeftRadius: 25,
    borderBottomRightRadius: 25,
    paddingHorizontal: 20,
    paddingVertical: 20,
  },
  subHeaderContainer: {
    paddingVertical: 10,
    paddingHorizontal: 20,
    backgroundColor: "white",
    borderRadius: 10,
    elevation: 5,
    marginVertical: 5,
    marginHorizontal: 10,
  },
  subHeaderTitle: {
    color: "white",
    fontWeight: "bold",
    fontSize: 16,
    backgroundColor: "#2c1855",
    padding: 10,
    borderRadius: 10,
  },
  defaultText: {
    color: "black",
  },
});


此处为工作示例 - https://snack.expo.dev/@emmbyiringiro/971a0c