Я использую ReactNative
с Expo
, и я заметил, что в Android 6 функция доступности TalkBack не работает должным образом. На первом экране он работает, но на следующих экранах он не работает (который также динамически загружает FlatList). Я проверил на последней версии Android 7, 8, и там функция TalkBack работает довольно хорошо на всех экранах.
Вот мой пример кода:
Package.json
{
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"eject": "expo eject",
"test": "node ./node_modules/jest/bin/jest.js --watchAll"
},
"jest": {
"preset": "jest-expo"
},
"dependencies": {
"@expo/samples": "2.1.1",
"expo": "^32.0.0",
"react": "16.5.0",
"react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz",
"react-native-cheerio": "^1.0.0-rc.4",
"react-native-xml2js": "^1.0.3",
"react-navigation": "^3.0.9"
},
"devDependencies": {
"babel-preset-expo": "^5.0.0",
"jest-expo": "^32.0.0"
},
"private": true
}
HomeScreen
import React from 'react';
import {
Image,
Platform,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View, FlatList, ToastAndroid
} from 'react-native';
import {NEWS_SOURCES} from '../libraries/loader';
export const Toast = (props) => {
if (props.visible) {
ToastAndroid.showWithGravityAndOffset(props.message, ToastAndroid.LONG, ToastAndroid.BOTTOM, 25, 50);
return null;
}
return null;
};
export default class HomeScreen extends React.Component {
static navigationOptions = {
header: null,
};
constructor(props){
super(props);
this._renderItem = this._renderItem.bind(this);
this._handleItemClick = this._handleItemClick.bind(this);
}
render() {
console.log('\nnavigation props: ', this.props.navigation);
return (
<ScrollView style={styles.container} accessibilityLiveRegion="polite">
<View style={{flex:1, flexDirection: 'column', marginTop:50}}>
<Text style={styles.app_name} accessibilityLabel={"Select 1 of "+NEWS_SOURCES.length+" News Channels, from the list - given on screen"}>News Assistant</Text>
</View>
<View style={{flex:5, flexDirection: 'column'}}>
<FlatList
style={{padding:8}}
data={NEWS_SOURCES}
renderItem={this._renderItem}
keyExtractor={this._keyExtractor}
/>
</View>
<Toast visible={true} message={'Welcome, to news assistant. We got '+NEWS_SOURCES.length+' news channels loaded for you. Tap to know their names. And double tap to read headlines, on next screen.'} />
</ScrollView>
);
}
_handleItemClick(source_index) {
var selectedSource = NEWS_SOURCES[source_index];
console.log('Channel Selected: '+source_index, selectedSource);
this.props.navigation.navigate('HeadlinesStack', {channel_data: NEWS_SOURCES[source_index]});
}
_renderItem(obj){
console.log(obj);
return (
<View key={obj.index} style={styles.row} >
<TouchableOpacity style={{}} onPress={()=>this._handleItemClick(obj.index)} accessible={true} accessibilityRole="button" accessibilityLabel={obj.item.id+'. '+obj.item.title}>
<Text style={[styles.lightTxt, styles.rowTxt]}>{obj.item.title}</Text>
</TouchableOpacity>
</View>
);
}
_keyExtractor = (item, index) => 'key'+item.id;
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
app_name:{
color: '#fff', textAlign: 'center', fontSize:18
},
lightTxt: {
color: '#fff'
},
rowTxt:{
padding:25
},
row: {
borderBottomWidth: 1, borderBottomColor: '#333', borderStyle: 'solid'
},
});
HeadlinesScreen.js
import React from 'react';
import { ScrollView, View, Text, FlatList, TouchableOpacity, Modal, Alert, StyleSheet, ActivityIndicator, Button, ToastAndroid, TouchableHighlight, UIManager, findNodeHandle } from 'react-native';
import {NEWS_SOURCES, fetch_newsByCountry, fetch_newsBySourceID, fetch_newsFullStory} from '../libraries/loader';
export const Toast=(props)=> {
if (props.visible) {
ToastAndroid.showWithGravityAndOffset(props.message, ToastAndroid.LONG, ToastAndroid.BOTTOM, 25, 50);
return null;
}
return null;
};
export default class HeadlinesScreen extends React.Component {
static navigationOptions= {
title: 'Headlines',
};
constructor(props){
super(props);
this.state = { channelSource: this.props.navigation.getParam('channel_data', 'none'), // default value 'none'
dataList: [], modalVisible: false, selectedItemId: null, selectedItem_desc: null };
this._renderItem = this._renderItem.bind(this);
this._handleItemClick = this._handleItemClick.bind(this);
this._displayItemDetails = this._displayItemDetails.bind(this);
this._setModalVisible = this._setModalVisible.bind(this);
this._onFetchComplete = this._onFetchComplete.bind(this);
this._forceTriggerSpeak = this._forceTriggerSpeak.bind(this);
}
render() {
console.log('\n\this.props.navigation:', this.props.navigation);
console.log('\n\nSelected Channel state.params Data: ', this.props.navigation.state.params);
console.log('\n\nSelected Channel param Data: ', this.props.navigation.getParam('channel_data'));
console.log('\n\nSelected Channel state Data: ', this.state.channelSource);
//return (<View></View>) ;
//console.log('\n\nRender: \n',this.state);
console.log('\n\nRender - modal state: \n',this.state.modalVisible);
return (
<ScrollView style={styles.container} accessibilityLiveRegion="polite" importantForAccessibility={this.state.modalVisible==false ? 'yes' : 'no-hide-descendants'}>
{(this.state.dataList.length ==0? <ActivityIndicator size="large" Accessible={true} accessibilityLabel="Loading news, please wait." /> :
this.state.dataList.map(this._renderItem)
)}
<Toast visible={this.state.dataList.length ==0 && this.state.modalVisible==false ? true: false} message="Loading news, please wait." />
<Toast visible={this.state.dataList.length > 0 && this.state.modalVisible==false ? true: false} message={this.state.dataList.length+' news loaded. Tap to read headline. And double tap to read details.'} />
<Toast visible={this.state.dataList.length > 0 && this.state.modalVisible==true ? true: false} message={'Loading full story, please wait.'} />
<Toast visible={this.state.dataList.length > 0 && this.state.modalVisible==true && this.state.selectedItem_desc != null? true: false} message={'Story loaded, please tap on screen to read. And scroll bottom of screen and hit Close button, to close this story.'} />
{(this.state.modalVisible ? this._displayItemDetails() : null)}
</ScrollView>
);
}
async componentDidMount(){
console.log("\n\n\n", this.props.navigation.getParam('channel_data'));
var newsList = await fetch_newsBySourceID(this.state.channelSource, this._onFetchComplete); // external api call
}
_onFetchComplete(payload, payloadType){ // payloadType: list | story
console.log('\n\n onFetchComplete list: \n', payload);
if(Array.isArray(payload) && payloadType == 'list'){
console.log('\n\n onFetchComplete list: \n'+payload.length);
this.setState(previousState => {
return { dataList: payload};
});
}
else{
this.setState(previousState => {
return { selectedItem_desc: payload};
});
}
}
_renderItem(obj, index){
return (
<View key={index} style={{ borderBottomWidth: 1, borderBottomColor: '#ccc', borderStyle: 'solid', padding:10}} >
<TouchableOpacity style={{flexDirection: 'row', }} onPress={()=>this._handleItemClick(index)} accessible={true} accessibilityLiveRegion="polite" accessibilityLabel={(index-1+2)+". "+obj.title} accessibilityHint="Double tap to read full news on next screen.">
<Text style={[styles.lightTxt, styles.rowTxt]} >{obj.title}</Text>
</TouchableOpacity>
</View>
);
}
async _handleItemClick(itemIndex){
console.log('showModal for item: '+itemIndex);
fetch_newsFullStory(this.state.dataList[itemIndex].story_url, this.state.channelSource, this._onFetchComplete);
}
_forceTriggerSpeak(){
UIManager.sendAccessibilityEvent( findNodeHandle(this), UIManager.AccessibilityEventTypes.typeViewClicked);
}
// usually item details in a Popup Modal View
_displayItemDetails(){ // this.state.dataList[this.state.selectedItemId].content
console.log('\n\nDisplay Modal:\n', this.state.modalVisible);
}
// usually used to hide Popup Modal
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 15,
backgroundColor: '#000',
},
lightTxt: {
color: '#fff'
},
rowTxt:{
padding:25
},
});