Wednesday, August 29, 2012

Chương trình chơi nhạc đơn giản

Hôm nay là ngày 29/8/2012. Bắt đầu học lập trình android từ hôm 23/8. Trong mấy ngày cật lực mình đã làm được một chương trình chơi nhạc đơn giản. Chương trình này hiển thị danh sách tất cả bài hát có sẵn trong máy. Có nút chơi nhạc, nút Next và Previous. Một thanh cuộn để tua bài hát. Hiển thị tên bài hát hiện tại và thời gian chơi nhạc.
Bài viết chỉ nêu tóm tắt những gì đã học được khi viết chương trình này.
Mã nguồn xem ở cuối bài blog.



Tổng quát

Sau đây là cách mình làm chương trình một chương trình chơi nhạc đơn giản. Để làm chương trình này mình tham khảo các nơi sau:








Khung chương trình có file nguồn:

  1. MainActivity.java để làm Activity giao diện và điều khiển.
  2. MusicService.java để làm Service chơi nhạc (chạy ngay cả khi Activity đã bị hủy).
  3. Playlist.java là Class quản lý danh sách bài hát. Trong đó có Class SongItem chứa thông tin bài hát (class này thừa kế interface Parcelable để cho phép gửi qua lại giữa Activity và Service).
  4. MusicRetriever.java để lấy danh sách bài hát từ hệ thống thông qua ContentResolver và Provider (android.provider.media).
  5. NewAdapter.java là class thừa kế BaseAdapter nhằm mục đích tùy biến ListView hiển thị danh sách bài hát.
Resource:
  1. File ảnh png làm hình ảnh nút bấm lấy từ bộ icon pack của google.
  2. activity_main.xml là layout cho MainActivity.
  3. list_row.xml là layout cho từng row của ListView để hiển thị playlist.

Nhưng gì đã học được sau khi làm xong project này:

Activity, các trạng thái và lưu đồ

Lưu đồ hoạt động của một Activity cần biết để xử lý các biến, khi nào tạo ra/đăng kí, khi nào hủy/bỏ đăng kí.
Ví dụ onCreate() đăng kí một broadcastReciever và ở onStop()  hủy đăng kí thì sẽ gây lỗi vì sau onStop() Activity có thể quay lại onStart() chứ không qua onCreate(), do đó broadcastReciever bị hủy lại không sống trở lại được. Cần phải đăng kí broadcastReciever ở onStart()

Biết về Activity hoạt động thế nào khi có tác động của người dùng. Sử dụng catlog để ghi lại hoạt động là rất cần thiết.
Ví dụ, khi xoay màn hình hệ thống sẽ gọi lại onDestroy() sau đó onCreate(), chương trình sẽ xử sự thế nào?
Khi màn hình khóa lại hệ thống sẽ gọi hàm onPause() rồi onStop() (hình như thế)



Cách làm layout đơn giản

Layout đơn giản học làm: LinearLayout, RelativeLayout.
Các nút bấm, SeekBar, ListView, ...
Làm việc với xml, các attribute của android, ...
Cái này phải đọc phần tham khảo.

Cách tùy biến cho ListView

Truyền dữ liệu từ ArrayList cho ListView bằng Adapter. Ở đây, NewAdapter là class thừa kế của BaseAdapter. 
NewAdapter adapter = new NewAdapter(this, playlist.getArrayList()); 
playlistView.setAdapter(adapter);

NewAdapter khi khởi tạo sẽ được thông tin về đối tượng Activity và data nó cần xử lý. Nó sẽ dùng đối tượng activity này để gọi hàm lấy ra 1 đối tượng LayoutInflater.
 public NewAdapter(Activity a, ArrayList<SongItem> d) {
  myActivity = a;
  data = d;
  inflater = (LayoutInflater) myActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
 }
Inflate có nghĩa là thổi phồng?? Không biết. Nhưng nói chung là nó dùng để xử lý giao diện từng dòng trong ListView.  Khi ListView vẽ, nó gọi hàm getView cho từng dòng dữ liệu được xử lý (dòng nào thì thể hiện bằng position) của data. Với dòng đầu tiên, convertView = null. Sau khi phát hiện nó là null, ta inflate nó theo layout list_row. Sau đó thì dùng các hàm thông thường để đưa dữ liệu vào các widget của layout đấy. Sau đó trả lại view.

Tới dòng thứ hai, convertView đã khác null và không cần inflate lại nữa.

 public View getView(int position, View convertView, ViewGroup parent) {
  // TODO Auto-generated method stub
  View vi = convertView;
  if (vi == null) {
   vi = inflater.inflate(R.layout.list_row, null);
  }

  TextView title = (TextView) vi.findViewById(R.id.Title);
  TextView artist = (TextView) vi.findViewById(R.id.Artist);

  title.setText(data.get(position).mTitle);
  artist.setText(data.get(position).path);
  return vi;
 }

Các điều khiển Mediaplayer

android.media.mediaplayer là một class cho phép chơi nhạc. Đưa vào cho nó địa chỉ file nhạc, nó sẽ chơi nhạc. Có thể điều khiển nó dừng, chơi, tua đi, ... Tất cả mô tả trong Reference Mediaplayer và trong Guide to use MediaPlayer. Thứ quan trọng cần thiết là cần hiểu các trạng thái (state) và sơ đồ hoạt động. 

Android SDK có một ví dụ chơi nhạc này ở trong thư mục /sample/, có thể học ở đấy cách viết ra một service chơi nhạc.








Làm việc với provider

Làm việc với provider giống như làm việc với cơ sở dữ liệu. Provider cung cấp các dữ liệu chung như danh bạ, các file nhạc trong máy, ... Người lập trình cũng có thể tạo ra provider của riêng mình.
Tập dượt đầu tiên làm việc với android.provider.MediaStore:

  • Lấy ContentResolver từ Activity thông qua hàm getContentResolver()
  • Đặt Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI. Đây giống như là khai báo nguồn dữ liệu.
  • Gọi hàm query của đối tượng ContentResolver với đầu vào là uri (database source), cùng với các tham số đầu vào của hàm giống như khi thực hiện select trong SQL.
  • Hàm sẽ trả về Cursor để ta làm việc với từng dòng của bảng lấy ra bởi query. Cursor có thể di chuyển tiếp, lên đầu, xuống cuối, ...

Tất cả ở trong Class MusicRetriever:

public class MusicRetriever {
 private ContentResolver mCR;
 private Playlist allSongList;

 public MusicRetriever(ContentResolver cr) {
  mCR = cr;
  allSongList = new Playlist();
 }

 public void prepare() {
  Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;

  Cursor cur = mCR.query(uri, null, MediaStore.Audio.Media.IS_MUSIC
    + "=1", null, null);

  if (cur == null) {
  }

  if (!cur.moveToFirst()) {
   return;
  }

  int idColumn = cur.getColumnIndex(MediaStore.Audio.Media._ID);
  int artistColumn = cur.getColumnIndex(MediaStore.Audio.Media.ARTIST);
  int titleColumn = cur.getColumnIndex(MediaStore.Audio.Media.TITLE);
  int dataColumn = cur.getColumnIndex(MediaStore.Audio.Media.DATA);

  do {
   allSongList.add(new SongItem(
     cur.getLong(idColumn),
     cur.getString(titleColumn), 
     cur.getString(artistColumn), 
     cur.getString(dataColumn)
     ));
  } while (cur.moveToNext());
 }
 
 public Playlist getPlaylist(){
  return allSongList;
 }
}

Truyền dữ liệu giữa Activity và Service

Khi làm chương trình này, mình hiểu thế này: Activity là cái thể hiện giao diện, Service là thằng chạy nền.
Activity đôi khi bị ẩn đi, đóng đi, hủy đi do nguyên nhân người dùng, do hệ thống, ... Do đó để nghe nhạc ổn định, phải làm service. 

Phải truyền thông tin từ activity cho service để ra lệnh chơi bài, dừng bài, tua đi, ... thì dễ dàng bằng cách gọi hàm startService(..), và truyền thông tin cho nó Intent. Intent giống như thông điệp được truyền đi truyền lại trong hệ thống giữa các Activity, Service, ... Có thể gói thêm dữ liệu cho nó bằng các hàm put..(..). Ngoài ra có thể gói thêm một nhóm dữ liệu vào 1 cái là Bundle.
Xem thêm ở đây cách làm: Pass data from activity to service.

Khi truyền thông tin ngược lại từ service về activity (ví dụ những lúc hết 1 bài hát, service chuyển sang bài mới, giao diện phải cập nhật tên bài mới), lại phải chơi kiểu kì công là dùng broadcastReciever. Service không gửi thẳng tới Activity được mà phải gửi broadcast (kiểu như loa gọi trong sân bay). Activity nếu có đang ở trên màn hình thì dùng 1 đối tượng broadcastReciever để bắt, nếu như khi đó màn hình tắt, người dùng đang ở chương trình khác, ... thì activity không có mặt để mà bắt.
Xem thêm ở đây cách làm: Pass data from Service to Activity.

Một vấn đề khi truyền thông tin là sự hạn chế về loại dữ liệu có thể đóng gói vào Intent để gửi. Ta có thể dùng các hàm put..(...) để đưa dữ liệu vào intent, dữ liệu đó là int, long, String, .... Nhưng nếu dữ liệu cần gửi là một bản ghi phức tạp hơn. Như trong trường hợp chương trình này là SongItem. SongItem lưu thông tin về bài hát, trong đó có tên, ca sĩ, đường dẫn, ... Không có hàm nào có thể put dữ liệu Object vào Intent được. 
Cách giải quyết là sử dụng interface Parcelable. Có thể put dữ liệu có interface Parcelable vào Intent. Mình đã phải khai báo lại cho SongItem implements Parcelable. Phải viết thêm một số hàm cho interface này nữa. Và sau đó có thể đóng gói SongItem vào Intent để truyền thông tin qua lại giữa Service và Activity.
Chi tiết cách làm việc với Parcelable: Android Parcelable Example

Có cách khác để truyền dữ liệu phức tạp hơn giữa Activity và Service là dùng Singleton. Mình viết 1 bài ở đây: Singleton

3 comments:

  1. cám ơn bạn! mình đang rất cần nó

    ReplyDelete
  2. đây là part 1. góp ý chút là bạn viết thế nào mà cho người đọc biết là vẫn cón các part khác nữa. chứ mình mới đọc tưởng có mỗi 1 part. góp ý thôi. thank nhé

    ReplyDelete