Focus animation working

This commit is contained in:
Zhiyuan Zheng 2020-12-06 12:52:29 +01:00
parent b274aef31a
commit 0a86c2d627
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
9 changed files with 409 additions and 82 deletions

View File

@ -40,6 +40,7 @@
"react-native-safe-area-context": "3.1.4",
"react-native-screens": "~2.10.1",
"react-native-shimmer-placeholder": "^2.0.6",
"react-native-svg": "12.1.0",
"react-native-toast-message": "^1.3.4",
"react-native-webview": "10.7.0",
"react-navigation": "^4.4.3",

View File

@ -4,9 +4,7 @@ import {
Dimensions,
Modal,
PanResponder,
Pressable,
StyleSheet,
Text,
View
} from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'

View File

@ -56,8 +56,10 @@ const ComposeActions: React.FC<Props> = ({
size={24}
color={
postState.poll.active || postState.attachments.length >= 4
? theme.secondary
: theme.primary
? theme.disabled
: postState.attachments.length
? theme.primary
: theme.secondary
}
onPress={async () => {
if (!postState.poll.active && postState.attachments.length < 4) {
@ -70,8 +72,10 @@ const ComposeActions: React.FC<Props> = ({
size={24}
color={
postState.attachments.length || postState.attachmentUploadProgress
? theme.secondary
: theme.primary
? theme.disabled
: postState.poll.active
? theme.primary
: theme.secondary
}
onPress={() => {
if (
@ -91,7 +95,7 @@ const ComposeActions: React.FC<Props> = ({
<Feather
name={getVisibilityIcon()}
size={24}
color={theme.primary}
color={theme.secondary}
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
@ -120,7 +124,7 @@ const ComposeActions: React.FC<Props> = ({
<Feather
name='smile'
size={24}
color={postState.emoji.emojis?.length ? theme.primary : theme.secondary}
color={postState.emoji.emojis?.length ? theme.secondary : theme.disabled}
{...(postState.emoji.emojis && {
onPress: () => {
if (postState.emoji.active) {
@ -141,7 +145,7 @@ const ComposeActions: React.FC<Props> = ({
<Text
style={[
styles.count,
{ color: postState.text.count < 0 ? theme.error : theme.primary }
{ color: postState.text.count < 0 ? theme.error : theme.secondary }
]}
>
{postState.text.count}

View File

@ -1,15 +1,14 @@
import React, { Dispatch, useCallback } from 'react'
import {
ActivityIndicator,
FlatList,
Image,
StyleSheet,
View
} from 'react-native'
import { FlatList, Image, Pressable, StyleSheet, View } from 'react-native'
import { Feather } from '@expo/vector-icons'
import { PostAction, PostState } from '../Compose'
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
import { useNavigation } from '@react-navigation/native'
import ShimmerPlaceholder from 'react-native-shimmer-placeholder'
const DEFAULT_HEIGHT = 200
export interface Props {
postState: PostState
@ -17,57 +16,82 @@ export interface Props {
}
const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
const renderImage = useCallback(({ item, index }) => {
return (
<View key={index}>
<Image
style={[
styles.image,
{
width: (item.meta?.original?.aspect || 1) * 200
}
]}
source={{ uri: item!.preview_url }}
/>
<Feather
name='edit'
size={24}
color='white'
style={styles.buttonEdit}
/>
<Feather
name='trash-2'
size={24}
color='white'
style={styles.buttonRemove}
onPress={() =>
postDispatch({
type: 'attachments',
payload: postState.attachments.filter(e => e.id !== item.id)
})
}
/>
</View>
)
}, [])
const { theme } = useTheme()
const navigation = useNavigation()
const listFooter = useCallback(() => {
return postState.attachmentUploadProgress ? (
<View
style={{
width: postState.attachmentUploadProgress.aspect * 200,
height: 200,
flex: 1,
backgroundColor: 'gray',
marginLeft: StyleConstants.Spacing.Global.PagePadding,
marginTop: StyleConstants.Spacing.Global.PagePadding,
marginBottom: StyleConstants.Spacing.Global.PagePadding
}}
const imageActions = ({
type,
icon,
onPress
}: {
type: 'edit' | 'delete'
icon: string
onPress: () => void
}) => {
return (
<Pressable
style={[
styles.actions,
styles[type],
{ backgroundColor: theme.backgroundOverlay }
]}
onPress={onPress}
>
<ActivityIndicator />
</View>
) : null
}, [postState.attachmentUploadProgress])
<Feather
name={icon}
size={StyleConstants.Font.Size.L}
color={theme.primaryOverlay}
/>
</Pressable>
)
}
const renderImage = useCallback(
({
item,
index
}: {
item: Mastodon.Attachment & { local_url?: string }
index: number
}) => {
return (
<View key={index}>
<Image
style={[
styles.image,
{
width: (item.meta?.original?.aspect || 1) * DEFAULT_HEIGHT
}
]}
source={{
uri:
item.type === 'image'
? item.local_url || item.preview_url
: item.preview_url
}}
/>
{imageActions({
type: 'delete',
icon: 'x',
onPress: () =>
postDispatch({
type: 'attachments',
payload: postState.attachments.filter(e => e.id !== item.id)
})
})}
{imageActions({
type: 'edit',
icon: 'edit',
onPress: () =>
navigation.navigate('Screen-Shared-Compose-EditAttachment', {
attachment: item
})
})}
</View>
)
},
[]
)
return (
<View style={styles.base}>
@ -75,7 +99,18 @@ const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
horizontal
data={postState.attachments}
renderItem={renderImage}
ListFooterComponent={listFooter}
ListFooterComponent={
<ShimmerPlaceholder
style={styles.progressContainer}
visible={postState.attachmentUploadProgress === undefined}
width={
(postState.attachmentUploadProgress?.aspect || 16 / 9) *
DEFAULT_HEIGHT
}
height={200}
/>
}
showsHorizontalScrollIndicator={false}
/>
</View>
)
@ -86,7 +121,7 @@ const styles = StyleSheet.create({
flex: 1,
flexDirection: 'row',
marginRight: StyleConstants.Spacing.Global.PagePadding,
height: 200
height: DEFAULT_HEIGHT
},
image: {
flex: 1,
@ -94,15 +129,28 @@ const styles = StyleSheet.create({
marginTop: StyleConstants.Spacing.Global.PagePadding,
marginBottom: StyleConstants.Spacing.Global.PagePadding
},
buttonEdit: {
actions: {
position: 'absolute',
top: 0,
left: 0
padding: StyleConstants.Spacing.S * 1.5,
borderRadius: StyleConstants.Spacing.XL
},
buttonRemove: {
position: 'absolute',
top: 0,
right: 0
edit: {
bottom:
StyleConstants.Spacing.Global.PagePadding + StyleConstants.Spacing.S,
right: StyleConstants.Spacing.S
},
delete: {
top: StyleConstants.Spacing.Global.PagePadding + StyleConstants.Spacing.S,
right: StyleConstants.Spacing.S
},
progressContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
height: DEFAULT_HEIGHT,
marginLeft: StyleConstants.Spacing.Global.PagePadding,
marginTop: StyleConstants.Spacing.Global.PagePadding,
marginBottom: StyleConstants.Spacing.Global.PagePadding
}
})

View File

@ -0,0 +1,218 @@
import { useNavigation } from '@react-navigation/native'
import React, { useCallback, useMemo, useRef, useState } from 'react'
import {
Animated,
Dimensions,
Image,
KeyboardAvoidingView,
PanResponder,
ScrollView,
StyleSheet,
Text,
TextInput,
View
} from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import Svg, { Circle, G, Path } from 'react-native-svg'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { HeaderLeft, HeaderRight } from 'src/components/Header'
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
import { PanGestureHandler } from 'react-native-gesture-handler'
const Stack = createNativeStackNavigator()
export interface Props {
route: {
params: {
attachment: Mastodon.Attachment & { local_url: string }
}
}
}
const ComposeEditAttachment: React.FC<Props> = ({
route: {
params: { attachment }
}
}) => {
const navigation = useNavigation()
const { theme } = useTheme()
const [altText, setAltText] = useState<string | undefined>()
const imageFocus = useCallback(() => {
const imageDimensionis = {
width: Dimensions.get('screen').width,
height:
Dimensions.get('screen').width / attachment.meta?.original?.aspect!
}
const panFocus = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current
const panX = panFocus.x.interpolate({
inputRange: [-imageDimensionis.width / 2, imageDimensionis.width / 2],
outputRange: [-imageDimensionis.width / 2, imageDimensionis.width / 2],
extrapolate: 'clamp'
})
const panY = panFocus.y.interpolate({
inputRange: [-imageDimensionis.height / 2, imageDimensionis.height / 2],
outputRange: [-imageDimensionis.height / 2, imageDimensionis.height / 2],
extrapolate: 'clamp'
})
const handleGesture = Animated.event(
[{ nativeEvent: { translationX: panFocus.x, translationY: panFocus.y } }],
{ useNativeDriver: true }
)
const onHandlerStateChange = useCallback(() => {
panFocus.extractOffset()
}, [])
return (
<View style={{ overflow: 'hidden' }}>
<Image
style={{
width: imageDimensionis.width,
height: imageDimensionis.height
}}
source={{
uri: attachment.local_url || attachment.preview_url
}}
/>
<PanGestureHandler
onGestureEvent={handleGesture}
onHandlerStateChange={onHandlerStateChange}
>
<Animated.View
style={[
{
position: 'absolute',
top: -500 + imageDimensionis.height / 2,
left: -500 + imageDimensionis.width / 2,
transform: [{ translateX: panX }, { translateY: panY }]
}
]}
>
<Svg width='1000px' height='1000px' viewBox='0 0 1000 1000'>
<G>
<G id='Mask'>
<Path
d={
'M1000,0 L1000,1000 L0,1000 L0,0 L1000,0 Z M500,467 C481.774603,467 467,481.774603 467,500 C467,518.225397 481.774603,533 500,533 C518.225397,533 533,518.225397 533,500 C533,481.774603 518.225397,467 500,467 Z'
}
fillOpacity='0.35'
fill='#000000'
/>
<G transform='translate(467.000000, 467.000000)'>
<Circle
stroke='#FFFFFF'
strokeWidth='2'
cx='33'
cy='33'
r='33'
/>
<Circle fill='#FFFFFF' cx='33' cy='33' r='2' />
</G>
</G>
</G>
</Svg>
</Animated.View>
</PanGestureHandler>
</View>
)
}, [])
const altTextInput = useCallback(() => {
return (
<>
<Text style={[styles.altTextInputHeading, { color: theme.primary }]}>
</Text>
<TextInput
style={[styles.altTextInput, { borderColor: theme.border }]}
autoCapitalize='none'
autoCorrect={false}
maxLength={1500}
multiline
onChangeText={e => setAltText(e)}
placeholder={
'你可以为附件添加文字说明,以便更多人可以查看他们(包括视力障碍或视力受损人士)。\n\n优质的描述应该简洁明了但要准确地描述照片中的内容以便用户理解其含义。'
}
placeholderTextColor={theme.secondary}
scrollEnabled
/>
<Text style={[styles.altTextLength, { color: theme.secondary }]}>
{1500 - (altText?.length || 0)}
</Text>
</>
)
}, [altText?.length])
return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
<SafeAreaView style={{ flex: 1 }} edges={['right', 'bottom', 'left']}>
<Stack.Navigator>
<Stack.Screen
name='Screen-Shared-Compose-EditAttachment-Root'
options={{
title: '编辑附件',
headerLeft: () => (
<HeaderLeft text='取消' onPress={() => navigation.goBack()} />
),
headerRight: () => <HeaderRight text='应用' onPress={() => {}} />
}}
>
{() => {
switch (attachment.type) {
case 'image':
return (
<View style={{ flex: 1 }}>
{imageFocus()}
<Text
style={[
styles.imageFocusText,
{ color: theme.primary }
]}
>
</Text>
<View style={styles.editContainer}>{altTextInput()}</View>
</View>
)
}
return null
}}
</Stack.Screen>
</Stack.Navigator>
</SafeAreaView>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
editContainer: { padding: StyleConstants.Spacing.Global.PagePadding },
imageFocusText: {
fontSize: StyleConstants.Font.Size.M
},
altTextInputHeading: {
fontSize: StyleConstants.Font.Size.M,
fontWeight: StyleConstants.Font.Weight.Bold,
marginTop: StyleConstants.Spacing.XL
},
altTextInput: {
height: 200,
fontSize: StyleConstants.Font.Size.M,
marginTop: StyleConstants.Spacing.M,
marginBottom: StyleConstants.Spacing.S,
padding: StyleConstants.Spacing.Global.PagePadding,
paddingTop: StyleConstants.Spacing.S * 1.5,
borderWidth: StyleSheet.hairlineWidth
},
altTextLength: {
textAlign: 'right',
marginRight: StyleConstants.Spacing.S,
fontSize: StyleConstants.Font.Size.S,
marginBottom: StyleConstants.Spacing.M
}
})
export default ComposeEditAttachment

View File

@ -6,6 +6,7 @@ import {
ActivityIndicator,
FlatList,
Pressable,
ProgressViewIOS,
StyleSheet,
Text,
TextInput,
@ -112,6 +113,10 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
return (
<View style={styles.base}>
<ProgressViewIOS
progress={postState.attachmentUploadProgress?.progress || 0}
progressViewStyle='bar'
/>
<FlatList
ListHeaderComponent={
<ComposeTextInput

View File

@ -39,12 +39,13 @@ const uploadAttachment = async ({
})
}
})
.then(({ body }: { body: Mastodon.Attachment }) => {
.then(({ body }: { body: Mastodon.Attachment & { local_url: string } }) => {
postDispatch({
type: 'attachmentUploadProgress',
payload: undefined
})
if (body.id) {
body.local_url = result.uri
postDispatch({
type: 'attachments',
payload: postState.attachments.concat([body])

View File

@ -5,6 +5,7 @@ import ScreenSharedHashtag from 'src/screens/Shared/Hashtag'
import ScreenSharedToot from 'src/screens/Shared/Toot'
import ScreenSharedWebview from 'src/screens/Shared/Webview'
import Compose from 'src/screens/Shared/Compose'
import ComposeEditAttachment from './Compose/EditAttachment'
import ScreenSharedSearch from './Search'
import { useTranslation } from 'react-i18next'
@ -54,6 +55,14 @@ const sharedScreens = (Stack: any) => {
stackPresentation: 'fullScreenModal'
}}
/>,
<Stack.Screen
key='Screen-Shared-Compose-EditAttachment'
name='Screen-Shared-Compose-EditAttachment'
component={ComposeEditAttachment}
options={{
stackPresentation: 'fullScreenModal'
}}
/>,
<Stack.Screen
key='Screen-Shared-Search'
name='Screen-Shared-Search'

View File

@ -1875,6 +1875,11 @@ blueimp-md5@^2.10.0:
resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.18.0.tgz#1152be1335f0c6b3911ed9e36db54f3e6ac52935"
integrity sha512-vE52okJvzsVWhcgUHOv+69OG3Mdg151xyn41aVQN/5W5S+S43qZhxECtYLAEHMSFWX6Mv5IZrzj3T5+JqXfj5Q==
boolbase@^1.0.0, boolbase@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
bplist-creator@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/bplist-creator/-/bplist-creator-0.0.8.tgz#56b2a6e79e9aec3fc33bf831d09347d73794e79c"
@ -2320,6 +2325,29 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
shebang-command "^1.2.0"
which "^1.2.9"
css-select@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef"
integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==
dependencies:
boolbase "^1.0.0"
css-what "^3.2.1"
domutils "^1.7.0"
nth-check "^1.0.2"
css-tree@^1.0.0-alpha.39:
version "1.1.2"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.2.tgz#9ae393b5dafd7dae8a622475caec78d3d8fbd7b5"
integrity sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==
dependencies:
mdn-data "2.0.14"
source-map "^0.6.1"
css-what@^3.2.1:
version "3.4.2"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4"
integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==
csstype@^3.0.2:
version "3.0.5"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.5.tgz#7fdec6a28a67ae18647c51668a9ff95bb2fa7bb8"
@ -2455,7 +2483,7 @@ domhandler@^2.3.0:
dependencies:
domelementtype "1"
domutils@^1.5.1:
domutils@^1.5.1, domutils@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
@ -3886,11 +3914,6 @@ klaw@^1.0.0:
optionalDependencies:
graceful-fs "^4.1.9"
ky@^0.24.0:
version "0.24.0"
resolved "https://registry.yarnpkg.com/ky/-/ky-0.24.0.tgz#337e534a7f47c12476988eef3cb968daef318349"
integrity sha512-/vpuQguwV30jErrqLpoaU/YJAFALrUkqqWLILnSoBOj5/O/LKzro/pPNtxbLgY6m4w5XNM6YZ3v7/or8qLlFuw==
leven@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@ -3994,6 +4017,11 @@ md5-file@^3.2.3:
dependencies:
buffer-alloc "^1.1.0"
mdn-data@2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
merge-stream@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1"
@ -4541,6 +4569,13 @@ npm-run-path@^2.0.0:
dependencies:
path-key "^2.0.0"
nth-check@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
dependencies:
boolbase "~1.0.0"
nullthrows@^1.1.0, nullthrows@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
@ -5068,6 +5103,14 @@ react-native-shimmer-placeholder@^2.0.6:
resolved "https://registry.yarnpkg.com/react-native-shimmer-placeholder/-/react-native-shimmer-placeholder-2.0.6.tgz#a6626d955945edb1aa01f8863f3e039a738d53d1"
integrity sha512-eq0Jxi/j/WseijfSeNjoAsaz1164XUCDvKpG/+My+c5YeVMfjTnl9SwoVAIr9uOpuDXXia67j8xME+eFJZvBXw==
react-native-svg@12.1.0:
version "12.1.0"
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-12.1.0.tgz#acfe48c35cd5fca3d5fd767abae0560c36cfc03d"
integrity sha512-1g9qBRci7man8QsHoXn6tP3DhCDiypGgc6+AOWq+Sy+PmP6yiyf8VmvKuoqrPam/tf5x+ZaBT2KI0gl7bptZ7w==
dependencies:
css-select "^2.1.0"
css-tree "^1.0.0-alpha.39"
react-native-toast-message@^1.3.4:
version "1.3.6"
resolved "https://registry.yarnpkg.com/react-native-toast-message/-/react-native-toast-message-1.3.6.tgz#78f90f78bbd8c97ce987f5dabc99ecc4a2d5eaef"
@ -5655,7 +5698,7 @@ source-map@^0.5.0, source-map@^0.5.6:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
source-map@^0.6.0, source-map@~0.6.1:
source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==