프로젝트/할일목록 앱 (RN)

19. 화면 레이아웃 깨지는 문제 해결하기

syleemomo 2023. 10. 31. 17:29
728x90

* 할일목록이 비어있는 경우 키보드가 보여질때 이미지가 밀려올라가는 문제 해결하기

import React from 'react'
import { View, Text, Image, StyleSheet, KeyboardAvoidingView, Dimensions } from 'react-native'

function Default(){
    return (
        <KeyboardAvoidingView style={styles.container}>
            <Image source={require('../assets/imgs/todo.png')}/>
            <Text style={styles.guideText}>현재 할일목록이 비어있습니다.</Text>
        </KeyboardAvoidingView>
    )
}

const styles = StyleSheet.create({
    container: {
        position: 'absolute',
        left: 0,
        top: 0,
        justifyContent: 'center',
        alignItems: 'center',
        width: Dimensions.get('window').width,
        height: Dimensions.get('window').height,
        backgroundColor: '#fff'
    },
    guideText: {
        fontSize: 20,
        marginTop: 30
    }
}) 

export default Default

components > Default.js 파일을 위와 같이 수정한다. 

import { View, Text, Image, StyleSheet, KeyboardAvoidingView, Dimensions } from 'react-native'

KeyboardAvoidingView, Dimensions 를 추가로 임포트한다.

<KeyboardAvoidingView style={styles.container}>
    <Image source={require('../assets/imgs/todo.png')}/>
    <Text style={styles.guideText}>현재 할일목록이 비어있습니다.</Text>
</KeyboardAvoidingView>

View 컴포넌트를 KeyboardAvoidingView 컴포넌트로 변경한다.

container: {
    position: 'absolute',
    left: 0,
    top: 0,
    justifyContent: 'center',
    alignItems: 'center',
    width: Dimensions.get('window').width,
    height: Dimensions.get('window').height,
    backgroundColor: '#fff'
},

KeyboardAvoidingView 컴포넌트 스타일을 위와 같이 수정한다. 기존에는 flex: 1 을 설정하여 입력창을 제외한 나머지 영역을 모두 차지하도록 했는데 이렇게 하면 키보드가 나타나면서 입력창이 밀려올라가고, 그만큼 이미지가 보여지는 영역이 줄어들게 된다. 그러면서 이미지가 위로 밀려올라간다. position: 'absolute'로 설정하고, 전체화면을 다 차지하도록 하면 키보드가 나타날때 이미지는 고정된다. 

import React from 'react'
import { 
    View, 
    Text,
    TextInput, 
    TouchableOpacity, 
    StyleSheet, 
    Keyboard
} from 'react-native'

function TodoInsert({ onInsertTodo, todoText, setTodoText, warning, setWarning, disabled }){ // disabled props 추가함
  const onPress = () => {
    const trimedText = todoText.trim()
    onInsertTodo(trimedText)
  }
  const handleChange = (text) => { 
    if (/\n/.test(text)) { // 엔터키 입력시 
      console.log("제출")
      onPress() // 할일추가
    }else {
      console.log("엔터키 아님")
      setTodoText(text)
      setWarning(false)
    }
  }
  const hideKeyboard = (e) => {
    Keyboard.dismiss()
  }
  console.log(todoText)
  return (
    <View style={styles.container}> 
      <TextInput 
        editable={!disabled} // disabled 값에 따른 입력창 비활성화
        selectTextOnFocus={!disabled} // disabled 값에 따른 입력창 비활성화
        placeholder={disabled ? 'X 할일을 작성할 수 없습니다 X' : '할일을 작성해주세요!'} // disabled 값에 따른 안내문구 변경
        placeholderTextColor={disabled ? 'red': '#a8c8ffff'}  // disabled 값에 따른 안내문구 색상 변경 
        selectionColor={'#d6e3ffff'}  // 커서색상
        style={[styles.input, { color: warning ? 'red': '#a8c8ffff' } ]}
        value={disabled ? "" : todoText} // disabled 값이 true 인 경우 입력창 초기화
        blurOnSubmit={ false } // 탭키 누를때 키보드 사라지지 않게 하기
        onChangeText={handleChange} // 입력창에 글자를 입력할때
        returnKeyType="done" // 엔터키 아이콘 변경
        maxLength={50} // 최대 글자수 제한
        autoCorrect={false} // 자동완성기능 끄기
        onSubmitEditing={hideKeyboard} // 여기서 하면 엔터키 두번 눌러야 할일추가됨 (키보드만 닫는걸로 수정함)
        />
        <TouchableOpacity 
            disabled={disabled} // disabled 값에 따른 버튼 비활성화
            activeOpacity={0.7} // 버튼 클릭시 투명도 변경
            onPress={onPress}   // 버튼 클릭시 실행
        > 
        {/* disabled 값에 따른 버튼 색상 변경 */}
            <View style={[styles.button, { backgroundColor: disabled ? "red" : '#a8c8ffff' }]}> 
                <Text style={styles.buttonText}>추가</Text>
            </View>
        </TouchableOpacity>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    height: 70,
    paddingLeft: 10,
    borderColor: 'transparent',
    borderTopWidth: 3,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    backgroundColor: '#fff',
    position: 'absolute',
    bottom: 0, left: 0, right: 0
  },
  input: {
    color: '#a8c8ffff',
    fontSize: 20,
    paddingVertical: 20,
    flex: 1
  },
  button: {
    width: 80,
    height: 35,
    borderRadius: 20,
    marginRight: 10,
    justifyContent: 'center',
    alignItems: 'center',
    overflow: 'hidden'
  },
  buttonText: {
    color: '#fff',
    letterSpacing: 3,
    fontWeight: 'bold',
    fontSize: 15
  }
})

export default TodoInsert

components > TodoInsert.js 파일을 위와 같이 수정한다. 

container: {
    height: 70,
    paddingLeft: 10,
    borderColor: 'transparent',
    borderTopWidth: 3,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    backgroundColor: '#fff',
    position: 'absolute', // 수정
    bottom: 0, left: 0, right: 0 // 수정
  },

Default 컴포넌트의 레이아웃을 수정하면서 입력창의 레이아웃이 틀어진다. 입력창은 화면 하단에 고정될 수 있도록 위와 같이 수정하도록 한다. 

 

* 할일제목이 길때 레이아웃 깨지는 문제 해결하기 

할일 제목이 길때 레이아웃 깨짐

import React, { useState, useRef, useEffect } from 'react'
import { View, Text, StyleSheet, TouchableWithoutFeedback, TextInput, TouchableOpacity, Keyboard } from 'react-native'
import moment from 'moment'

import { 
    updateDate
  } from '../apis/firebase'

let lastTap = null 

function TodoItem({ id, title, category, isDone, createdAt, removeTodo }){
    console.log("할일 생성시각: ", title, createdAt)
    const [doubleTabbed, setDoubleTabbed] = useState(false)
    const [text, setText] = useState("")
    const inputRef = useRef(null)

    const handleDoubleTab = (e) => {
        console.log(inputRef.current)
        setDoubleTabbed(!doubleTabbed)
        setText(title)
    }
    const ishandleDoubleTap = () => {
        const now = Date.now() // 밀리세컨드초
        const delay = 300
        if(lastTap && (now - lastTap) < delay){
            return true 
        }else{
            lastTap = now
            return false  
        }
    }
    const handleTap = () => {
        updateDate('todos', id, {
            isDone: !isDone
        })
    }
    const handlePress = (e) => {
        if(ishandleDoubleTap()){
            handleDoubleTab()
            console.log("더블탭")
            handleTap()
        }else{
            handleTap()   
            console.log("------ 탭 ----------")
        }
    }
    const handleBlur = (e) => {
        e.stopPropagation()
        console.log("블러")
        setDoubleTabbed(!doubleTabbed)
        Keyboard.dismiss()
        updateDate('todos', id, {
            title: text.trim()
        })
    }
    const handleChange = (text) => {
        // if (/\n/.test(text)) { // 엔터키 입력시 
        //     Keyboard.dismiss()
        //     // inputRef.current.blur()

        // }else{
        //     setText(text)
        // }
        setText(text)
    }
    const hideKeyboard = (e) => {
        Keyboard.dismiss()
        // inputRef.current.blur()
      }
    const handleRemove = (e) => {
        e.stopPropagation()
        removeTodo(id, title)
    }
    useEffect(() => {
        if(inputRef.current){
            inputRef.current.focus()
        }
    })
    return (
        <TouchableWithoutFeedback onPress={handlePress} onLongPress={handleRemove}>
            <View style={styles.item}>
                <View style={styles.titleMargin} onTouchStart={(e) => {e.stopPropagation()}}>
                    {doubleTabbed ? 
                        (
                            <TouchableWithoutFeedback>
                                <TextInput 
                                    value={text} 
                                    onBlur={handleBlur} 
                                    ref={inputRef}
                                    onChangeText={handleChange} // 입력창에 글자를 입력할때
                                    // onSubmitEditing={hideKeyboard} // 여기서 하면 엔터키 두번 눌러야 할일추가됨 (키보드만 닫는걸로 수정함)
                            />
                            </TouchableWithoutFeedback>
                        ) : 
                        <Text style={[styles.title, {textDecorationLine: (isDone && !doubleTabbed ) ? 'line-through': 'none'}]}>{title}</Text>}
                </View>
                <View>
                    <Text>{category} ({isDone ? "종료": "진행중"})</Text>
                    <Text style={styles.dateText}>{createdAt && moment(createdAt.toDate()).format('YY-MM-DD hh:mm:ss')}</Text>
                </View>
            </View>
        </TouchableWithoutFeedback>
    )
}

const styles = StyleSheet.create({
    item: {
        flexDirection: 'row',
        alignItems: 'flex-start',
        paddingLeft: 10,
        paddingVertical: 10,
        // backgroundColor: '#d6e3ffff',
        // borderBottomWidth: 1,
        // borderBottomColor: '#a8c8ffff',
    },
    titleMargin: {
        marginRight: 10,
        flex: 1
    },
    title: {
        fontWeight: 'bold',
        fontSize: 20,
    },
    dateText: {
        fontSize: 12,
        paddingRight: 10
    }
})

export default React.memo(TodoItem)

components > TodoItem.js 파일을 위와 같이 수정한다. 

titleMargin: {
        marginRight: 10,
        flex: 1 // 추가
 },

수정된 부분은 위와 같다. 할일제목이 보여지는 영역에 flex: 1을 적용해서 카테고리와 할일 생성시각이 보이는 영역을 제외한 나머지 영역을 다 차지할 수 있도록 한다. 

dateText: {
    fontSize: 12,
    paddingRight: 10 // 추가
}

카테고리와 할일 생성시각이 보이는 영역 우측에 패딩을 적용하였다. 

728x90