Giao diện người dùng của ứng dụng Android

pdf 97 trang vanle 3120
Bạn đang xem 20 trang mẫu của tài liệu "Giao diện người dùng của ứng dụng Android", để tải tài liệu gốc về máy bạn click vào nút DOWNLOAD ở trên

Tài liệu đính kèm:

  • pdfgiao_dien_nguoi_dung_cua_ung_dung_android.pdf

Nội dung text: Giao diện người dùng của ứng dụng Android

  1. Giáo trình Android Giao diện người dùng của ứng dụng Android Trong các chương trước ta đã làm quen với thành phần cơ bản của giao diện Android là Activity và vòng đời của nó. Tuy nhiên, bản thân Activity không phải là thứ chúng ta nhìn thấy trên màn hình điện thoại, thay vào đó Activity cần có các thành phần đồ họa khác bên trong nó, là các View và ViewGroup. Trong chương này chúng ta sẽ tìm hiểu chi tiết hơn về các View và ViewGroup trong Android để tạo nên giao diện đồ họa của ứng dụng, cũng như cách thức tương tác với chúng. Giao diện người dùng của ứng dụng Android 65
  2. Giáo trình Android View và ViewGroup Như đã đề cập ở trên, mỗi Activity muốn hiển thị giao diện đồ họa cần chứa các thành phần giao diện khác như nút bấm, các nhãn, các ô nhập liệu, checkbox, radio button Những thành phần như vậy trong Android được gọi chung là các View . Tất cả các View đều được kế thừa từ lớp android.view.View . Một hoặc nhiều View có thể được nhóm lại với nhau thành một ViewGroup . Mỗi ViewGroup cũng là một View , được dùng để nhóm các View con bên trong nó và hiển thị chúng theo một thứ tự hay quy luật nào đó. Mọi ViewGroup đều được kế thừa từ lớp android.view.ViewGroup . Các loại ViewGroup phổ biến nhất trong Android bao gồm: LinearLayout AbsoluteLayout TableLayout RelativeLayout FrameLayout ScrollView Các View và ViewGroup tạo thành giao diện của Activity và thường được mô tả ngay trong file layout của Activity, nằm trong thư mục res/layout (file main.xml trong các ví dụ trước). Ví dụ: Một số thuộc tính chung của các View và ViewGroup được kể ra trong bảng dưới đây: Thuộc tính Mô tả layout_width Chiều rộng của View/ViewGroup layout_height Chiều cao của View/ViewGroup layout_marginTop Chiều rộng khoảng trống (lề) phía trên của View layout_marginBottom Chiều rộng khoảng trống (lề) phía dưới của View layout_marginLeft Chiều rộng khoảng trống (lề) phía bên trái của View View và ViewGroup 66
  3. Giáo trình Android layout_marginRight Chiều rộng khoảng trống (lề) phía bên phải của View layout_gravity Cách xếp đặt View (trái, phải, trên, dưới, giữa theo chiều dọc, giữa theo chiều ngang) Phần tiếp theo sẽ mô tả chi tiết hơn về một số loại ViewGroup phổ biến trên. Cần chú ý rằng trong thực tế sử dụng, giao diện đồ họa của ứng dụng thường được tạo thành bởi một tổ hợp phân cấp giữa các loại ViewGroup khác nhau. LinearLayout LinearLayout sắp xếp các view con bên trong nó theo một cột (từ trên xuống dưới) hoặc theo một hàng (từ trái qua phải). Các view con được xếp dọc hoặc ngang tùy thuộc vào tham số android:orientation của LinearLayout, giá trị của tham số này có thể là “ vertical ” (dọc) hoặc “horizontal” (ngang). Xem ví dụ sau: Giao diện trên bao gồm một LinearLayout theo chiều dọc, chứa 1 view bên trong là đoạn chữ “Hello world” và nút bấm “Button” bên dưới nó. Chiều rộng và cao của LinearLayout là fill_parent, tức là nó sẽ chiếm hết chiều rộng, chiều dài của view mẹ (trong trường hợp này là Activity, tức là toàn màn hình). Chiều rộng của textview là 100dp (điểm ảnh không phụ thuộc vào mật độ màn hình, chi tiết xem bên dưới), chiều cao của text này là wrap_content tức là bằng đúng chiều cao của nội dung chứa trong nó (phụ thuộc vào số dòng chữ, kích thước chữ, khoảng cách thực tế). Các đơn vị đo kích thước trong Android bao gồm: dp (hoặc dip) - Density-independent pixel (điểm ảnh không phụ thuộc vào mật độ màn hình). Một dp tương đương với một pixel trên màn hình có mật độ 160 dpi (160 điểm ảnh trên mỗi inch màn hình). Đây là đơn vị được khuyến nghị dùng trong hầu hết các trường hợp đặt kích thước của view trong layout. Chi tiết hơn về mật độ màn hình được đề cập ở phần sau của giáo trình. sp - Scale-independent pixel, đơn vị này tương tự dp, được dùng khi mô tả kích thước font chữ (font size) pt - Point. 1 point = 1/72 inch, dựa trên kích thước vật lý thật của màn hình. px – Pixel – một pixel vật lý trên màn hình, đơn vị này không được khuyên dùng trong thiết kế giao diện ứng dụng vì giao diện sẽ hiển thị không đồng nhất trên các màn hình có độ phân giải khác nhau. Trong ví dụ ở trên, nút bấm có chiều rộng là 160dp và textview là 100dp. Để hiểu được kích thước này, trước hết ta xem khái niệm kích thước và mật độ màn hình trong Android. Ta xét trên ví dụ cụ thể: điện thoại Nexus S của Google. Thiết bị này có màn hình 4 inch theo đường chéo, 2.04 inch theo chiều ngang, với độ phân giải 480x800 pixel. Chiều rộng 2.04 inch với 480 pixel cho ta mật độ điểm ảnh khoảng 235 dpi (dots per inch – điểm ảnh mỗi inch) – xem hình bên dưới. View và ViewGroup 67
  4. Giáo trình Android Android định nghĩa 4 loại mật độ màn hình như sau: Mật độ thấp: Low density (ldpi) - 120 dpi Mật độ trung bình: Medium density (mdpi) - 160 dpi Mật độ cao: High density (hdpi) - 240 dpi Mật độ rất cao: Extra High density (xhdpi) - 320 dpi Mỗi thiết bị sẽ được xếp vào một trong các loại mật độ trên. Ví dụ thiết bị Nexus S ở trên sẽ được xếp vào thiết bị mật độ cao, do mật độ màn hình (235dpi) gần nhất với mật độ hdpi – 240dpi. Còn điện thoại The HTC Hero, có màn hình 3.2inch, độ phân giải 320x480, có mật độ 180dpi sẽ được xếp vào điện thoại mật độ trung bình (mdpi) do gần nhất với con số 160dpi. Dưới đây là hình ảnh của layout trên chạy trên 2 thiết bị có kích thước và độ phân giải khác nhau. Hình bên trái là thiết bị 4 inch, độ phân giải 480x800 (mật độ 235dpi – hdpi), hình bên phải là thiết bị 3.2 inch, độ phân giải 320x480 (mật độ 180dpi). View và ViewGroup 68
  5. Giáo trình Android Có thể thấy mặc dù chạy trên 2 thiết bị có độ phân giải, kích thước và mật độ khác nhau, nhưng nút bấm và text có kích thước rất đồng nhất (nút bấm chiếm khoảng 1/2 chiều ngang màn hình). Kích thước thực tế (tính bằng pixel vật lý) được tính từ kích thước dp như sau: Kích thước pixel thực tế = dp * (dpi / 160), trong đó dpi = 120, 160, 240, hoặc 320 tùy thuộc vào màn hình thiết bị. Ở ví dụ trên, nút bấm trên màn hình bên trái sẽ có kích thước thật là: 160*(240/160) = 240 pixel, còn trên màn hình bên phải sẽ là 160x(160/160) = 160 pixel. Nếu ta thay đơn vị dp trong khai báo layout ở trên thành đơn vị px như dưới đây: thì kết quả sẽ là: View và ViewGroup 69
  6. Giáo trình Android Như vậy có thể thấy việc sử dụng đơn vị px trong thiết kế giao diện là không nên, mà nên sử dụng đơn vị dp, đơn vị này đã bao gồm việc thích nghi giao diện với các kích thước màn hình khác nhau. Nếu thay tham số android:orientation="vertical" của linearlayout thành android:orientation="horizontal" , sẽ thu được: View và ViewGroup 70
  7. Giáo trình Android AbsoluteLayout AbsoluteLayout cho phép đặt các view con bên trong nó tại vị trí chính xác, cố định, tính theo px hoặc dp. Layout loại này không linh hoạt và không thích nghi được với sự thay đổi của độ phân giải màn hình, vì vậy không được khuyên dùng trong các phiên bản Android gần đây. Vì vậy cho đến thời điểm hiện tại, ta có thể quên đi loại layout này. TableLayout Tablelayout cho phép sắp xếp các view con bên trong nó theo dòng và cột. Mỗi dòng được đặt trong thẻ , mỗi view con trong TableRow được đặt trong một ô của dòng, chiều rộng của mỗi cột được xác định bằng chiều rộng lớn nhất của các ô trong cột đó. Xét ví dụ sau: Kết quả của layout trên khi chạy trên emulator với màn hình dọc và ngang sẽ như sau: View và ViewGroup 71
  8. Giáo trình Android RelativeLayout RelativeLayout cho phép các view con bên trong được sắp đặt tương đối với nhau và tương đối so với view mẹ. View và ViewGroup 72
  9. Giáo trình Android Kết quả của layout trên như sau: Mỗi view con trong relativelayout có một số thuộc tính nhất định giúp chúng căn chỉnh theo view mẹ hoặc theo các view khác cùng cấp. Ta có thể thấy các thuộc tính này trong ví dụ ở trên như: layout_alignParentTop – xếp trên cùng so với layout mẹ layout_alignParentLeft – căn trái so với layout mẹ layout_alignLeft – căn trái so với layout khác (có id được chỉ ra trong giá trị của thuộc tính này) layout_alignRight – căn trái so với layout khác (có id được chỉ ra trong giá trị của thuộc tính này) layout_below – đặt xuống dưới layout khác (có id được chỉ ra trong giá trị của thuộc tính này) layout_centerHorizontal – căn giữa theo chiều ngang so với layout mẹ (relativelayout hiện tại) FrameLayout Là layout được dùng để hiển thị một view bên trong. View con của Framelayout luôn được căn phía trên, bên trái so với layout mẹ này. Nếu bạn thêm nhiều hơn 1 view vào bên trong FrameLayout, thì các view này sẽ nằm chồng lên nhau như ví dụ dưới đây. <TextView android:id="@+id/lblComments" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello, Android!" View và ViewGroup 73
  10. Giáo trình Android android:layout_alignParentTop="true" android:layout_alignParentLeft="true" /> ScrollView ScrollView là một FrameLayout đặc biệt, cho phép người dùng cuộn dọc màn hình khi nội dung bên trong của ScrollView chiếm nhiều diện tích hơn view mẹ. <Button android:id="@+id/button2" View và ViewGroup 74
  11. Giáo trình Android android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Button 2" /> Trong ví dụ trên, EditText ở giữa được đặt chiều cao lên đến 600dp để chắc chắn nó vượt quá chiều cao của màn hình điện thoại, khi đó khi chạy trên điện thoại/emulator, ta sẽ thấy view có thể cuộn lên, cuộn xuống như hình vẽ sau: Hình: Scroll view đang được cuộn lên (trái) và cuộn xuống (phải) View và ViewGroup 75
  12. Giáo trình Android Thiết kế giao diện thích nghi với hướng màn hình Một tính năng xuất hiện trên hầu hết các điện thoại thông minh hiện hành là tính năng tự động thay đổi hướng hiển thị của màn hình từ dọc sang ngang hoặc ngược lại khi người dùng xoay điện thoại theo hướng tương ứng. Không ngoại lệ, khi thiết bị Android thay đổi hướng từ dọc sang ngang hoặc ngược lại, activity hiện tại sẽ vẽ lại các view con của mình theo hướng mới, khi đó hàm onCreate của Activity sẽ được gọi lại từ đầu. Mặt khác, do diện tích và tỷ lệ vùng hiển thị theo hướng dọc và hướng ngang khác nhau tương đối nhiều, nên khi thiết kế layout ta cần có biện pháp thích hợp cho sự thay đổi này. Có 2 phương pháp phổ biến để thiết kế layout thích nghi với sự thay đổi này: Neo – neo (xếp cố định) các view con theo các cạnh của màn hình. Đây là cách dễ làm nhất, khi kích thước màn hình thay đổi, vị trí của các layout con sẽ được xếp lại nhưng vẫn căn theo các cạnh gần nhất. Thay đổi kích thước và vị trí – cách làm tốn công và linh hoạt hơn và sắp xếp lại toàn bộ layout của các view con khi có sự thay đổi về màn hình. Ngoài, ra còn có thể thêm/bớt view con với những hướng nhất định. Ta sẽ xem ví dụ cụ thể cho 2 phương án trên. Neo các view con theo các cạnh màn hình Trong ví dụ trên button1 được neo theo góc trên bên trái, button2 – góc trên bên phải, button3 – góc dưới bên trái, Bố cục giao diện thích nghi với hướng màn hình 76
  13. Giáo trình Android button4 – góc dưới bên phải, button5 – chính giữa màn hình. Layout trên khi hiển thị trên màn hình dọc và ngang sẽ như sau: Hình: Neo các nút bấm theo cạnh màn hình - màn hình dọc Hình: Neo các nút bấm theo cạnh màn hình - màn hình ngang Bố cục giao diện thích nghi với hướng màn hình 77
  14. Giáo trình Android Thay đổi kích thước và vị trí Trong phương pháp này, thông thường mỗi hướng màn hình sẽ có một layout riêng. Layout này có thể được cấu hình trong code, hoặc đặt file layout vào thư mục cấu hình sẵn để hệ thống tự lựa chọn trong quá trình chạy. Để tạo layout riêng cho Activity trên theo màn hình ngang, ta tạo thư mục layout-land trong thư mục res, sau đó tạo file main.xml (trùng tên với file layout trong thư mục res/layout), với nội dung như sau: Để ý ta thấy layout này có 7 nút thay vì 5 nút như layout mặc định (cho màn hình dọc). Kết quả hiển thị cho màn hình ngang sẽ xuất hiện thêm 2 nút bấm ở trên và dưới màn hình, căn giữa theo chiều ngang như hình dưới: Bố cục giao diện thích nghi với hướng màn hình 78
  15. Giáo trình Android Hình: Giao diện theo màn hình ngang với 2 nút mới xuất hiện (Top Middle và Bottom Middle) Ngoài ra khi thay đổi hướng màn hình, Activity sẽ được vẽ lại hoàn toàn từ đầu và trạng thái hiện tại sẽ mất, bao gồm giá trị của các trường, nội dung của các view không được đặt tên trong layout Vì vậy, trong trường hợp cần thiết phải lưu trữ trạng thái hiện tại của Activity, việc ta cần làm là nạp chồng 2 hàm sau: @Override public void onSaveInstanceState(Bundle outState) { // save whatever you need to persist outState.putString("ID", "1234567890"); super.onSaveInstanceState(outState); } @Override public void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); // retrieve the information persisted earlier String ID = savedInstanceState.getString("ID"); } Dữ liệu cần lưu trữ được chứa trong một đối tượng Bundle , chi tiết về đối tượng này và các loại dữ liệu có thể lưu trữ trong nó bạn đọc tự tìm hiểu và phần này được coi như bài tập. Trong ví dụ trên ta đã tiến hành lưu và lấy lại giá trị của một chuỗi (“0123456789”) vào Bundle dưới tên là “ID”. Điều khiển hướng của màn hình Trong một số trường hợp, ta cần thiết kế một số Activity chỉ hỗ trợ một loại hướng màn hình cụ thể. Ví dụ, màn hình xem phim có thể được thiết lập chỉ có hướng nằm ngang, hay màn hình gọi điện thoại được thiết lập luôn luôn hiển thị theo chiều dọc. Để làm việc này, ta có thể khai báo thuộc tính của activity tương ứng trong file Manifest hoặc cấu hình trong mã nguồn của hàm onCreate tương ứng với Activity cần thiết lập. Ví dụ về cấu hình hướng màn hình luôn nằm ngang trong mã nguồn: Bố cục giao diện thích nghi với hướng màn hình 79
  16. Giáo trình Android @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // change to landscape mode setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } Và trong Manifest: Bố cục giao diện thích nghi với hướng màn hình 80
  17. Giáo trình Android Sử dụng trình đơn (Menu) Trình đơn (bảng chọn, menu) dùng để hiển thị các hành động thường ít dùng hơn và không hiển thị trực tiếp lên màn hình. Trong Android có 2 loại trình đơn : Trình đơn chính (Options menu) – hiển thị các hành động liên quan đến toàn bộ Activity hiện tại. Trong Android, để kích hoạt trình đơn này, ta bấm nút Menu của thiết bị (phím cứng hoặc phím ảo trên màn hình) Trình đơn ngữ cảnh (Context Menu) – hiển thị các hành động liên quan đến một view cụ thể trên màn hình, trình đơn này được kích hoạt bằng cách bấm và giữ ngón tay (long tap) trên view tương ứng. Hình dưới đây minh họa trình đơn chính (bên trái) và trình đơn ngữ cảnh khi bấm và giữ tay trên ảnh (bên phải) của ứng dụng Browser (trình duyệt web): Hình: Trình đơn chính (trái) và trình đơn ngữ cảnh (phải) Về mặt lập trình, 2 loại menu này sử dụng cùng một lớp ( android.view.Menu ) và chứa các đối tượng giống nhau ( android.view.MenuItem ). Vì vậy ta tạo sẵn 2 phương thức dùng chung cho 2 loại menu này để tạo menu với các mục (item) bên trong và xử lý sự kiện bấm vào từng item như dưới đây: Hàm CreateMenu dùng để thêm 7 mục vào menu có sẵn (được truyền vào dưới ạng tham số): private void CreateMenu(Menu menu) { menu.setQwertyMode(true); MenuItem mnu1 = menu.add(0, 0, 0, "Item 1"); { mnu1.setAlphabeticShortcut('a'); mnu1.setIcon(R.drawable.ic_launcher); } MenuItem mnu2 = menu.add(0, 1, 1, "Item 2"); Sử dụng trình đơn (Menu) 81
  18. Giáo trình Android { mnu2.setAlphabeticShortcut('b'); mnu2.setIcon(R.drawable.ic_launcher); } MenuItem mnu3 = menu.add(0, 2, 2, "Item 3"); { mnu3.setAlphabeticShortcut('c'); mnu3.setIcon(R.drawable.ic_launcher); } MenuItem mnu4 = menu.add(0, 3, 3, "Item 4"); { mnu4.setAlphabeticShortcut('d'); } menu.add(0, 4, 4, "Item 5"); menu.add(0, 5, 5, "Item 6"); menu.add(0, 6, 6, "Item 7"); } Hàm MenuChoice để xử lý sự kiện tương ứng với từng mục được lựa chọn (truyền vào dạng tham số). Với mục đích minh họa, trong ví dụ sau, khi một mục được chọn, ta chỉ đơn giản hiển thị tên của mục đó bằng thông báo dạng Toast . private boolean MenuChoice(MenuItem item) { switch (item.getItemId()) { case 0: Toast.makeText(this, "You clicked on Item 1", Toast.LENGTH_LONG).show(); return true; case 1: Toast.makeText(this, "You clicked on Item 2", Toast.LENGTH_LONG).show(); return true; case 2: Toast.makeText(this, "You clicked on Item 3", Toast.LENGTH_LONG).show(); return true; case 3: Toast.makeText(this, "You clicked on Item 4", Toast.LENGTH_LONG).show(); return true; case 4: Toast.makeText(this, "You clicked on Item 5", Toast.LENGTH_LONG).show(); return true; case 5: Toast.makeText(this, "You clicked on Item 6", Toast.LENGTH_LONG).show(); return true; case 6: Toast.makeText(this, "You clicked on Item 7", Toast.LENGTH_LONG).show(); return true; } return false; } Trình đơn chính Để hiển thị trình đơn chính, ta nạp chồng hàm onCreateOptionMenu(Menu menu) và thêm các mục vào đối tượng menu (trong tham số của hàm): @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); CreateMenu(menu); return true; } Sử dụng trình đơn (Menu) 82
  19. Giáo trình Android Để hiển thị menu này lúc chạy ứng dụng, ta bấm phím MENU của thiết bị. Để xử lý sự kiện chọn một item trong menu hiện ra, ta cần nạp chồng hàm onOptionsItemSelected(MenuItem item) : @Override public boolean onOptionsItemSelected(MenuItem item) { return MenuChoice(item); } Hình bên dưới minh họa menu chính được tạo ở trên trong trường hợp thiết lập SDK nhỏ nhất được hỗ trợ trong AndroidManifest.xml >=10 ( - hình bên trái) và hình bên phải). Hình: Trình đơn chính với API mức 10 (trái) và API mức 9 (phải) Trình đơn ngữ cảnh Để hiển thị trình đơn ngữ cảnh, ta nạp chồng hàm onCreateContextMenu(Menu menu) và thêm các mục vào đối tượng menu (là tham số của hàm): @Override public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, view, menuInfo); CreateMenu(menu); } Sử dụng trình đơn (Menu) 83
  20. Giáo trình Android Để xử lý sự kiện chọn một item trong menu hiện ra, ta cần nạp chồng hàm onContextItemSelected (MenuItem item) : @Override public boolean onContextItemSelected (MenuItem item) { return MenuChoice(item); } Để gắn menu ngữ cảnh này vào nút bấm button1, ta thêm đoạn mã sau vào hàm onCreate của activity: Button btn = (Button) findViewById(R.id.button1); btn.setOnCreateContextMenuListener(this); Để hiển thị menu này lúc chạy ứng dụng, ta bấm và giữ ngón tay lên trên nút bấm button1, trình đơn hiện ra như hình bên dưới: Hình: Trình đơn ngữ cảnh xuất hiện khi bấm và giữ ngón tay trên nút bấm button1 Sử dụng trình đơn (Menu) 84
  21. Giáo trình Android Sử dụng thanh tác vụ (Action Bar) Cùng với Fragment, một tính năng mới được giới thiệu từ Android 3.0 HoneyComb là thanh tác vụ bên trong ứng dụng (Action Bar) để thay thế cho thanh tiêu đề cũ. Action bar bao gồm icon của ứng dụng và tiêu đề của Activity ở bên trái. Ngoài ra, người dùng có thể đặt ở bên phải các nút bấm đặc biệt, gọi là action item. Hình bên dưới minh họa action bar của ứng dụng email trong Android với icon ứng dụng, tiêu đề email và 4 action item ở bên phải: Hình: ActionBar với tiêu đề và các nút bấm Action Bar dùng chung menu chính (option menu) như mô tả trong phần trước. Để các menu item trong menu chính hiển thị trên Action bar dưới dạng các action item, ta chỉ cần gọi hàm mnu1.setShowAsAction() của mỗi menu item. private void CreateMenu(Menu menu) { MenuItem mnu1 = menu.add(0, 0, 0, "Item 1"); { mnu1.setIcon(R.drawable.ic_launcher); mnu1.setShowAsAction( MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); } MenuItem mnu2 = menu.add(0, 1, 1, "Item 2"); { mnu2.setIcon(R.drawable.ic_launcher); mnu2.setShowAsAction( MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); } MenuItem mnu3 = menu.add(0, 2, 2, "Item 3"); { mnu3.setIcon(R.drawable.ic_launcher); mnu3.setShowAsAction( MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); Sử dụng thanh tác vụ (Action Bar) 85
  22. Giáo trình Android } MenuItem mnu4 = menu.add(0, 3, 3, "Item 4"); { mnu4.setShowAsAction( MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); } MenuItem mnu5 = menu.add(0, 4, 4, "Item 5"); { mnu5.setShowAsAction( MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); } } Kết quả khi chạy trên thiết bị như hình bên dưới. Đối với màn hình dọc, có ít diện tích để hiển thị action item hơn (2 cái), 3 item còn lại sẽ hiển thị dưới dạng trình đơn chính như bình thường: Hình: Action Bar với 2 nút bấm khi màn hình dọc Đối với màn hình ngang, nhiều item được hiển thị dưới dạng action item hơn (4 cái): Sử dụng thanh tác vụ (Action Bar) 86
  23. Giáo trình Android Hình: Action Bar với 4 nút bấm khi màn hình ngang Sử dụng thanh tác vụ (Action Bar) 87
  24. Giáo trình Android Xử lý sự kiện tương tác với các thành phần đồ họa Người dùng tương tác với giao diện đồ họa của ứng dụng Android theo 2 mức: mức Activity và mức View. Ở mức Activity, lớp Activity cần nạp chồng các phương thức tương ứng được định nghĩa sẵn. Một số phương thức như vậy có thể kể đến như: onKeyDown – được gọi khi một phím được nhấn xuống và sự kiện này chưa được xử lý bởi bất cứ view con nào trong activty onKeyUp – được gọi khi một phím được nhả ra và sự kiện này chưa được xử lý bởi bất cứ view con nào trong activty onMenuItemSelected – được gọi khi người dùng lựa chọn một item trong menu đang được mở ra onMenuOpened – được gọi khi một trình đơn được mở ra. Nạp chồng hàm xử lý sự kiện của Activity Ví dụ: @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: Toast.makeText(getBaseContext(), "Center was clicked", Toast.LENGTH_LONG).show(); break; case KeyEvent.KEYCODE_DPAD_LEFT: Toast.makeText(getBaseContext(), "Left arrow was clicked", Toast.LENGTH_LONG).show(); break; case KeyEvent.KEYCODE_DPAD_RIGHT: Toast.makeText(getBaseContext(), "Right arrow was clicked", Toast.LENGTH_LONG).show(); break; case KeyEvent.KEYCODE_DPAD_UP: Toast.makeText(getBaseContext(), "Up arrow was clicked", Toast.LENGTH_LONG).show(); break; case KeyEvent.KEYCODE_DPAD_DOWN: Toast.makeText(getBaseContext(), "Down arrow was clicked", Toast.LENGTH_LONG).show(); break; } return false; } Đăng ký sự kiện cho từng View Mỗi view trong Android có thể sinh ra các sự kiện khi người dùng tác động lên nó. Ví dụ nút bấm sinh ra sự kiện onClick khi người dùng bấm vào nó, EditText sinh ra sự kiện onFocusChange khi nhận focus (người dùng bấm vào ô nhập liệu này) hoặc mất focus (khi view khác nhận focus). Để đăng ký sự kiện tương tác với các view, ta có thể sử dụng “lớp ẩn danh” (anonymous class) như ví dụ dưới đây: Button btn1 = (Button)findViewById(R.id.btn1); btn1.setOnClickListener(btnListener); Xử lý sự kiện tương tác với các thành phần đồ họa 88
  25. Giáo trình Android và: // create an anonymous class to act as a button click listener private OnClickListener btnListener = new OnClickListener() { public void onClick(View v) { Toast.makeText(getBaseContext(), ((Button) v).getText() + " was clicked", Toast.LENGTH_LONG).show(); } }; Hoặc sử dụng hàm nội bộ ẩn danh (anonymous inner class) như sau: // create an anonymous inner class to act as an onfocus listener EditText txt1 = (EditText)findViewById(R.id.txt1); txt1.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { Toast.makeText(getBaseContext(), ((EditText) v).getId() + " has focus - " + hasFocus, Toast.LENGTH_LONG).show(); } }); Xử lý sự kiện tương tác với các thành phần đồ họa 89
  26. Giáo trình Android Thiết kế giao diện người dùng với các View cơ bản Trong chương trước, chúng ta đã làm quen với các loại viewgroup trong Android và cách sử dụng chúng để sắp xếp các view con trong activity. Trong chương này, ta sẽ làm quen với các loại view dùng để tạo nên giao diện đồ họa cho ứng dụng: các nút bấm, chữ, hình ảnh, danh sách Thiết kế giao diện người dùng với các View cơ bản 90
  27. Giáo trình Android Sử dụng các View cơ bản trong Android Phần này mô tả một số View cơ bản nhất thường được dùng để tạo nên giao diện đồ họa cho ứng dụng Android như: TextView EditText Button ImageButton CheckBox ToggleButton RadioButton RadioGroup TextView Như tên gọi của nó, TextView dùng để hiển thị đoạn văn bản (dạng chữ) lên màn hình. Đây là view đơn giản nhất và dùng rất nhiều trong Android. Ta đã gặp view này ngay từ dự án HelloAndroid trong những chương đầu tiên: Ngoài một số thuộc tính chung như layout_width, layout_height , ta có thể lựa chọn tùy chỉnh kích thước font chữ (thường tính bằng sp – scalable pixel), loại font chữ, kiểu chữ (in đậm, in nghiêng), số dòng tối đa để hiển thị đoạn chữ Nếu ta muốn cho phép người dùng thay đổi/nhập nội dung chữ thì cần sử dụng một view kế thừa từ TextView là EditText sẽ được nhắc đến ở phần dưới. Button và ImageButton Dùng để hiển thị nút bấm lên màn hình. Sự kiện phổ biến nhất của nút bấm là sự kiện onClick , được sinh ra khi người dùng bấm lên nút bấm. Điểm khác nhau giữa Button và ImageButton là Button dùng chữ (text) làm nhãn, còn ImageButton dùng ảnh. EditText Kế thừa từ lớp TextView , cho phép người dùng sửa được nội dung chữ trong nó. CheckBox Là một loại nút bấm đặc biệt, có 2 trạng thái: được chọn ( checked ) và không được chọn ( unchecked ) RadioButton và RadioGroup RadioButton có 2 trạng thái là được chọn và không được chọn, còn RadioGroup nhóm các RadioButton lại với nhau để đảm bảo cùng một lúc chỉ có tối đa 01 RadioButton trong nhóm có trạng thái được chọn. ToggleButton Tương tự như CheckBox, là một loại nút bấm đặc biệt, có 2 trạng thái là bật ( checked ) và tắt ( unchecked ). Sử dụng các View cơ bản trong Android 91
  28. Giáo trình Android Ta sẽ xem các view cơ bản trên trong cùng một ví dụ dưới đây: Chạy ứng dụng với activity có layout như trên, ta sẽ thấy các view cơ bản này được xếp từ trên xuống dưới theo thứ tự lần lượt (do nằm trong LinearLayout). Thử thực hiện các thao tác chọn trên các nút ToggleButton , CheckBox và RadioButton ta sẽ thấy trạng thái chọn của chúng như hình bên phải: Sử dụng các View cơ bản trong Android 92
  29. Giáo trình Android Hình: Các view cơ bản và các trạng thái không được chọn (trái) và được chọn (phải) Ô nhập liệu (EditText) ở trên được thiết lập chiều cao là “wrap_content” nên chiều cao của nó sẽ được tự động điều chỉnh khi nội dung bên trong thay đổi: View RadioButton có thuộc tính orientation tương tự như LinearLayout, cho phép xếp các RadionButton bên trong theo chiều dọc hoặc chiều ngang. Mỗi view trong ví dụ trên đều được đặt tên (dùng thuộc tính android:id ), tên này được sử dụng trong mã nguồn để truy cập đến view tương ứng thông qua các hàm View.findViewById() hoặc Activity.findViewById() . Sau khi xác định được view này theo id trong mã nguồn, ta có thể thêm các hàm xử lý sự kiện cho chúng. Ta xét ví dụ dưới đây: Sử dụng các View cơ bản trong Android 93
  30. Giáo trình Android // Button view Button btnOpen = (Button) findViewById(R.id.btnOpen); btnOpen.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { DisplayToast("You have clicked the Open button"); } }); // CheckBox CheckBox checkBox = (CheckBox) findViewById(R.id.chkAutosave); checkBox.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { if (((CheckBox)v).isChecked()) DisplayToast("CheckBox is checked"); else DisplayToast("CheckBox is unchecked"); } }); // RadioButton RadioGroup radioGroup = (RadioGroup) findViewById(R.id.rdbGp1); radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() { public void onCheckedChanged(RadioGroup group, int checkedId) { RadioButton rb1 = (RadioButton) findViewById(R.id.rdb1); if (rb1.isChecked()) { DisplayToast("Option 1 checked!"); } else { DisplayToast("Option 2 checked!"); } } }); // ToggleButton ToggleButton toggleButton = (ToggleButton) findViewById(R.id.toggle1); toggleButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { if (((ToggleButton)v).isChecked()) DisplayToast("Toggle button is On"); else DisplayToast("Toggle button is Off"); } }); Trong đó, hàm DisplayToast đơn giản chỉ hiển thị thông báo dạng Toast lên màn hình: private void DisplayToast(String msg) { Toast.makeText(getBaseContext(), msg, Toast.LENGTH_SHORT).show(); } Riêng sự kiện onClick cho nút bấm btnSave được khai báo ngay trong file layout: android:onClick="btnSaved_clicked" khi đó trong mã nguồn của Activity cần khai báo hàm tương ứng: public void btnSaved_clicked (View view) { DisplayToast("You have clicked the Save button1"); } ProgressBar Sử dụng các View cơ bản trong Android 94
  31. Giáo trình Android ProgressBar cho phép hiển thị thanh trạng thái “chờ đợi” khi có một tác vụ gì đó đang chạy, như khi có một tác vụ tải một tập tin chạy ngầm phía dưới. Ví dụ dưới đây minh họa cách sử dụng thanh tiến trình này. Trong layout của Activity, thêm view ProgressBar như sau: Trong mã nguồn của Activity, ta tạo một Thread riêng để giả lập thao tác dài chạy ngầm và cập nhật lại trạng thái của progressBar sau mỗi bước thực hiện: progress = 0; progressBar = (ProgressBar) findViewById(R.id.progressbar); progressBar.setMax(200); // do some work in background thread new Thread(new Runnable() { public void run() { //—-do some work here—- while (progressStatus < 100) { progressStatus = doSomeWork(); //—-Update the progress bar—- handler.post(new Runnable() { public void run() { progressBar.setProgress(progressStatus); } }); } // hides the progress bar handler.post(new Runnable() { public void run() { progressBar.setVisibility(View.GONE); } }); } // do some long running work here private int doSomeWork() { try { // simulate doing some work Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } return ++progress; } }).start(); Chạy ứng dụng với Activity như trên, ta sẽ thấy ProgressBar như hình minh họa bên dưới: Sử dụng các View cơ bản trong Android 95
  32. Giáo trình Android Hình: Progress Bar không xác định tiến trình cụ thể Mặc định ProgressBar là thanh trạng thái không xác định (không có trạng thái cụ thể) nên ta sẽ chỉ thấy hình tròn xoay xoay. Ta có thể thay đổi giao diện của thanh trạng thái này theo các cách khác nhau. Ví dụ dưới đây ta thêm thuộc tính style="@android:style/Widget.ProgressBar.Horizontal" để biến thanh trạng thái thành dạng thanh chạy ngang có hiển thị phần đã thực hiện và phần còn lại: Hình: Progress Bar có xác định tiến trình đã chạy xong Sử dụng các View cơ bản trong Android 96
  33. Giáo trình Android TimePicker và DatePicker Chọn ngày tháng, giờ là thao tác tương đối phổ biến trong các ứng dụng đi động. Android cũng hỗ trợ sẵn thao tác này thông qua các view TimePicker và DatePicker. TimePicker TimePicker cho phép người dùng chọn thời gian trong ngày (giờ, phút) theo cả 2 chế độ 24 giờ và 12 giờ với AM/PM. Giao diện trên bao gồm view chọn thời gian và nút bấm, trong hàm onClick xử lý sự kiện bấm của nút bấm này. Ta sẽ hiển thị thời gian đã được chọn trong TimePicker: public void onClick(View view) { Toast.makeText(getBaseContext(), "Time selected:" + timePicker.getCurrentHour() + ":" + timePicker.getCurrentMinute(), Toast.LENGTH_SHORT).show(); } Giao diện ứng dụng sau khi chạy sẽ như sau: TimePicker và DatePicker 97
  34. Giáo trình Android Hình: TimePicker với lựa chọn AM/PM Bấm nút “I am all set” , thời gian vừa được chọn sẽ được in ra màn hình dưới dạng Toast. Theo mặc định, TimePicker cho ta chọn thời gian dưới dạng 12 giờ với AM và PM. Để hiển thị thời gian dưới dạng 24 giờ, ta gọi hàm setIs24HourView như sau: timePicker = (TimePicker) findViewById(R.id.timePicker); timePicker.setIs24HourView(true); Kết quả thu được: TimePicker và DatePicker 98
  35. Giáo trình Android Hình: TimePicker với định dạng thời gian theo 24 giờ Tuy nhiên TimePicker tương đối tốn diện tích màn hình, vì vậy thông thường người ta hay để người dùng lựa chọn thời gian trong một cửa sổ riêng, sau đó cập nhật thời gian vừa lựa chọn vào giao diện chính dưới dạng view khác (TextView chẳng hạn). Để chọn thời gian trong hộp thoại, ta dùng TimePickerDialog cho người dùng chọn thời gian và viết sẵn một hàm lắng nghe sự kiện khi người dùng chọn xong: showDialog(TIME_DIALOG_ID); và: @Override protected Dialog onCreateDialog(int id) { switch (id) { case TIME_DIALOG_ID: return new TimePickerDialog( this, mTimeSetListener, hour, minute, false); } return null; } private TimePickerDialog.OnTimeSetListener mTimeSetListener = new TimePickerDialog.OnTimeSetListener() { public void onTimeSet( TimePicker view, int hourOfDay, int minuteOfHour) { hour = hourOfDay; minute = minuteOfHour; SimpleDateFormat timeFormat = new SimpleDateFormat("hh:mm aa"); TimePicker và DatePicker 99
  36. Giáo trình Android Date date = new Date(0,0,0, hour, minute); String strDate = timeFormat.format(date); Toast.makeText(getBaseContext(), "You have selected " + strDate, Toast.LENGTH_SHORT).show(); } }; Kết quả ta thu được như sau: Hình: TimePickerDialog được hiển thị dưới dạng hộp thoại DatePicker Tương tự như TimePicker để chọn thời gian, DatePicker được dùng để chọn ngày tháng với cách sử dụng tương tự. Sử dụng trong layout: <DatePicker android:id="@+id/datePicker" android:layout_width="wrap_content" TimePicker và DatePicker 100
  37. Giáo trình Android android:layout_height="wrap_content" /> Hình: TimePicker (trái) và DatePicker (phải) Hoặc sử dụng hộp thoại thông qua DatePickerDialog: showDialog(DATE_DIALOG_ID); và: @Override protected Dialog onCreateDialog(int id) { switch (id) { case DATE_DIALOG_ID: return new DatePickerDialog( this, mDateSetListener, yr, month, day); } return null; } private DatePickerDialog.OnDateSetListener mDateSetListener = new DatePickerDialog.OnDateSetListener() { public void onDateSet( DatePicker view, int year, int monthOfYear, int dayOfMonth) { yr = year; TimePicker và DatePicker 101
  38. Giáo trình Android month = monthOfYear; day = dayOfMonth; Toast.makeText(getBaseContext(), "You have selected : " + (month + 1) + "/" + day + "/" + year, Toast.LENGTH_SHORT).show(); } }; Hình: DatePickerDialog được hiển thị trong hộp thoại TimePicker và DatePicker 102
  39. Giáo trình Android Hiển thị ảnh với ImageView và Gallery Ảnh là đối tượng được sử dụng rất tích cực trong các ứng dụng hiện đại. Trong phần này ta sẽ tìm hiểu cách hiển thị ảnh trong Android với ImageView và hiển thị danh sách ảnh với Gallery view. Ta sẽ xem xét 2 loại view này trong một ví dụ tương đối điển hình: hiển thị danh sách ảnh cho phép người dùng cuộn và chọn ảnh cần xem. Ảnh được chọn sẽ được hiển thị to hơn ở bên dưới. Trước tiên ta chuẩn bị một số ảnh cho dự án này: Các ảnh này ta sẽ đặt vào thư mục res/ drawable-mdpi: Trong mã nguồn, các ảnh này sẽ được truy cập thông qua id của nó trong lớp R (resource): R.drawable.pic1, R.drawable.pic2, R.drawable.pic3, R.drawable.pic4, R.drawable.pic5, R.drawable.pic6, R.drawable.pic7 Hiển thị ảnh với ImageView và Gallery 103
  40. Giáo trình Android Layout của activity cho ví dụ này như sau: Mã nguồn của activity này như sau: public class GalleryActivity extends Activity { // the images to display Integer[] imageIDs = { R.drawable.pic1, R.drawable.pic2, R.drawable.pic3, R.drawable.pic4, R.drawable.pic5, R.drawable.pic6, R.drawable.pic7 }; / Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Gallery gallery = (Gallery) findViewById(R.id.gallery1); gallery.setAdapter(new ImageAdapter(this)); gallery.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView parent, View v, int position, long id) { Toast.makeText(getBaseContext(), "pic" + (position + 1) + " selected", Toast.LENGTH_SHORT).show(); // display the images selected ImageView imageView = (ImageView) findViewById(R.id.image1); imageView.setImageResource(imageIDs[position]); } }); } public class ImageAdapter extends BaseAdapter { Context context; int itemBackground; public ImageAdapter(Context c) { Hiển thị ảnh với ImageView và Gallery 104
  41. Giáo trình Android context = c; // setting the style TypedArray a = obtainStyledAttributes(R.styleable.Gallery1); itemBackground = a.getResourceId( R.styleable.Gallery1_android_galleryItemBackground, 0); a.recycle(); } // returns the number of images public int getCount() { return imageIDs.length; } // returns the item public Object getItem(int position) { return position; } // returns the ID of an item public long getItemId(int position) { return position; } // returns an ImageView view public View getView(int position, View convertView, ViewGroup parent) { ImageView imageView; if (convertView == null) { imageView = new ImageView(context); imageView.setImageResource(imageIDs[position]); imageView.setScaleType(ImageView.ScaleType.FIT_XY); imageView.setLayoutParams(new Gallery.LayoutParams(150, 120)); } else { imageView = (ImageView) convertView; } imageView.setBackgroundResource(itemBackground); return imageView; } } } Sau khi chạy ứng dụng, ta sẽ quan sát thấy danh sách các ảnh nhỏ ở phía trên cùng của activity, ta có thể cuộn ngang danh sách này để chọn ảnh muốn xem, ảnh được chọn sẽ luôn được căn giữa trong Gallery. Sau khi được chọn, ảnh to sẽ được hiển thị bên dưới (xem hình minh họa bên dưới). Hiển thị ảnh với ImageView và Gallery 105
  42. Giáo trình Android Hình: Gallery Ta cần giải thích thêm một chút về đoạn mã nguồn của Activity phía trên. Đầu tiên, ta lưu danh sách ID của các ảnh cần thêm vào Gallery: Integer[] imageIDs = { R.drawable.pic1, R.drawable.pic2, R.drawable.pic3, R.drawable.pic4, R.drawable.pic5, R.drawable.pic6, R.drawable.pic7 }; Sau đó, để thêm các ảnh này vào Gallery, ta cần một lớp trung gian, gọi là adapter. Adapter này sẽ cung cấp nguồn nội dung cho các đối tượng gồm nhiều phần tử như Gallery hay các danh sách. Ta sẽ xem chi tiết adapter này ở phía dưới. Để gắn adapter này vào gallery, ta dùng hàm setAdapter: gallery.setAdapter(new ImageAdapter(this)); Khi một ảnh trong gallery được chọn, ta sẽ hiển thị ảnh to phía dưới. Để làm việc này, ta cần thêm hàm xử lý sự kiện Hiển thị ảnh với ImageView và Gallery 106
  43. Giáo trình Android onItemClick của gallery: gallery.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView parent, View v, int position, long id) { Toast.makeText(getBaseContext(), "pic" + (position + 1) + " selected", Toast.LENGTH_SHORT).show(); // display the images selected ImageView imageView = (ImageView) findViewById(R.id.image1); imageView.setImageResource(imageIDs[position]); } }); Mỗi lớp adapter cho các view nhiều đối tượng (như gallery) cần kế thừa từ lớp BaseAdapter và cần nạp chồng các phương thức sau: public class ImageAdapter extends BaseAdapter { public ImageAdapter(Context c){ } public int getCount(){ } public Object getItem(int position) { } public long getItemId(int position) { } public View getView(int position, View convertView, ViewGroup parent) { } } Trong đó hàm getView trả về view của đối tượng con (item) trong danh sách, tại vị trí position. Một số view sử dụng Adapter làm nguồn dữ liệu trong Android có thể kể đến như: ListView: danh sách các đối tượng (theo chiều dọc) GridView: danh sách các đối tượng dạng bảng (nhiều cột, cuốn theo chiều dọc) Spinner: danh sách xổ xuống (giống khái niệm combo box trong lập trình web) Gallery: thư viện ảnh như ví dụ ở trên Android cũng định nghĩa sẵn một số lớp con của lớp BasicAdapter như: ListAdapter ArrayAdapter CursorAdapter SpinnerAdapter Chúng ta sẽ tìm hiểu một số lớp trong số đó trong phần còn lại của giáo trình. Hiển thị ảnh với ImageView và Gallery 107
  44. Giáo trình Android Sử dụng ListView để hiển thị danh sách dài Trong Android để hiển thị tập hợp nhiều phần tử cùng loại, ta dùng danh sách. Có 2 loại danh sách được định nghĩa sẵn là ListView và SpinnerView . Trong phần này ta sẽ lần lượt xem xét từng loại danh sách này. ListView ListView hiển thị danh sách các đối tượng con dưới dạng danh sách dọc, có khả năng cuộn khi chiều dài danh sách vượt quá chiều cao của view mẹ. Ta sẽ xem xét ListView trong trường hợp đơn giản nhất: hiển thị danh sách các phần tử dạng chữ. Trước tiên, ta chuẩn bị mảng dữ liệu các chữ cần hiển thị trong danh sách. Ta có thể nhập cứng (hard code) danh sách này trong mã nguồn java của Activity như sau: String[] presidents= { "Dwight D. Eisenhower", "John F. Kennedy", "Lyndon B. Johnson", "Richard Nixon", "Gerald Ford", "Jimmy Carter", "Ronald Reagan", "George H. W. Bush", "Bill Clinton", "George W. Bush", "Barack Obama" }; Tuy nhiên cách làm này làm cho mã nguồn rối hơn, gây khó khăn cho việc bảo trì, cũng như hạn chế khả năng địa phương hóa (thay đổi ngôn ngữ cho ứng dụng). Vì vậy, trong lập trình Android, phương pháp được khuyên dùng là định nghĩa các dữ liệu tĩnh này trong thư mục “res”. Cụ thể, đối với mảng chữ như trên, ta có thể định nghĩa trong file “res/values/string.xml” như sau: Hello World, BasicViews5Activity! BasicViews5 Dwight D. Eisenhower John F. Kennedy Lyndon B. Johnson Richard Nixon Gerald Ford Jimmy Carter Ronald Reagan George H. W. Bush Bill Clinton George W. Bush Barack Obama Sau đó, trong mã nguồn Java, ta có thể dễ dàng lấy ra mảng này bằng cách như sau: String[] presidents; presidents = getResources().getStringArray(R.array.presidents_array); Để tạo một danh sách, trước tiên ta khai báo 1 ListView trong file layout của Activity: Sử dụng ListView để hiển thị danh sách dài 108
  45. Giáo trình Android Đối với Activity có một ListView bên trong, Android định nghĩa sẵn lớp con của Activity là ListActivity giúp cho quá trình làm việc với ListView trở đơn giản hơn. Để có thể dụng được ListActivity này, có 2 việc cần phải làm: Activity phải kế thừa từ lớp android.app.ListActivity ListView trong file layout của Activity phải có id là "@+id/android:list" Mã nguồn của Activity sẽ như sau: public class BasicViews5Activity extends ListActivity { String[] presidents; / Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); presidents = getResources().getStringArray(R.array.presidents_array); setListAdapter(new ArrayAdapter (this, android.R.layout.simple_list_item_checked, presidents)); } @Override public void onListItemClick( ListView parent, View v, int position, long id) { Toast.makeText(this, "You have selected " + presidents[position], Toast.LENGTH_SHORT).show(); } } ListView trong Android cũng lấy nội dung từ một Adapter giống như trường hợp của Gallery ta đã làm quen trước đó. Đối với ListActivity, ta không cần lấy tham chiếu đến ListView một cách minh bạch, mà có thể gọi thẳng hàm setListAdapter để đặt adapter cho listView trong Activity (được đặt id theo quy định như đã nói ở trên). Để xử lý sự kiện người dùng bấm chọn một phần tử trong ListView, ta chỉ cần nạp chồng hàm onListItemClick như đoạn mã ở trên. Trong ví dụ này ta chỉ đơn thuần in ra chữ của phần tử được chọn. Activity trên khi chạy trên emulator sẽ có dạng như sau: Sử dụng ListView để hiển thị danh sách dài 109
  46. Giáo trình Android Hình: Hiển thị danh sách với ListView Ta có thể vuốt lên/xuống màn hình để xem toàn bộ danh sách. Có rất nhiều tùy chọn có thể làm với ListView. Bạn đọc tự tìm hiểu coi như bài tập. Ở đây ta chỉ nói thêm một tính khả năng của ListView cho phép lựa chọn nhiều phần tử cùng lúc (multi-item selection). Để bật tính năng này, ta chỉ cần gọi hàm setChoiceMode của ListView như sau: ListView lstView = getListView(); lstView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); ListView với thuộc tính lựa chọn nhiều phần tử được bật sẽ có dạng như sau: Sử dụng ListView để hiển thị danh sách dài 110
  47. Giáo trình Android Hình: ListView cho phép lựa chọn nhiều phần tử cùng lúc SpinnerView ListView rất tiện dụng cho việc hiển thị danh sách các phần tử đồng dạng. Tuy nhiên, ListView chiếm tương đối nhiều diện tích trên màn hình. Trong thực tế có nhiều trường hợp ta chỉ cần hiển thị phần tử đang chọn của danh sách, khi bấm vào phần tử này, sẽ hiện ra danh sách đầy đủ các phần tử còn lại để ta lựa chọn. Để làm được việc này, ta dùng SpinnerView. Để dễ hình dung, ta có thể nhiều SpinnerView chính là ComboBox trong lập trình web và Windows Form. Trong ví dụ dưới đây, ta vẫn hiển thị danh sách các phần tử dạng chữ như ví dụ trước, tuy nhiên dùng SpinerView thay cho ListView. Trước tiên, ta thêm khai báo một SpinnerView trong file layout của Activity: Sau đó, trong hàm onCreate của Activity, ta cần thêm mã nguồn để truy xuất đến SpinnerView này, đặt adapter cho nó và thêm hàm xử lý sự kiện khi ta chọn một phần tử của spinner: Sử dụng ListView để hiển thị danh sách dài 111
  48. Giáo trình Android presidents = getResources().getStringArray(R.array.presidents_array); Spinner s1 = (Spinner) findViewById(R.id.spinner1); ArrayAdapter adapter = new ArrayAdapter (this, android.R.layout.simple_spinner_item, presidents); s1.setAdapter(adapter); s1.setOnItemSelectedListener(new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) { int index = arg0.getSelectedItemPosition(); Toast.makeText(getBaseContext(), "You have selected item : " + presidents[index], Toast.LENGTH_SHORT).show(); } @Override public void onNothingSelected(AdapterView arg0) { } }); Khi một phần tử của SpinnerView được chọn, ta chỉ đơn thuần in lên màn hình thông báo dạng Toast. Chạy ứng dụng vừa tạo lên thiết bị hoặc emulator, spinner view của chúng ta sẽ có dạng như hình dưới đây: Hình: Spinner View Sử dụng ListView để hiển thị danh sách dài 112
  49. Giáo trình Android Hiển thị nội dung trang web với WebView WebView cho phép chúng ta nhúng trình duyệt web vào bên trong ứng dụng của mình. Sử dụng web view rất hữu dụng trong trường hợp chúng ta cần hiển thị nội dung một trang web bên trong ứng dụng, cũng như khi chúng ta muốn thiết kế một phần hoặc thậm chí toàn bộ giao diện bằng ngôn ngữ web quen thuộc (HTML5, Javascript, CSS). Trong ví dụ dưới đây, ta sẽ sử dụng web view để hiển thị nội dung của một trang web trên Internet, cụ thể ta sẽ hiển thị một biểu đồ từ dịch vụ Chart của Google. Trước tiên, ta cần thêm một WebView vào file layout của Activity: Sau đó trong hàm onCreate của Activity, ta cần thêm đoạn mã để cấu hình webview này hiển thị nội dung trang web từ Internet: WebView wv = (WebView) findViewById(R.id.webview1); WebSettings webSettings = wv.getSettings(); webSettings.setBuiltInZoomControls(true); wv.loadUrl( " " + "?chs=300x225" + "&cht=v" + "&chco=FF6342,ADDE63,63C6DE" + "&chd=t:100,80,60,30,30,30,10" + "&chdl=A|B|C"); Ngoài ra, do ứng dụng muốn truy cập vào Internet để lấy dữ liệu từ trang web của Google Chart, ta cần khai báo quyền truy cập Internet một cách tường minh trong file AndroidManifest.xml như sau: Chạy ứng dụng trên điện thoại hoặc Emulator, ta sẽ thấy một đồ thị được vẽ bởi dịch vụ Google Chart trên màn hình: Hiển thị nội dung trang web với WebView 113
  50. Giáo trình Android Hình: WebView hiển thị trang Web từ Internet Ngoài ra, WebView cũng có thể được dùng để hiển thị giao diện bằng HTML ta tự khai báo trong mã nguồn như ví dụ dưới đây: WebView wv = (WebView) findViewById(R.id.webview1); WebSettings webSettings = wv.getSettings(); webSettings.setBuiltInZoomControls(true); final String mimeType = "text/html"; final String encoding = "UTF-8"; String html = " A simple HTML page " + " The quick brown fox jumps over the lazy dog "; wv.loadDataWithBaseURL("", html, mimeType, encoding, ""); Chạy ứng dụng, ta sẽ thấy giao diện tương tự như minh họa trong hình sau: Hiển thị nội dung trang web với WebView 114
  51. Giáo trình Android Hình: WebView hiển thị trực tiếp mã HTML Hiển thị nội dung trang web với WebView 115
  52. Giáo trình Android Lưu trữ dữ liệu Trong chương này chúng ta sẽ xem xét cách thức lưu trữ dữ liệu trong Android. Lưu trữ dữ liệu là tính năng quan trọng đối với ứng dụng, giúp cho người dùng có thể dùng lại được những dữ liệu trước đó mà không cần nhập lại. Trong Android có 3 cách để lưu lại dữ liệu: Cơ chế “cấu hình chia sẻ” (shared preferences) được dùng để lưu những dữ liệu nhỏ dưới dạng key-value (tên khóa – giá trị khóa) Lưu dữ liệu cố định vào tập tin trong bộ nhớ trong hoặc bộ nhớ ngoài của điện thoại Lưu dữ liệu sử dụng cơ sở dữ liệu quan hệ cục bộ SQLite Chúng ta sẽ lần lượt duyệt qua các phương pháp kê trên trong chương này. Lưu trữ dữ liệu 116
  53. Giáo trình Android Lưu trữ dữ liệu cố định với shared preferences Android cung cấp sẵn một cơ chế đơn giản giúp chúng ta lưu trữ nhanh các dữ liệu ngắn như cấu hình ứng dụng, tên đăng nhập, email và lấy lại dữ liệu đã ghi này trong các lần chạy ứng dụng tiếp theo. Cách thức thuận tiện nhất là mô tả tập trung các cấu hình ta cần lưu lại trong một Activity (thường là màn hình “Settings” của ứng dụng). Android cung cấp sẵn một loại Activity đặc biệt là PreferenceActivity giúp đơn giản hóa quá trình này. Ví dụ dưới đây minh họa cách sử dụng Activity này. Để sử dụng được PreferenceActivity, trước hết ta mô tả các thông tin cần lưu lại trong một tài liệu xml nằm trong thư mục “res/xml”. Trong ví dụ này, ta tạo file “res/xml/myapppreferences.xml” với nội dung như sau: Sau đó, ta cần tạo một Activity kế thừa từ PreferenceActivity với nội dung như sau: import android.os.Bundle; import android.preference.PreferenceActivity; public class AppPreferenceActivity extends PreferenceActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); PreferenceManager prefMgr = getPreferenceManager(); prefMgr.setSharedPreferencesName("appPreferences"); // load the preferences from an XML file addPreferencesFromResource(R.xml.myapppreferences); } } Lưu trữ dữ liệu cố định với shared preferences 117
  54. Giáo trình Android Chạy ứng dụng với Activity như trên, ta sẽ có ngay một màn hình settings cho ứng dụng: Nội dung của màn hình này được mô tả hoàn toàn trong file xml ở trên, trong đó chia các cấu hình này thành 2 danh mục (category 1 và category 2) và một số loại cấu hình khác nhau như checkbox, edittext, nhạc chuông và một ví dụ minh họa cho việc mở thêm màn hình cấu hình thứ 2 (khi màn hình đầu có nhiều chi tiết). Bấm vào edittext, sẽ có cửa sổ popup cho bạn nhập giá trị cần lưu lại: Lưu trữ dữ liệu cố định với shared preferences 118
  55. Giáo trình Android Hình: Cửa sổ popup cho phép nhập cấu hình dạng text (EditText) Cấu hình nhạc chuông: Lưu trữ dữ liệu cố định với shared preferences 119
  56. Giáo trình Android Hình: Cửa sổ popup cho phép lựa chọn cấu hình từ danh sách các RadioButton Màn hình preference thứ 2: Lưu trữ dữ liệu cố định với shared preferences 120
  57. Giáo trình Android Hình: Màn hình cấu hình mở rộng Sau khi bạn thay đổi giá trị của các mục cấu hình trong Activity này, hệ thống sẽ tự động lưu lại giá trị của chúng để có thể sử dụng được trong các lần tiếp theo. Việc lưu trữ dữ liệu này là trong suốt với người dùng. Tuy nhiên với máy ảo Android ta có thể xem được cụ thể các giá trị này. Trên thực tế, chúng được lưu trong 1 file xml nằm trên bộ nhớ trong, trong vùng nhớ chỉ có thể truy cập được bởi ứng dụng tạo ra nó (thư mục /data/data/{package-name}/shared_prefs/appPreferences.xml): Nội dung file này có dạng như sau: Lưu trữ dữ liệu cố định với shared preferences 121
  58. Giáo trình Android HUMG - Software engineer HUMG - 2nd screen text content://settings/system/ringtone Để lấy giá trị của các cấu hình này trong code, ta làm như sau: SharedPreferences appPrefs = getSharedPreferences("appPreferences", MODE_PRIVATE); Toast.makeText(this, appPrefs.getString("editTextPref", ""), Toast.LENGTH_LONG).show(); Trong đó, appPreferences là tên của file cấu hình cần mở (được đặt ở hàm prefMgr.setSharedPreferencesName("appPreferences") ; phía trên), còn editTextPref là tên của thuộc tính cần lấy giá trị (được đặt trong file res/xml/myappprefererences.xml ở trên). Ngoài ra, ta cũng có thể đặt lại giá trị của các cấu hình này bằng tay mà không cần thông qua PreferenceActivity bằng cách sử dụng SharedPreferences.Editor như sau: SharedPreferences appPrefs = getSharedPreferences("appPreferences", MODE_PRIVATE); SharedPreferences.Editor prefsEditor = appPrefs.edit(); prefsEditor.putString("editTextPref", ((EditText) findViewById(R.id.txtString)).getText().toString()); prefsEditor.commit(); Sau khi thay đổi giá trị của các thuộc tính cần thay đổi, ta cần gọi phương thức commit() của lớp Editor này để các thay đổi có hiệu lực (tiến hành ghi vào file xml trong bộ nhớ trong như mô tả ở trên). Lưu trữ dữ liệu cố định với shared preferences 122
  59. Giáo trình Android Lưu trữ dữ liệu bằng file trên bộ nhớ trong và bộ nhớ ngoài Trong trường hợp cần lưu lại dữ liệu phức tạp hơn (khó có thể lưu lại dạng key-value trong shared preference), ta có thể dùng hệ thống file. Trong Android, để làm việc (nhập/xuất) với file, ta có thể dùng các lớp của gói java.io . Trong phần này, ta sẽ xem cách làm việc với file trong bộ nhớ trong lẫn bộ nhớ ngoài. Làm việc với file trong bộ nhớ trong Ta sẽ tạo một Activity có một ô nhập văn bản (EditText) và 2 nút bấm cho phép ghi và đọc văn bản này vào file. Layout của Activity này như sau: Mã nguồn của Activity với 2 hàm đọc ( onClickLoad ) và ghi ( onClickSave ) vào file như sau: import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.Toast; public class FilesActivity extends Activity { EditText textBox; static final int READ_BLOCK_SIZE = 100; / Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Lưu trữ dữ liệu với file trên bộ nhớ trong và bộ nhớ ngoài 123
  60. Giáo trình Android textBox = (EditText) findViewById(R.id.txtText1); } public void onClickSave(View view) { String str = textBox.getText().toString(); try { FileOutputStream fOut = openFileOutput("textfile.txt", MODE_PRIVATE); OutputStreamWriter osw = new OutputStreamWriter(fOut); // write the string to the file osw.write(str); osw.flush(); osw.close(); // display file saved message Toast.makeText(getBaseContext(), "File saved successfully!", Toast.LENGTH_SHORT).show(); // clears the EditText textBox.setText(""); } catch (IOException ioe) { ioe.printStackTrace(); } } public void onClickLoad(View view) { try { FileInputStream fIn = openFileInput("textfile.txt"); InputStreamReader isr = new InputStreamReader(fIn); char[] inputBuffer = new char[READ_BLOCK_SIZE]; String s = ""; int charRead; while ((charRead = isr.read(inputBuffer))>0) { // convert the chars to a String String readString = String.copyValueOf(inputBuffer, 0, charRead); s += readString; inputBuffer = new char[READ_BLOCK_SIZE]; } // set the EditText to the text that has been // read textBox.setText(s); Toast.makeText(getBaseContext(), "File loaded successfully!", Toast.LENGTH_SHORT).show(); } catch (IOException ioe) { ioe.printStackTrace(); } } } Trong đoạn mã trên, ta thấy việc đọc và ghi vào file tương đối đơn giản và quen thuộc. Để ghi dữ liệu vào file, ta tạo một đối tượng OutputStreamWriter trên luồng xuất FileOutputStream và tiến hành ghi vào qua phương thức write. Sau đó, gọi flush để đẩy hết dữ liệu trong bộ đệm vào file và đóng luồng lại: Lưu trữ dữ liệu với file trên bộ nhớ trong và bộ nhớ ngoài 124
  61. Giáo trình Android FileOutputStream fOut = openFileOutput("textfile.txt", MODE_PRIVATE); OutputStreamWriter osw = new OutputStreamWriter(fOut); // write the string to the file osw.write(str); osw.flush(); osw.close(); Để đọc nội dung file này, ta cũng thao tác tương tự. Tạo đối tượng InputStreamReader từ luồng nhập liệu ( FileInputStream ) và tiến hành đọc dữ liệu bằng phương thức read() . Mỗi lần đọc sẽ đọc READ_BLOCK_SIZE byte và lưu vào bộ đệm (mảng byte). Nội dung này sẽ được chuyển thành string và nối thêm vào biến s . Quá trình được lặp lại cho đến khi hết nội dung của file: FileInputStream fIn = openFileInput("textfile.txt"); InputStreamReader isr = new InputStreamReader(fIn); char[] inputBuffer = new char[READ_BLOCK_SIZE]; String s = ""; int charRead; while ((charRead = isr.read(inputBuffer))>0) { String readString = String.copyValueOf(inputBuffer, 0, charRead); s += readString; inputBuffer = new char[READ_BLOCK_SIZE]; } textBox.setText(s); Một câu hỏi đặt ra là file "textfile.txt" ở trên được tạo ra ở đâu trong cây thư mục. Câu trả lời là nó được tạo ra trong bộ nhớ trong của thiết bị, trong thư mục dành riêng cho ứng dụng (/data/data/{package-name}/files) Chạy ứng dụng, nhập nội dung cho ô nhập liệu và bấm Save, ta sẽ thấy nội dung văn bản này được ghi vào file trong bộ nhớ trong. Lưu trữ dữ liệu với file trên bộ nhớ trong và bộ nhớ ngoài 125
  62. Giáo trình Android Hình: Giao diện nhập/hiển thị dữ liệu đọc và ghi file Hình: Vị trí file dữ liệu vừa ghi trong bộ nhớ thiết bị Kéo file này về máy tính để xem nội dung file, ta sẽ thấy nội dung văn bản ta nhập vào trước đó Lưu trữ dữ liệu với file trên bộ nhớ trong và bộ nhớ ngoài 126
  63. Giáo trình Android Hình: Nội dung file dữ liệu vừa ghi trong bộ nhớ thiết bị Làm việc với file trong bộ nhớ ngoài File trong bộ nhớ trong chỉ được truy cập bởi ứng dụng tạo ra nó. Ngoài ra, dung lượng lưu trữ của bộ nhớ trong thường hạn chế hơn bộ nhớ ngoài (SDCard). Vì vậy, trong trường hợp ta muốn chia sẻ thông tin lưu trữ với các ứng dụng khác, ta nên sử dụng bộ nhớ ngoài. Làm việc với file trong bộ nhớ ngoài hoàn toàn tương tự với file trong bộ nhớ trong, chỉ khác phần lấy ra FileInputStream và FileOutputStream : Thay vì dùng: FileOutputStream fOut = openFileOutput("textfile.txt", MODE_PRIVATE); Ta dùng: File sdCard = Environment.getExternalStorageDirectory(); File directory = new File(sdCard.getAbsolutePath() + "/MyFiles"); directory.mkdirs(); File file = new File(directory, "textfile.txt"); FileOutputStream fOut = new FileOutputStream(file); Và thay vì: FileInputStream fIn = openFileInput("textfile.txt"); Ta dùng: File sdCard = Environment.getExternalStorageDirectory(); File directory = new File (sdCard.getAbsolutePath() + "/MyFiles"); File file = new File(directory, "textfile.txt"); FileInputStream fIn = new FileInputStream(file); InputStreamReader isr = new InputStreamReader(fIn); Mọi thao tác vẫn như trường hợp trước, chỉ khác bị trí lưu trữ file trong cây thư mục: Lưu trữ dữ liệu với file trên bộ nhớ trong và bộ nhớ ngoài 127
  64. Giáo trình Android Hình: Vị trí file dữ liệu vừa ghi trên bộ nhớ ngoài của thiết bị Lưu trữ dữ liệu với file trên bộ nhớ trong và bộ nhớ ngoài 128
  65. Giáo trình Android Cơ sở dữ liệu SQLite trong ứng dụng Android Trong phần trước, ta đã tìm hiểu cách lưu dữ liệu vào file và vào shared preferences. Tuy nhiên, với loại dữ liệu quan hệ thì việc sử dụng cơ sở dữ liệu quan hệ sẽ thuận tiện hơn rất nhiều. Ví dụ, ta cần lưu trữ kết quả kiểm tra của các sinh viên trong trường học. Dùng cơ sở dữ liệu sẽ cho phép chúng ta truy vấn kết quả của tập sinh viên nhất định theo các tiêu chí khác nhau. Việc thêm, bớt, thay đổi thông tin thông qua các câu truy vấn SQL cũng dễ dàng hơn nhiều so với việc thao tác trên file. Android sử dụng hệ cơ sở dữ liệu SQLite. CSDL do một ứng dụng tạo ra sẽ chỉ được truy xuất bởi ứng dụng đó, và file CSDL sẽ nằm trong bộ nhớ trong dành riêng cho ứng dụng (/data/data/{package-name}/databases/). Một thói quen tốt thường được các lập trình viên kinh nghiệm sử dụng là tập trung tất cả mã lệnh truy cập đến CSDL vào một lớp riêng để thao tác trên CSDL trở nên trong suốt với môi trường ngoài. Chúng ta sẽ tạo trước một lớp như vậy, gọi là DBAdapter. Tạo lớp DBAdapter Trong ví dụ này ta sẽ tạo một CSDL tên là MyDB, chứa một bảng duy nhất là contacts. Bảng này chứa các trường _id, name và email. Lớp DBAdapter có mã nguồn như sau: import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; public class DBAdapter { static final String KEY_ROWID = "_id"; static final String KEY_NAME = "name"; static final String KEY_EMAIL = "email"; static final String TAG = "DBAdapter"; static final String DATABASE_NAME = "MyDB"; static final String DATABASE_TABLE = "contacts"; static final int DATABASE_VERSION = 2; static final String DATABASE_CREATE = "create table contacts (_id integer primary key autoincrement, " + "name text not null, email text not null);"; final Context context; DatabaseHelper DBHelper; SQLiteDatabase db; public DBAdapter(Context ctx) { this.context = ctx; DBHelper = new DatabaseHelper(context); } private static class DatabaseHelper extends SQLiteOpenHelper { DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { try { db.execSQL(DATABASE_CREATE); } catch (SQLException e) { e.printStackTrace(); } CSDL SQLite trong ứng dụng Android 129
  66. Giáo trình Android } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data"); db.execSQL("DROP TABLE IF EXISTS contacts"); onCreate(db); } } // opens the database public DBAdapter open() throws SQLException { db = DBHelper.getWritableDatabase(); return this; } // closes the database public void close() { DBHelper.close(); } // insert a contact into the database public long insertContact(String name, String email) { ContentValues initialValues = new ContentValues(); initialValues.put(KEY_NAME, name); initialValues.put(KEY_EMAIL, email); return db.insert(DATABASE_TABLE, null, initialValues); } // deletes a particular contact public boolean deleteContact(long rowId) { return db.delete(DATABASE_TABLE, KEY_ROWID + "=" + rowId, null) > 0; } // retrieves all the contacts public Cursor getAllContacts() { return db.query(DATABASE_TABLE, new String[] {KEY_ROWID, KEY_NAME, KEY_EMAIL}, null, null, null, null, null); } // retrieves a particular contact public Cursor getContact(long rowId) throws SQLException { Cursor mCursor = db.query(true, DATABASE_TABLE, new String[] {KEY_ROWID, KEY_NAME, KEY_EMAIL}, KEY_ROWID + "=" + rowId, null, null, null, null, null); if (mCursor != null) { mCursor.moveToFirst(); } return mCursor; } // updates a contact public boolean updateContact(long rowId, String name, String email) { ContentValues args = new ContentValues(); args.put(KEY_NAME, name); args.put(KEY_EMAIL, email); return db.update(DATABASE_TABLE, args, KEY_ROWID + "=" + rowId, null) > 0; } } Trước tiên ta khai báo các hằng số như: tên CSDL, tên bảng, tên các trường để dễ dàng truy xuất và thay đổi trong quá trình phát triển. Ngoài ra, ta cũng khai báo phiên bản (do ta tự đánh số) của CSDL trong ứng dụng và viết sẵn câu truy vấn dùng để tạo CSDL: CSDL SQLite trong ứng dụng Android 130
  67. Giáo trình Android static final String KEY_ROWID = "_id"; static final String KEY_NAME = "name"; static final String KEY_EMAIL = "email"; static final String TAG = "DBAdapter"; static final String DATABASE_NAME = "MyDB"; static final String DATABASE_TABLE = "contacts"; static final int DATABASE_VERSION = 2; static final String DATABASE_CREATE = "create table contacts (_id integer primary key autoincrement, " + "name text not null, email text not null);"; Ta cũng tạo thêm một lớp cục bộ (lớp DatabaseHelper ở trên) để trợ giúp cho việc tạo CSDL và nâng cấp cấu trúc khi có sự thay đổi trong các phiên bản tiếp theo. Lớp này kế thừa từ lớp SQLiteOpenHelper . Hàm dựng của lớp này sẽ gọi về hàm dựng của lớp mẹ với tên và phiên bản CSDL của ứng dụng: super(context, DATABASE_NAME, null, DATABASE_VERSION); Ngoài ra, trong lớp này ta nạp chồng 2 hàm: Hàm onCreate() : được gọi để khởi tạo CSDL trong lần đầu tiên chạy ứng dụng. Trong hàm này ta tiến hành thực thi câu lệnh tạo CSDL ở trên (DATABASE_CREATE) Hàm onUpgrade() : được gọi khi ta nâng cấp ứng dụng và thay đổi giá trị của phiên bản CSDL (DATABASE_VERSION) ở trên. Trong ví dụ ở trên, khi có thay đổi phiên bản này, ta xóa CSDL cũ và tạo lại cái mới. Ngoài ra, ta cũng viết thêm các hàm để mở CSDL, tạo mới bản ghi, cập nhật bản ghi, lấy tất cả bản ghi, lấy bản ghi theo id (xem đoạn mã ở trên). Sau khi có lớp DBAdapter này, việc truy xuất CSDL trở nên tương đối đơn giản, đoạn mã dưới đây minh họa các thao tác thêm, bớt, truy vấn CSDL: DBAdapter db = new DBAdapter(this); // thêm một bản ghi db.open(); long id = db.insertContact("Wei-Meng Lee", "weimenglee@learn2develop.net"); id = db.insertContact("Mary Jackson", "mary@jackson.com"); db.close(); // lấy danh sách tất cả bản ghi db.open(); Cursor c = db.getAllContacts(); if (c.moveToFirst()) { do { DisplayContact(c); } while (c.moveToNext()); } db.close(); // lấy một bản ghi theo id db.open(); c = db.getContact(2); if (c.moveToFirst()) DisplayContact(c); else Toast.makeText(this, "No contact found", Toast.LENGTH_LONG).show(); db.close(); // cập nhật bản ghi db.open(); if (db.updateContact(1, "Wei-Meng Lee", "weimenglee@gmail.com")) Toast.makeText(this, "Update successful.", Toast.LENGTH_LONG).show(); else Toast.makeText(this, "Update failed.", Toast.LENGTH_LONG).show(); db.close(); CSDL SQLite trong ứng dụng Android 131
  68. Giáo trình Android // xóa bản ghi db.open(); if (db.deleteContact(1)) Toast.makeText(this, "Delete successful.", Toast.LENGTH_LONG).show(); else Toast.makeText(this, "Delete failed.", Toast.LENGTH_LONG).show(); db.close(); Trong đó hàm DisplayContact(c) chỉ đơn giản hiển thị lên màn hình nội dung bản ghi dưới dạng Toast: public void DisplayContact(Cursor c) { Toast.makeText(this, "id: " + c.getString(0) + "\n" + "Name: " + c.getString(1) + "\n" + "Email: " + c.getString(2), Toast.LENGTH_LONG).show(); } Chạy ứng dụng và dùng trình duyệt file của Emulator, ta có thể thấy file CSDL được tạo ra trong thư mục /data/data/{package-name}/databases/myDB: Hình: Vị trí file CSDL trong bộ nhớ của thiết bị Nếu ta lấy file này về máy tính và đọc nó bằng các phần mềm hỗ trợ CSDL SQLite (như NaviCat) ta sẽ thấy được nội dung bảng contacts bên trong. CSDL SQLite trong ứng dụng Android 132
  69. Giáo trình Android Lập trình mạng với Android Trong các ứng dụng hiện đại, nhu cầu giao tiếp giữa ứng dụng với thế giới bên ngoài (Internet) hết sức phổ biến, từ việc lấy và cập nhật nội dung trực tuyến (từ các web site, các web service), tương tác với máy chủ, cho đến lập trình socket để mở kết nối cố định đến ứng dụng trên server. Trong chương này, ta sẽ tìm hiểu cách thức tải dữ liệu dạng nhị phân cũng như dạng văn bản từ máy chủ web thông qua giao thức HTTP, cũng như việc phân tích cú pháp dữ liệu nhận về dạng XML hoặc JSON để lấy ra thông tin cần thiết. Việc lập trình socket cần thêm ứng dụng phía server, nằm ngoài phạm vi của giáo trình, nên sẽ không được đề cập ở đây, bạn đọc quan tâm có thể tự tìm hiểu thêm trong các tài liệu khác. Lập trình mạng với Android 133
  70. Giáo trình Android Sử dụng các dịch vụ web services thông qua giao thức HTTP Cách thức phổ biến nhất để cập nhật dữ liệu trực tuyến là lấy dữ liệu từ trang web trên Internet thông qua giao thức HTTP. Sử dụng giao thức này, ta có thể thực hiện rất nhiều việc trao đổi dữ liệu với thế giới bên ngoài, từ việc lấy nội dung trang web, tải dữ liệu nhị phân (file nhị phân, ảnh ), tải dữ liệu dạng văn bản Về mặt kỹ thuật, các thao tác này là như nhau cho mọi loại dữ liệu tải về. Vì vậy, trước tiên ta tạo một dự án khung cho các ứng dụng sử dụng tài nguyên mạng theo giao thức HTTP. Trước hết, ta cần yêu cầu quyền truy cập Internet cho ứng dụng một cách tường minh trong file AndroidManifest.xml như sau: Sau đó, trong mã nguồn của Activity chính, ta viết thêm một hàm để lấy dữ liệu từ trên mạng về qua giao thức HTTP. Kết quả của việc lấy dữ liệu này sẽ cho ta một luồng nhập liệu ( InputStream ) để ta xử lý dữ liệu (việc lấy dữ liệu ra loại gì – nhị phân hay văn bản sẽ do luồng nhập liệu này xử lý). Mã nguồn của Activity ban đầu sẽ như sau: public class NetworkingActivity extends Activity { private InputStream OpenHttpConnection(String urlString) throws IOException { InputStream in = null; int response = -1; URL url = new URL(urlString); URLConnection conn = url.openConnection(); if (!(conn instanceof HttpURLConnection)) throw new IOException("Not an HTTP connection"); try{ HttpURLConnection httpConn = (HttpURLConnection) conn; httpConn.setAllowUserInteraction(false); httpConn.setInstanceFollowRedirects(true); httpConn.setRequestMethod("GET"); httpConn.connect(); response = httpConn.getResponseCode(); if (response == HttpURLConnection.HTTP_OK) { in = httpConn.getInputStream(); Sử dụng web services thông qua giao thức HTTP 134
  71. Giáo trình Android } } catch (Exception ex) { Log.d("Networking", ex.getLocalizedMessage()); throw new IOException("Error connecting"); } return in; } / Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } } Hàm OpenHttpConnection ở trên nhận vào đường link đến tài nguyên trên Internet, thực hiện kết nối và trả dữ liệu về vào luồng nhập liệu ( InputStream ). Do việc làm việc với tài nguyên mạng thường xảy ra lỗi (không kết nối được, link không còn tồn tại ), toàn bộ đoạn mã cho việc này nằm trong bộ xử lý nhận ngoại lệ try-catch để bắt các lỗi có thể xảy ra. Lưu ý là việc kết nối đến máy chủ để lấy tài nguyên có thể kéo dài, tùy thuộc vào kích thước tài nguyên, tốc độ đường truyền, khả năng xử lý của thiết bị Vì vậy, hàm trên thường được gọi trong thread riêng để tránh việc khóa cứng giao diện người dùng trong quá trình xử lý. Ta sẽ làm việc này trong các ví dụ cụ thể bên dưới với việc tải dữ liệu nhị phân cũng như tải dữ liệu văn bản. Sử dụng web services thông qua giao thức HTTP 135
  72. Giáo trình Android Tải dữ liệu nhị phân thông qua HTTP Trong phần này, ta sẽ minh họa một trường hợp tải dữ liệu nhị phân từ máy chủ web thông qua giao thức HTTP. Ta sẽ dùng ứng dụng khung về sử dụng tài nguyên qua HTTP ở trên để tải một ảnh (ảnh là dữ liệu nhị phân) từ trên mạng về và hiển thị bên trong Activity. Trước tiên, ta thêm một đối tượng ImageView vào file layout của Activity để hiển thị ảnh khi được tải về: Như đã nói ở trên, việc tải tài nguyên từ mạng phải nằm trong thread riêng, khác với thread vẽ giao diện ứng dụng (UI thread). Ở đây ta dùng AsyncTask cho việc này: private Bitmap DownloadImage(String URL) { Bitmap bitmap = null; InputStream in = null; try { in = OpenHttpConnection(URL); bitmap = BitmapFactory.decodeStream(in); in.close(); } catch (IOException e1) { Log.d("NetworkingActivity", e1.getLocalizedMessage()); } return bitmap; } private class DownloadImageTask extends AsyncTask { protected Bitmap doInBackground(String urls) { return DownloadImage(urls[0]); } protected void onPostExecute(Bitmap result) { ImageView img = (ImageView) findViewById(R.id.img); img.setImageBitmap(result); } } Hàm DownloadImage thực hiện tải ảnh từ trên mạng về và lưu vào một đối tượng Bitmap (đối tượng này sẵn sàng để đưa vào ImageView). Hàm này sử dụng hàm OpenHttpConnection đã viết ở trên để lấy một luồng nhập liệu chứa dữ liệu ảnh này, sau đó dùng hàm tĩnh decodeStream của lớp BitmapFactory để giải mã luồng nhập liệu này và lưu vào đối tượng Bitmap. Tuy nhiên, hàm DownloadImage này cũng sẽ chiếm nhiều thời gian (bằng thời gian tải ảnh về của hàm OpenHttpConnection và thời gian giải mã ảnh). Vì vậy ta cần gọi hàm này bên trong một thread khác. Trong ví dụ trên ta dùng AsyncTask cho việc này. Để sử dụng AsyncTask , ta cần nạp chồng tối thiểu 2 hàm: doInBackground - hàm này được thực hiện trong thread riêng. Khi kết thúc hàm này, nó sẽ tự động gọi hàm onPostExecute trong UI thread. Đây là nơi ta sẽ thực hiện các thao tác tốn thời gian. onPostExecute hàm này được gọi trong UI thread, sau khi công việc ngầm ở hàm trên đã kết thúc. Đây là nơi ta cập Tải dữ liệu nhị phân thông qua HTTP 136
  73. Giáo trình Android nhật lại UI từ dữ liệu được tải về. Trong ví dụ trên, ta tiến hành tải ảnh trong thread riêng (trong hàm doInBackground ) và tiến hành hiển thị ảnh mới được tải về trong hàm onPostExecute . Cuối cùng, để tiến hành tải ảnh, ta thêm dòng mã gọi đến AsyncTask này trong hàm onCreate của Activity: @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); new DownloadImageTask().execute(" "); } Chạy ứng dụng trên điện thoại hoặc Android Emulator, ta thấy ảnh từ mạng sẽ được hiển thị trên màn hình (có thể phải chờ một lúc nếu kết nối mạng không tốt) như hình minh họa bên dưới: Hình: Hình ảnh được tải từ Internet thông qua HTTP Tải dữ liệu nhị phân thông qua HTTP 137
  74. Giáo trình Android Tải dữ liệu dạng text thông qua HTTP Tương tự như việc tải dữ liệu nhị phân ở trên, trong nhiều trường hợp ta cần tải dữ liệu dạng văn bản từ mạng (lấy dữ liệu từ các web service chẳng hạn). Ta cũng sẽ thực hiện thao tác này trong thread riêng, sử dụng AsyncTask và trả về dữ liệu dạng String trước khi hiển thị lên màn hình (dạng Toast). Đoạn mã tải và hiển dữ liệu văn bản trong thread riêng như sau: private String DownloadText(String URL) { int BUFFER_SIZE = 2000; InputStream in = null; try { in = OpenHttpConnection(URL); } catch (IOException e) { Log.d("NetworkingActivity", e.getLocalizedMessage()); return ""; } InputStreamReader isr = new InputStreamReader(in); int charRead; String str = ""; char[] inputBuffer = new char[BUFFER_SIZE]; try { while ((charRead = isr.read(inputBuffer))>0) { // convert the chars to a String String readString = String.copyValueOf(inputBuffer, 0, charRead); str += readString; inputBuffer = new char[BUFFER_SIZE]; } in.close(); } catch (IOException e) { Log.d("NetworkingActivity", e.getLocalizedMessage()); return ""; } return str; } private class DownloadTextTask extends AsyncTask { protected String doInBackground(String urls) { return DownloadText(urls[0]); } @Override protected void onPostExecute(String result) { Toast.makeText(getBaseContext(), result, Toast.LENGTH_LONG).show(); } } Đoạn mã trên chỉ khác với việc tải ảnh trong phần trước ở đoạn đọc dữ liệu từ luồng nhập liệu trả về. Trong trường hợp dữ liệu văn bản, ta dùng lớp InputStreamReader và hàm read của nó để đọc lần lượt dữ liệu dạng char ra, sau đó gắn thêm dần vào string kết quả. Cuối cùng, ta chỉ cần gọi AsyncTask mới tạo ra này trong hàm onCreate của Activity để xem kết quả: @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // download text new DownloadTextTask().execute( " "); } Tải dữ liệu dạng text thông qua HTTP 138
  75. Giáo trình Android Chạy ứng dụng, ta sẽ thấy dữ liệu từ web service trên được in ra màn hình: Hình: Dữ liệu dạng văn bản (XML) được tải từ Internet qua HTTP Tải dữ liệu dạng text thông qua HTTP 139
  76. Giáo trình Android Web service với dữ liệu XML Trong phần trên ta đã tiến hành lấy dữ liệu văn bản từ web service thông qua giao thức HTTP. Tuy nhiên trong ví dụ trước ta cũng thấy dữ liệu trả về có thể ở dạng XML. Đây là định dạng tương đối phổ biến của các web service hiện nay (bên cạnh JSON), và để có thể sử dụng được dữ liệu này trong ứng dụng, ta cần phân tích cú pháp (parsing) của dữ liệu XML này. Trong phần này ta sẽ xem xét cách thức phân tích tài liệu XML trong Android. Trong ví dụ này, ta sẽ thực hiện tra nghĩa của một từ tiếng anh trên từ điển trực tuyến của Aonaware qua web service. Để thực hiện tra từ, ta gọi API như sau: {từ-cần-tra} . Ví dụ để tra nghĩa của từ “apple”, ta cần gọi API như sau: Nếu bạn mở link này vào trình duyệt web, ta sẽ thấy nội dung web service này trả về dưới dạng XML như sau: Nhiệm vụ của chúng ra là viết đoạn mã phân tích tài liệu XML trên để lấy ra phần nghĩa của từ (trong thẻ WordDefinition ) và hiển thị lên màn hình. Để thực hiện việc này, ta cũng cần khai báo một AsyncTask để tránh khóa cứng UI thread: private String WordDefinition(String word) { InputStream in = null; String strDefinition = ""; Web service với dữ liệu XML 140
  77. Giáo trình Android try { in = OpenHttpConnection(" " + word); Document doc = null; DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db; try { db = dbf.newDocumentBuilder(); doc = db.parse(in); } catch (ParserConfigurationException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } doc.getDocumentElement().normalize(); // retrieve all the elements NodeList definitionElements = doc .getElementsByTagName("Definition"); // iterate through each elements for (int i = 0; i elements under // the element NodeList wordDefinitionElements = (definitionElement) .getElementsByTagName("WordDefinition"); strDefinition = ""; // iterate through each elements for (int j = 0; j node into an Element Element wordDefinitionElement = (Element) wordDefinitionElements .item(j); // get all the child nodes under the // element NodeList textNodes = ((Node) wordDefinitionElement) .getChildNodes(); strDefinition += ((Node) textNodes.item(0)) .getNodeValue() + ". \n"; } } } } catch (IOException e1) { Log.d("NetworkingActivity", e1.getLocalizedMessage()); } // return the definitions of the word return strDefinition; } private class AccessWebServiceTask extends AsyncTask { protected String doInBackground(String urls) { return WordDefinition(urls[0]); } protected void onPostExecute(String result) { Toast.makeText(getBaseContext(), result, Toast.LENGTH_LONG).show(); } } Trong đó hàm WordDefinition sẽ tải nội dung của webservice và tiến hành phân tích cú pháp tài liệu XML trả về. Công việc này tốn thời gian nên ta cần làm trong thread riêng, do đó ta cần viết thêm lớp AccessWebServiceTask như ở trên. Việc cuối cùng là gọi AsyncTask này trong hàm onCreate của Activity: Web service với dữ liệu XML 141
  78. Giáo trình Android / Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // access a Web Service using GET new AccessWebServiceTask().execute("apple"); } Ứng dụng khi chạy sẽ có dạng như minh họa trong hình bên dưới. Hình: Dữ liệu XML đã được phân tích Ta sẽ đi chi tiết hơn một chút về hàm WordDefinition ở trên. Trước hết, ta vẫn sử dụng hàm OpenHttpConnection đã viết ở trên để lấy thông tin từ mạng và đưa vào luồng nhập liệu: in = OpenHttpConnection( + word); Sau đó ta dùng lớp javax.xml.parsers.DocumentBuilder để phân tích tài liệu XML trong luồng nhập liệu này thành cây đối tượng tài liệu (DOM – Document object model): Document doc = null; DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db; db = dbf.newDocumentBuilder(); doc = db.parse(in); doc.getDocumentElement().normalize(); Web service với dữ liệu XML 142
  79. Giáo trình Android Lưu ý: trong đoạn code trên ta bỏ qua phần bắt ngoại lệ try-catch cho dễ quan sát. Sau khi parsing như trên, ta thu được một cây đối tượng trong trường “doc” (đối tượng của lớp org.w3c.dom.Document ). Ta có thể duyệt cây tài liệu này để lấy ra các trường mong muốn. Cụ thể: Để lấy danh sách các node “ Definitions ”: NodeList definitionElements = doc.getElementsByTagName("Definition"); Sau đó duyệt từng phần tử trong danh sách node trên để lấy ra WordDefinition của từng node: NodeList wordDefinitionElements = (definitionElement).getElementsByTagName("WordDefinition"); Việc duyệt cây DOM để trích xuất ra dữ liệu cần thiết là công việc khá điển hình và phổ biến. Bạn đọc quan tâm có thể tham khảo chi tiết hơn tại các tài liệu khác. Web service với dữ liệu XML 143
  80. Giáo trình Android Web service với dữ liệu JSON Ngoài XML, một đinh dạng dữ liệu văn bản được sử dụng rất phổ biến hiện nay trong các web service là JSON (JavaScript Object Notation). So với XML, định dạng JSON có một số ưu điểm: Json có độ nén dữ liệu tốt hơn: cùng một dữ liệu, XML tốn nhiều dung lượng hơn để đóng gói, do các thẻ (tag) trong XML có độ dài nhất định. Dung lượng lớn ảnh hưởng xấu đến tốc độ truyền tải cũng như khả năng lưu trữ tài liệu. Xử lý (phân tích) tài liệu XML tốn kém hơn so với JSON cả về bộ nhớ lẫn tài nguyên CPU. Ví dụ về một tài liệu JSON ta sẽ thử phân tích trong phần tiếp theo có thể lấy được qua đường link sau: Nếu mở link này bằng trình duyệt, ta sẽ quan sát được nội dung như hình bên dưới. Ví dụ dưới đây sẽ phân tích tài liệu JSON ở trên và in ra màn hình appeId và inputTime cho từng đối tượng trong danh Web service với dữ liệu JSON 144