Bố cục ứng dụng React Native phổ biến: Trang lịch
() translation by (you can also view the original English article)
Trong loạt bài này, bạn sẽ tìm hiểu cách sử dụng React Native để tạo bố cục trang thường được sử dụng trong các ứng dụng dành cho thiết bị di động. Các bố cục bạn sẽ tạo sẽ không hoạt động — thay vào đó, trọng tâm chính của loạt bài này là làm cho bàn tay của bạn bị bẩn khi đặt nội dung trong ứng dụng React Native của bạn.
Nếu bạn chưa từng bố trí ứng dụng React Native hoặc tạo kiểu nói chung, hãy xem bài hướng dẫn trước của tôi:
Để theo sát với loạt bài này, tôi mời bạn trước tiên thử tự mình tạo lại từng màn hình, trước khi đọc những chỉ chi tiết của tôi trong bài viết. Bạn sẽ thực sự không thu được nhiều lợi ích từ hướng dẫn này khi chỉ đọc nó! Hãy thử làm trước khi tìm câu trả lời ở đây. Nếu bạn thành công tạo ra nó giống như màn hình nguyên bản, thì hãy so sánh kết quả của bạn với tôi. Sau đó tự mình quyết định kết quả nào tốt hơn!
Trong phần thứ hai của loạt bài này, bạn sẽ tạo trang lịch (calendar page) sau đây:



Những ứng dụng lịch được sử dụng để theo dõi các sự kiện và cuộc hẹn do người dùng bổ sung vào. Bạn sẽ tìm thấy các biến thể khác nhau đang được sử dụng, nhưng đa số ứng dụng lịch sẽ có các yếu tố giống nhau mà một bộ lịch cần có: tháng và năm hiện tại, các ngày trong tháng và các sự kiện hoặc cuộc hẹn do người dùng thêm vào.
Dưới đây là một vài ví dụ về kiểu bố cục này:






Thiết lập dự án
Bước đầu tiên, tất nhiên là thiết lập dự án React Native mới:
1 |
react-native init react-native-common-screens |
Khi dự án được thiết lập, hãy mở file index.android.js
và thay thế code mặc định bằng code sau đây:
1 |
import React, { Component } from 'react'; |
2 |
import { |
3 |
AppRegistry
|
4 |
} from 'react-native'; |
5 |
|
6 |
import Calendar from './src/pages/Calendar'; |
7 |
|
8 |
export default class ReactNativeCommonScreens extends Component { |
9 |
|
10 |
render() { |
11 |
return ( |
12 |
<Calendar /> |
13 |
);
|
14 |
}
|
15 |
|
16 |
}
|
17 |
|
18 |
AppRegistry.registerComponent('ReactNativeCommonScreens', () => ReactNativeCommonScreens); |
Tạo một thư mục src/pages
và tạo một file Calendar.js
bên trong thư mục đó.
Bạn cũng sẽ cần package react-native-vector-icons
. Package này đặc biệt được sử dụng cho các biểu tượng điều hướng cũng như các biểu tượng khác cần có trong trang.
1 |
npm install --save react-native-vector-icons |
Mở file android/app/build.gradle
và bổ sung một tham chiếu tới package đó:
1 |
dependencies { |
2 |
//rest of the dependencies are here at the top |
3 |
compile project(':react-native-vector-icons') //add this |
4 |
} |
Thao tác tương tự với file android/settings.gradle
bằng cách thêm phần sau đây vào cuối file:
1 |
include ':react-native-vector-icons' |
2 |
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') |
Mở android/app/src/main/java/com/react-native-common-screens/MainApplication.java
và import package:
1 |
import java.util.Arrays; |
2 |
import java.util.List; |
3 |
|
4 |
import com.oblador.vectoricons.VectorIconsPackage; //add this |
Sau cùng, khởi tạo package:
1 |
@Override
|
2 |
protected List<ReactPackage> getPackages() { |
3 |
return Arrays.<ReactPackage>asList( |
4 |
new MainReactPackage(), |
5 |
new VectorIconsPackage() //add this |
6 |
);
|
7 |
}
|
Tạo trang Calendar (lịch)
Được rồi, bây giờ bạn đã tự mình cố gắng viết code cho bố cục (không gian lận, phải không nào?), Tôi sẽ chỉ cho bạn cách tôi xây dựng triển khai của mình.
Lúc đầu, tôi nghĩ rằng đây sẽ là một trong những điều khó khăn nhất để thực hiện, nhưng tin tôi đi, khi bạn đã nắm những căn bản thì việc này không quá phức tạp đâu. Có thể sử dụng code JavaScript để hỗ trợ hiển thị.
Bắt đầu bằng việc kèm tất cả các component và package bạn sẽ cần:
1 |
import React, { Component } from 'react'; |
2 |
|
3 |
import { |
4 |
StyleSheet, |
5 |
Text, |
6 |
View, |
7 |
ScrollView
|
8 |
} from 'react-native'; |
9 |
|
10 |
import Icon from 'react-native-vector-icons/FontAwesome'; |
11 |
import { range } from 'lodash'; |
12 |
import Button from '../components/Button'; |
Lần này có một package mới mà bạn chưa từng cài đặt và đó là lodash. Thực sự bạn sẽ không cần toàn bộ thư viện lodash, chỉ cần hàm range
. Hàm này được sử dụng để tạo một mảng các con số dựa trên một phạm vi cụ thể. Bạn có thể cài đặt riêng chức năng này bằng cách xử lý lệnh npm install --save lodash.range
trong terminal của bạn.
Thêm code mẫu có sẵn để tạo các trang:
1 |
export default class Calendar extends Component { |
2 |
render() { |
3 |
return ( |
4 |
<ScrollView style={styles.container}> |
5 |
...
|
6 |
</ScrollView> |
7 |
);
|
8 |
}
|
9 |
}
|
10 |
|
11 |
const styles = StyleSheet.create({ |
12 |
container: { |
13 |
flex: 1 |
14 |
}
|
15 |
});
|
Header có ba thành phần trong đó: nút quay lại trang trước, tiêu đề của trang hiện tại và văn bản hiển thị định dạng thân thiện với người xem của ngày đang được chọn.
1 |
<View style={styles.header}> |
2 |
<Button |
3 |
noDefaultStyles={true} |
4 |
onPress={this.press.bind(this)} |
5 |
styles={{button: styles.header_item}} |
6 |
>
|
7 |
<View style={styles.header_button}> |
8 |
<Icon name="chevron-left" size={30} color="#FFF" /> |
9 |
<Text style={[styles.header_text]}> Menu</Text> |
10 |
</View> |
11 |
</Button> |
12 |
<View style={styles.header_item}> |
13 |
<Text style={[styles.header_text, styles.text_center, styles.bold_text]}>Calendar</Text> |
14 |
</View> |
15 |
<View style={styles.header_item}> |
16 |
<Text style={[styles.header_text, styles.text_right]}>Today</Text> |
17 |
</View> |
18 |
</View> |



header
có một flexDirection
của row
vì thế mỗi header_item
được xếp chồng lên nhau theo chiều ngang. Giá trị flex tương tự được gán cho mỗi header_item để chúng chiếm lĩnh một lượng không gian bằng nhau. text_center
và text_right
được sử dụng để căn chỉnh văn bản bên trong các header_items
nằm ở trung tâm và bên phải. Điều đã hoàn tất bởi vì theo mặc định chúng được căn chỉnh ở phía bên trái của container chứa chúng.
1 |
header: { |
2 |
backgroundColor: '#329BCB', |
3 |
flexDirection: 'row', |
4 |
padding: 20 |
5 |
},
|
6 |
header_item: { |
7 |
flex: 1 |
8 |
},
|
9 |
header_button: { |
10 |
flexDirection: 'row' |
11 |
},
|
12 |
text_center: { |
13 |
textAlign: 'center' |
14 |
},
|
15 |
text_right: { |
16 |
textAlign: 'right' |
17 |
},
|
18 |
header_text: { |
19 |
color: '#fff', |
20 |
fontSize: 20 |
21 |
},
|
22 |
bold_text: { |
23 |
fontWeight: 'bold' |
24 |
},
|
Một khi các kiểu (styles) đã được thêm vào, nó sẽ trông giống như sau:



Tiếp theo là bộ lịch thực sự, được chia thành ba phần: tiêu đề, các ngày trong tuần và các ngày theo lịch:
1 |
<View> |
2 |
<View style={styles.calendar_header}> |
3 |
...
|
4 |
</View> |
5 |
<View style={styles.calendar_weekdays}> |
6 |
...
|
7 |
</View> |
8 |
<View style={styles.calendar_days}> |
9 |
...
|
10 |
</View> |
11 |
</View> |
Tiêu đề lịch cho phép người dùng thay đổi năm và tháng.
Có ít nhất hai cách để thực hiện việc này. Phương pháp đầu tiên là xem từng thành phần là một mục riêng lẻ và áp dụng justifyContent: 'space-between'
cho container của nó. Phương pháp thứ hai là nhóm tất cả các phần tử liên quan đến năm và nhóm những phần tử liên quan đến tháng đó.
Phương pháp thứ hai là phương thức được áp dụng bên dưới. Phương pháp này dễ hiểu hơn nhiều vì nút điều hướng dùng để trở về một năm trước, chính năm đó, và nút để tiến lên một năm đều có liên quan, vậy bạn có thể xem chúng là một thành phần duy nhất bằng cách đặt chúng trong cùng container. Điều này cũng đúng với các điều khiển tháng.
1 |
<View style={styles.calendar_header}> |
2 |
<View style={styles.calendar_header_item}> |
3 |
<Button |
4 |
noDefaultStyles={true} |
5 |
onPress={this.press.bind(this)} |
6 |
>
|
7 |
<Icon name="chevron-left" size={18} color="#333" /> |
8 |
</Button> |
9 |
<Text style={styles.calendar_header_text}>2013</Text> |
10 |
<Button |
11 |
noDefaultStyles={true} |
12 |
onPress={this.press.bind(this)} |
13 |
>
|
14 |
<Icon name="chevron-right" size={18} color="#333" /> |
15 |
</Button> |
16 |
</View> |
17 |
|
18 |
<View style={styles.calendar_header_item}> |
19 |
<Button |
20 |
noDefaultStyles={true} |
21 |
onPress={this.press.bind(this)} |
22 |
>
|
23 |
<Icon name="chevron-left" size={18} color="#333" /> |
24 |
</Button> |
25 |
<Text style={styles.calendar_header_text}>November</Text> |
26 |
<Button |
27 |
noDefaultStyles={true} |
28 |
onPress={this.press.bind(this)} |
29 |
>
|
30 |
<Icon name="chevron-right" size={18} color="#333" /> |
31 |
</Button> |
32 |
</View> |
33 |
</View> |



Từ đó, bạn có thể áp dụng cùng một kỹ thuật cho hai nhóm các thành phần đó trong cùng một dòng. Để thêm dấu cách giữa hai nút back (lùi lại) và toward (tiến tới) và label (nhãn), chúng tôi sử dụng justifyContent: 'space-between'
. Chúng tôi sử dụng alignItems: 'center'
để dời tất cả các phần tử bên trong nó ra giữa. Cuối cùng, chúng tôi thêm padding (vùng đệm) bên trái và bên phải để tạo thêm không gian giữa hai nhóm.
1 |
calendar_header: { |
2 |
flexDirection: 'row' |
3 |
},
|
4 |
calendar_header_item: { |
5 |
flex: 1, |
6 |
flexDirection: 'row', |
7 |
justifyContent: 'space-between', |
8 |
alignItems: 'center', |
9 |
paddingTop: 20, |
10 |
paddingRight: 40, |
11 |
paddingLeft: 40 |
12 |
},
|
13 |
calendar_header_text: { |
14 |
fontWeight: 'bold', |
15 |
fontSize: 20 |
16 |
},
|



Tiếp theo là các ngày trong tuần. Chúng tôi sử dụng một hàm để hiển thị chúng vì tốt nhất nên sử dụng code JavaScript để hiển thị tất cả các thành phần.
1 |
<View style={styles.calendar_weekdays}> |
2 |
{ this.renderWeekDays() } |
3 |
</View> |
Vậy thay vì có bảy thành phần View
hoặc Text
sẽ hiển thị mỗi ngày trong tuần, bạn chỉ có thể có một mảng chứa các ngày trong tuần. Sau đó bạn có thể chạy vòng lặp qua những ngày đó bằng cách sử dụng hàm Array.map()
. Đối với mỗi lần vòng lặp, hiển thị một thành phần Text
để cho xem ngày.
1 |
renderWeekDays() { |
2 |
let weekdays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; |
3 |
return weekdays.map((day) => { |
4 |
return ( |
5 |
<Text key={day} style={styles.calendar_weekdays_text}>{day.toUpperCase()}</Text> |
6 |
);
|
7 |
});
|
8 |
}
|
Lưu ý rằng trong đoạn mã trên, hàm toUpperCase()
được sử dụng để chuyển đổi sang chữ hoa cho các mẫu tự của mỗi ngày. React Native không đi có với thuộc tính CSS text-transform
, vì vậy đây là cách duy nhất để có được chữ in hoa ngoài việc sử dụng các chữ hoa theo cách thủ công.



Đây là style cho tiêu đề lịch:
1 |
calendar_weekdays_text: { |
2 |
flex: 1, |
3 |
color: '#C0C0C0', |
4 |
textAlign: 'center' |
5 |
},
|



Ngày trên lịch cũng sử dụng một hàm để hiển thị ngày:
1 |
<View style={styles.calendar_days}> |
2 |
{ this.renderWeeks() } |
3 |
</View> |
Hàm renderWeeks()
sử dụng hàm range()
trong lodash để tạo một mảng chứa các ngày của tháng trước và các ngày của tháng hiện tại. Hai mảng này sau đó được hợp làm một.
Tuy nhiên, bạn không thể trực tiếp sử dụng kết quả của mảng này làm nguồn dữ liệu cho các ngày trong lịch. Bởi vì nếu bạn chỉ đơn giản chạy vòng lặp qua các phần tử và xuất ra một Text
component cho mỗi ngày, thì giữa các tuần không có sự phân biệt nào. Bạn đã biết rằng để làm cho mỗi ngày lịch hiển thị inline, bạn cần phải áp kiểu flexDirection: 'row'
vào container chứa nó. Vì vậy, áp dụng kiểu này vào một container sẽ dẫn đến kết quả là tất cả các ngày trong lịch hiển thị trong một dòng.
Điều này có nghĩa là bạn cần phải có container riêng biệt cho mỗi tuần. Câu hỏi là làm thế nào. Một lần nữa, có ít nhất hai cách để thực hiện điều này.
Phương pháp đầu tiên là có biến lưu lại bao nhiêu ngày được xuất ra hiện tại và sau đó bổ sung một câu lệnh có điều kiện sẽ hiển thị một thẻ <View>
mỗi khi biến số chứa 0
và một thẻ </View>
mỗi khi nó là 7
. Khi nó là 7
, thiết lập trở về 0
. Đây là phương pháp đơn giản nhất.
Nhưng ta sẽ sử dụng một phương pháp khác ở đây. Dưới đây, hàm getWeeksArray()
được sử dụng để thực hiện điều này. Hàm này nhận một mảng các ngày và nhóm chúng thành các mảng chứa bảy ngày trong mỗi mảng. Từ đó, bạn có thể lặp qua từng mảng đó để hiển thị container của tuần. Sau đó, đối với mỗi vòng lặp, bạn chạy vòng lặp qua các ngày trong tuần để hiển thị chúng. Đây là điều mà hàm renderDays()
thực hiện.
1 |
renderWeeks() { |
2 |
let past_month_days = range(27, 31); |
3 |
let this_month_days = range(1, 30); |
4 |
|
5 |
let days = past_month_days.concat(past_month_days, this_month_days); |
6 |
let grouped_days = this.getWeeksArray(days); |
7 |
|
8 |
return grouped_days.map((week_days, index) => { |
9 |
return ( |
10 |
<View key={index} style={styles.week_days}> |
11 |
{ this.renderDays(week_days) } |
12 |
</View> |
13 |
);
|
14 |
});
|
15 |
}
|
Đây là hàm getWeeksArray()
:
1 |
getWeeksArray(days) { |
2 |
var weeks_r = []; |
3 |
var seven_days = []; |
4 |
var count = 0; |
5 |
days.forEach((day) => { |
6 |
count += 1; |
7 |
seven_days.push(day); |
8 |
if(count == 7){ |
9 |
weeks_r.push(seven_days) |
10 |
count = 0; |
11 |
seven_days = []; |
12 |
}
|
13 |
});
|
14 |
return weeks_r; |
15 |
}
|
Và đây là hàm renderDays()
:
1 |
renderDays(week_days) { |
2 |
return week_days.map((day, index) => { |
3 |
return ( |
4 |
<Button |
5 |
label={day} |
6 |
key={index} |
7 |
onPress={this.press.bind(this)} |
8 |
styles={{button: styles.day, label: styles.day_text}} |
9 |
noDefaultStyles={true} |
10 |
/> |
11 |
);
|
12 |
});
|
13 |
}
|



Thêm kiểu dáng cho mỗi tuần (week_days
) và ngày (day
và day_text
):
1 |
week_days: { |
2 |
flexDirection: 'row' |
3 |
},
|
4 |
day: { |
5 |
flex: 1, |
6 |
backgroundColor: '#F5F5F5', |
7 |
padding: 17, |
8 |
margin: 2 |
9 |
},
|
10 |
day_text: { |
11 |
textAlign: 'center', |
12 |
color: '#A9A9A9', |
13 |
fontSize: 25 |
14 |
},
|



Tiếp theo là ghi chú được người dùng thêm vào cho ngày đang được chọn và ngày và giờ đã chọn. Một lần nữa, tốt hơn là nhóm các phần tử theo mục đích của chúng thay vì theo cách chúng được bố trí trong trang. Chắc chắn tất cả các thành phần này có liên quan, vì vậy chúng tôi sẽ đặt chúng trong cùng container. Nhưng quan sát kỹ hơn, bạn sẽ bắt đầu thấy rằng có thể nhóm chúng thêm nữa: ghi chú thực sự và ngày đã chọn. Với suy nghĩ đó, đây là markup mà bạn sẽ có được:
1 |
<View style={styles.notes}> |
2 |
<View style={styles.notes_notes}> |
3 |
<Text style={styles.notes_text}>Riding my bike around the neighborhood.</Text> |
4 |
</View> |
5 |
<View style={[styles.notes_selected_date]}> |
6 |
<Text style={styles.small_text}>8:23 PM</Text> |
7 |
<Text style={styles.big_text}>14</Text> |
8 |
<View style={styles.inline}> |
9 |
<Icon name="bicycle" size={20} color="#CCC" /> |
10 |
<Text style={styles.small_text}> THURSDAY</Text> |
11 |
</View> |
12 |
</View> |
13 |
</View> |



Ngày được chọn chiếm ít không gian hơn so với ghi chú, vì vậy bạn phải áp dụng giá trị flex
lớn hơn cho ghi chú. flex: 3
và flex: 1
được sử dụng trong trường hợp này, có nghĩa là các ghi chú chiếm 3/4 không gian đang có và ngày chiếm 1/4. Bạn cũng có thể sử dụng số thập phân (0,75
và 0,25
) nếu điều đó khiến bạn dễ hiểu hơn. Điều quan trọng là chọn một tiêu chuẩn và bám chặt vào nó: alignItems: 'flex-end'
được sử dụng trên notes_selected_date
vì vậy tất cả các thành phần con của nó sẽ được căn lề ở bên phải. Điều này là cần thiết vì theo mặc định chúng được căn lề bên trái.
1 |
notes: { |
2 |
marginTop: 10, |
3 |
padding: 20, |
4 |
borderColor: '#F5F5F5', |
5 |
borderTopWidth: 1, |
6 |
borderBottomWidth: 1, |
7 |
flexDirection: 'row', |
8 |
backgroundColor: '#FAFAFA' |
9 |
},
|
10 |
notes_notes: { |
11 |
flex: 3 |
12 |
},
|
13 |
notes_text: { |
14 |
fontSize: 18 |
15 |
},
|
16 |
notes_selected_date: { |
17 |
flex: 1, |
18 |
alignItems: 'flex-end', |
19 |
flexDirection: 'column' |
20 |
},
|
21 |
small_text: { |
22 |
fontSize: 15 |
23 |
},
|
24 |
big_text: { |
25 |
fontSize: 50, |
26 |
fontWeight: 'bold' |
27 |
},
|
28 |
inline: { |
29 |
flexDirection: 'row' |
30 |
},
|



Cuối cùng, chúng tôi thêm các nhật ký, rất giống với các bản nhật ký trong bài viết trước, vì vậy tôi sẽ để cho bạn để tìm hiểu làm thế nào để hoàn thành bố cục!
1 |
<View style={styles.logs}> |
2 |
<View> |
3 |
<Text style={styles.log_text}>Create New Entry</Text> |
4 |
<Text style={styles.log_subtext}>On Thursday, November 14</Text> |
5 |
</View> |
6 |
<Button |
7 |
noDefaultStyles={true} |
8 |
onPress={this.press.bind(this)} |
9 |
>
|
10 |
<Icon name="chevron-right" size={30} color="#CCC" /> |
11 |
</Button> |
12 |
</View> |
Dưới đây là các kiểu (style):
1 |
logs: { |
2 |
flexDirection: 'row', |
3 |
justifyContent: 'space-between', |
4 |
alignItems: 'center', |
5 |
padding: 20, |
6 |
borderColor: '#F5F5F5', |
7 |
borderBottomWidth: 1 |
8 |
},
|
9 |
log_text: { |
10 |
fontSize: 25 |
11 |
},
|
12 |
log_subtext: { |
13 |
fontSize: 18 |
14 |
}
|
Tổng kết
Thế đấy! Trong hướng dẫn này, bạn đã tạo một trang lịch. Chúng ta đã thực hiện một bố cục lịch đẹp cho ứng dụng và tôi đã chỉ cho bạn làm thế nào JavaScript có thể được sử dụng để bù đắp cho những hạn chế của Flexbox.
Như bạn thấy đấy, chúng ta cần một giải pháp để giới hạn số ngày trong một hàng dừng ở bảy ngày. Flexbox không có cách nào để xác định điều này, vì vậy chúng tôi đã sử dụng JavaScript để tạo lại mảng ban đầu của các ngày theo cách mà chúng được chia thành các nhóm vớ mỗi nhóm có bảy ngày. Từ đó, tất cả những gì chúng ta phải làm là gom từng nhóm vào trong View
và sau đó áp kiểu flexDirection: 'row'
để làm cho mỗi nhóm chỉ hiển thị trong hàng của nó.
Trong hướng dẫn kế tiếp, bạn sẽ tìm hiểu cách triển khai bố cục thường được sử dụng trong các trang gallery (thư viện). Trong khi chờ đợi, hãy xem qua một số hướng dẫn khác của chúng tôi về React Native và Flexbox.