Author Archives: Phở Code
Android – Gửi SMS
Trong bài này chúng ta sẽ tìm hiểu cách gửi tin nhắn SMS. Về cơ bản thì có 2 cách để có thể gửi SMS là:
- Dùng lớp
SmsManager
- Dùng
Intent
Lớp SmsManager
cho phép chúng ta thiết lập các thông số khác nhau và do đó chúng ta có thể gửi tin nhắn một cách linh hoạt theo ý chúng ta muốn. Còn cách sử dụng Intent
tức là chúng ta chỉ đơn giản là mở một ứng dụng SMS khác trong máy ra rồi để người dùng “tự xử” thôi. Trong bài này chúng ta sẽ sử dụng lớp SmsManager.
Bây giờ chúng ta tạo project mới và bắt đầu tìm hiểu cách sử dụng.
Xin quyền gửi SMS
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="phocode.com.smsmanager"> <uses-permission android:name="android.permission.SEND_SMS"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Để ứng dụng có thể gửi tin nhắn SMS thì đầu tiên chúng ta phải xin cấp quyền android.permission.SEND_SMS.
Giao diện
Chúng ta thiết kế giao diện như sau:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="phocode.com.smsmanager.MainActivity"> <EditText android:id="@+id/phoneNumber" android:layout_width="fill_parent" android:layout_height="wrap_content" android:ems="10" android:hint="Phone number" android:inputType="phone"/> <EditText android:id="@+id/smsBody" android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="top" android:hint="Content" /> <Button android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Send" android:onClick="sendSms"/> </LinearLayout>
Ở đây chúng ta có 2 EditText
dùng để nhập số điện thoại cần gửi và nội dung tin nhắn, một Button
để gọi phương thức gửi.
MainActivity
Trong Activity
chúng ta xử lý như sau:
package phocode.com.smsmanager; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.telephony.SmsManager; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; public class MainActivity extends AppCompatActivity { private EditText phoneNumber; private EditText content; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); phoneNumber = (EditText) findViewById(R.id.phoneNumber); content = (EditText) findViewById(R.id.smsBody); } public void sendSms(View view) { try { SmsManager smsManager = SmsManager.getDefault(); smsManager.sendTextMessage(phoneNumber.getText().toString(), null, content.getText().toString(), null, null); Toast.makeText(getApplicationContext(), "SMS sent successful", Toast.LENGTH_LONG).show(); } catch(IllegalArgumentException ex) { Toast.makeText(getApplicationContext(), "Sending SMS failed", Toast.LENGTH_LONG).show(); } } }
Lớp Activity
này kế thừa từ lớp AppCompatActivity.
public void sendSms(View view) { ... }
Chúng ta định nghĩa phương thức sendSms()
tương ứng như đã khai báo với Button.
SmsManager smsManager = SmsManager.getDefault();
Lớp SmsManager
sẽ chịu trách nhiệm việc gửi tin nhắn. Chúng ta không tạo lớp này một cách trực tiếp mà gọi từ phương thức SmsManager.getDefault().
smsManager.sendTextMessage(phoneNumber.getText().toString(), null, content.getText().toString(), null, null);
Để gửi tin nhắn thì chúng ta gọi phương thức SmsManager.sendTextMessage().
Tham số đầu tiên là số điện thoại nhận. Tham số thứ 2 là chuỗi chỉ định địa chỉ của trung tâm tin nhắn (Short Message Service Center – SMSC), về cơ bản thì khi bạn gửi một tin nhắn đến một số điện thoại nào đó, tin nhắn của bạn phải được gửi về trung tâm này rồi trung tâm này sẽ gửi đến số điện thoại đích trong thẻ SIM của bạn, ở đây chúng ta truyền vào null thì phương thức này sẽ sử dụng số SMSC mặc định. Tham số thứ 3 là nội dung tin nhắn truyền đi. Tham số thứ 4 và thứ 5 là đối tượng Intent
được gọi khi tin nhắn đã được gửi đi và khi người nhận đã nhận được tin nhắn, ở đây chúng ta truyền vào null.
catch(IllegalArgumentException ex) { Toast.makeText(getApplicationContext(), "Sending SMS failed", Toast.LENGTH_LONG).show(); }
Phương thức sendTextMessage()
giải phóng lỗi IllegalArgumentException
khi số điện thoại và nội dung tin nhắn nhập vào là rỗng, chúng ta có thể bắt lỗi này để xử lý.
Android – Định vị GPS
Một trong những tính năng độc nhất của các ứng dụng trên thiết bị di động là thiết bị định vị tọa độ. Hầu như những người sử dụng smartphone đều mang theo chúng bên mình mọi lúc mọi nơi. Android cung cấp các lớp cho phép chúng ta truy cập vào hệ tọa độ địa lý của thiết bị, từ đó chúng ta có thể dùng để hiển thị vị trí đó trên bản đồ, hoặc tính khoảng cách giữa 2 điểm trên mặt đất… Trong phần này chúng ta sẽ tìm hiểu cách lấy kinh độ (Longitude) và vĩ độ (Latitude) của thiết bị.
Chúng ta tạo một project mới. Trong project này chúng ta cần dùng 3 quyền sau đây:
ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
ACCESS_MOCK_LOCATION
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.phocode" android:versionCode="1" android:versionName="1.0"> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <application android:label="@string/app_name" android:icon="@drawable/ic_launcher"> <activity android:name="MainActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Trong file layout chúng ta thiết kế như sau:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <Button android:id="@+id/getLocation" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Get location" android:textSize="20sp" android:onClick="onClick"/> <TextView android:id="@+id/longitude" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Longitude: " android:textSize="20sp"/> <TextView android:id="@+id/latitude" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Latitude: " android:textSize="20sp"/> </LinearLayout>
Chúng ta thiết kế một Button
và 2 TextView.
Khi click vào Button
thì hiển thị kinh độ và vĩ độ lên 2 TextView. Tiếp
theo là lớp MainActivity:
package com.phocode; import android.app.Activity; import android.os.Bundle; import android.content.Context; import android.location.Location; import android.location.LocationManager; import android.location.LocationListener; import android.view.View; import android.widget.Button; import android.widget.TextView; public class MainActivity extends Activity { private static final long MINIMUM_DISTANCE_CHANGE_FOR_UPDATES = 1; private static final long MINIMUM_TIME_BETWEEN_UPDATES = 1000; private LocationManager locationManager; private TextView longitude, latitude; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); longitude = (TextView) findViewById(R.id.longitude); latitude = (TextView) findViewById(R.id.latitude); locationManager = (LocationManager)getSystemService(Context.LOCATION_SERVICE); locationManager.requestLocationUpdates( LocationManager.GPS_PROVIDER, MINIMUM_TIME_BETWEEN_UPDATES, MINIMUM_DISTANCE_CHANGE_FOR_UPDATES, new MyLocationListener()); } public void onClick(View view) { Location location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if(location != null) { longitude.setText("Longitude: " + String.valueOf(location.getLongitude())); latitude.setText("Latitude: " + String.valueOf(location.getLatitude())); } } private class MyLocationListener implements LocationListener { @Override public void onLocationChanged(Location location) { longitude.setText("Longitude: " + String.valueOf(location.getLongitude())); latitude.setText("Latitude: " + String.valueOf(location.getLatitude())); } @Override public void onStatusChanged(String s, int i, Bundle b) {} @Override public void onProviderDisabled(String s) {} @Override public void onProviderEnabled(String s) {} } }
Lớp MainActivity
sẽ chịu trách nhiệm lấy dữ liệu tọa độ và hiển thị lên màn hình.
locationManager = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
Đầu tiên chúng ta lấy một đối tượng LocationManager
thông qua phương thức getSystemService().
locationManager.requestLocationUpdates( LocationManager.GPS_PROVIDER, MINIMUM_TIME_BETWEEN_UPDATES, MINIMUM_DISTANCE_CHANGE_FOR_UPDATES, new MyLocationListener() );
Phương thức requestLocationUpdates()
có tác dụng “gắn” đối tượng LocationListener
vào đối tượng LocationManager.
Mục đích là để bắt các sự kiện thay đổi trạng thái của LocationManager.
Ở đây chúng ta định nghĩa lớp MyLocationListener
kế thừa từ giao diện LocationListener,
lớp MyLocationListener
được định nghĩa ở dưới.
Tham số GPS_PROVIDER
là tên nhà cung cấp tọa độ. MINIMUM_TIME_BETWEEN_UPDATES
là thời gian ngắn nhất để cập nhật lại tọa độ, ở đây là 1 giây. MINIMUM_DISTANCE_CHANGE_FOR_UPDATES
là khoảng cách thay đổi thấp nhất để cập nhật lại tọa độ, ở đây chúng ta định nghĩa là 1000m, tức là nếu chúng ta cầm máy đi được 1000 mét thì thực hiện cập nhật lại. Các phương thức cập nhật sẽ được gọi trong lớp MyLocationListener.
public void onClick(View view) { Location location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if(location != null) { longitude.setText("Longitude: " + String.valueOf(location.getLongitude())); latitude.setText("Latitude: " + String.valueOf(location.getLatitude())); } }
Phương thức onClick()
sẽ được gọi khi người dùng click vào Button.
Ở đây chúng ta lấy đối tượng Location
từ phương thức getLastKnownLocation()
trong đối tượng LocationManager.
Chúng ta có thể lấy kinh độ và vĩ độ từ lớp Location.
private class MyLocationListener implements LocationListener { @Override public void onLocationChanged(Location location) { longitude.setText("Longitude: " + String.valueOf(location.getLongitude())); latitude.setText("Latitude: " + String.valueOf(location.getLatitude())); } ... }
Cuối cùng chúng ta định nghĩa lớp MyLocationListener
có cài đặt giao diện LocationListener.
Giao diện này có 4 phương thức cần được override là onLocationChanged(), onStatusChanged(), onProviderDisabled()
và onProviderEnabled().
Ở đây chúng ta chỉ dùng đến phương thức onLocationChanged(),
phương thức này được gọi khi tọa độ của máy thay đổi, trong phương thức này chúng ta cũng chỉ làm công việc lấy kinh độ và vĩ độ để gán lên TextView.
Chạy ứng dụng chúng ta có giao diện như thế này:
Từ 2 thông số “khô khan” là kinh độ và vĩ độ, chúng ta có thể dùng dể hiển thị lên bản đồ (trong bài sau chúng ta sẽ học cách sử dụng bản đồ), tính khoảng cách giữa 2 điểm trên mặt đất, lấy địa chỉ dựa theo kinh độ và vĩ độ…v.v
Android – Support Library
Khi chúng ta phát triển ứng dụng trên các hệ điều hành mới như từ phiên bản 5.x trở lên, mà muốn ứng dụng có thể chạy được trên các hệ điều hành cũ hơn thì chúng ta phải có biện pháp để hỗ trợ. Biện pháp đó là sử dụng Support Library, đây là một tập các thư viện cung cấp các hàm API tương thích với các hệ điều hành cũ. Support Library cũng có nhiều phiên bản, mỗi phiên bản sẽ có những tính năng khác nhau.
Mặc dù từ bài đầu tiên cho đến bây giờ chúng ta rất ít khi sử dụng các Support Library, tuy nhiên khi làm ứng dụng thực tế thì hầu như chúng ta sẽ phải dùng đến chúng để ứng dụng của chúng ta có thể chạy được trên càng nhiều thiết bị càng tốt.
Các tính năng của Support Library
Support Library có nhiều phiên bản, mỗi phiên bản hỗ trợ một số tính năng nhất định với một vài dòng hệ điều hành nhất định. Để có thể sử dụng Support Library một cách hiệu quả nhất, chúng ta cần phải biết phiên bản nào hỗ trợ những cái gì.
Bảng dưới đây mô tả phạm vi sử dụng của từng phiên bản Support Library:
Phiên bản | Nội dung |
---|---|
v4 | Tương thích với hệ điều hành Android 1.6 (API 14) trở lên. |
v7 | Tương thích với hệ điều hành Android 2.1 (API 17) trở lên. |
v8 | Tương thích với hệ điều hành Android đầu tiên (API 8) trở lên. |
v13 | Tương thích với hệ điều hành Android 3.2 (API 13) trở lên. |
Chi tiết các tính năng trong từng phiên bản có thể xem ở đây.
Cài đặt Support Library
Chúng ta cài đặt thư viện Support Library từ Android SDK→Extras→Android Support Library
Bạn có thể sẽ cần phải chạy Android SDK dưới quyền Administrator và check vào nút Obsolete nếu không thấy Support Library hiện ra.
Sau khi cài xong, bộ thư viện này sẽ nằm trong thư mục /extras/android/support.
Khi viết ứng dụng có sử dụng các thư viện Support Library, bạn nên khai báo phiên bản API thấp nhất mà ứng dụng hỗ trợ trong file AndroidManifest.xml,
ví dụ bạn sử dụng Support Library v13, tức là ứng dụng của bạn hỗ trợ Android 3.2 trở lên, thì trong file AndroidManifest.xml
bạn khai báo như sau:
<uses-sdk android:minSdkVersion="13" android:targetSdkVersion="22" />
Trong đó android:minSdkVersion
là thuộc tính khai báo phiên bản hệ điều hành thấp nhất mà ứng dụng hỗ trợ. Thuộc tính này sẽ báo cho Google Play biết ứng dụng của chúng ta tương thích với phiên bản nào.
Android – Hiển thị Notification
Notification (thông báo) là một tin nhắn được nhắn tới cho người dùng ở bên ngoài giao diện của ứng dụng. Các thông báo được hệ điều hành hiển thị trên vùng thông báo, khi có tin nhắn thông báo, hệ điều hành sẽ hiển thị biểu tượng của tin nhắn, nếu người dùng muốn xem nội dung thông báo thì phải mở phần thông báo đó ra (bằng cách dùng tay kéo), đây là khu vực được quản lý bởi hệ điều hành, người dùng có thể xem bất kỳ lúc nào.
Tạo thông báo
Chúng ta dùng lớp android.app.Notification
để tạo thông báo, tuy nhiên chúng ta không tạo trực tiếp từ lớp này mà dùng lớp Notification.Builder.
Việc quản lý các thông báo được thực hiện bởi lớp android.app.NotificationManager,
khi muốn hiển thị thông báo thì chúng ta sử dụng phương thức NotificationManager.notify().
Một đối tượng Notification
cần có ít nhất 3 thông tin sau:
- Biểu tượng của tin nhắn, được thiết lập bằng phương thức
setSmallIcon().
- Tiêu đề của tin nhắn, thiết lập bằng phương thức
setContentTitle().
- Nội dung tin nhắn, thiết lập bằng phương thức
setContentText().
Ngoài 3 thông tin bắt buộc ở trên, chúng ta còn có thể thêm vào các Action, đây là các nút bấm hiển thị cùng với tin nhắn, khi người dùng xem tin nhắn thì có thể bấm vào các nút này để thực hiện các công việc khác nhau.
Khi người dùng click vào tin nhắn thông báo, thường thì chúng ta nên mở một Activity
nào đó và thực hiện công việc nào đó, tuy nhiên việc này là không bắt buộc. Chúng ta sử dụng lớp android.app.PendingIntent,
lớp này chứa một đối tượng Intent
dùng để mở một Activity
nào đó. Để thiết lập cho tin nhắn mở Activity
nào thì chúng ta dùng phương thức setContentIntent().
Ví dụ
Chúng ta tạo một project mới. Trong project này chúng ta định nghĩa 2 Activity
là MainActivity
được khởi động khi chạy ứng dụng, và ResultActivity
được khởi động khi click vào tin nhắn.
Trong file AndroidManifest.xml
chúng ta khai báo lớp ResultActivity.
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.phocode" android:versionCode="1" android:versionName="1.0"> <application android:label="@string/app_name" android:icon="@drawable/ic_launcher"> <activity android:name="MainActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".ResultActivity"/> </application> </manifest>
Đầu tiên là file layout chính của lớp MainActivity:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center"> <Button android:id="@+id/btnNotification" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Send notification" android:onClick="sendNotification"/> </LinearLayout>
Chúng ta hiển thị một Button,
click vào Button
này thì gửi tin nhắn thông báo.
Tiếp theo là lớp MainActivity:
package com.phocode; import android.app.Activity; import android.os.Bundle; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Intent; import android.view.View; public class MainActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } public void sendNotification(View view) { Intent intent = new Intent(this, ResultActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, (int)System.currentTimeMillis(), intent, 0); Notification noti = new Notification.Builder(this) .setContentTitle("New message") .setContentText("Subject") .setSmallIcon(R.drawable.ic_launcher) .setContentIntent(pendingIntent) .build(); NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); noti.flags |= Notification.FLAG_AUTO_CANCEL; notificationManager.notify(0, noti); } }
Phương thức sendNotification()
sẽ được gọi khi người dùng click vào Button trên màn hình.
Intent intent = new Intent(this, ResultActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, (int)System.currentTimeMillis(), intent, 0);
Ở đây chúng ta tạo một đối tượng Intent
trở tới lớp ResultActivity
và đối tượng PendingIntent
chứa đối tượng Intent
vừa tạo.
Notification noti = new Notification.Builder(this) .setContentTitle("New message") .setContentText("Subject") .setSmallIcon(R.drawable.ic_launcher) .setContentIntent(pendingIntent) .build();
Trong đoạn code trên chúng ta tạo đối tượng Notification
từ lớp Notification.Builder.
Lưu ý phương thức build()
sẽ trả về đối tượng Notification,
tuy nhiên phương thức này chỉ có từ phiên bản API 16 trở lên, ở các phiên bản trước đó thì phương thức này có tên khác là getNotification().
noti.flags |= Notification.FLAG_AUTO_CANCEL;
Dòng code trên có nghĩa là ẩn thông báo đi sau khi người dùng đã click vào, nếu không thì chỉ khi dùng tay kéo đi thì thông báo mới biến mất.
notificationManager.notify(0, noti);
Phương thức notify()
sẽ gửi đối tượng Notification
đến hệ điều hành để hiển thị, tham số đầu tiên là Id mà chúng ta tự gán cho thông báo đó, nếu trước đó đã có tin nhắn thông báo có Id bị trùng thì tin nhắn thông báo đó sẽ bị ghi đè.
Tiếp theo là file layout của lớp ResultActivity:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Message received" /> </LinearLayout>
Chúng ta chỉ hiển thị một câu thông báo đơn giản thôi.
Cuối cùng là định nghĩa lớp ResultActivity:
package com.phocode; import android.app.Activity; import android.os.Bundle; public class ResultActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.result); } }
Chạy chương trình chúng ta có kết quả:
Android – Service
Service
(dịch vụ) là một ứng dụng chạy ngầm bên dưới hệ điều hành thực hiện các công việc riêng biệt, một Service
không có giao diện để tương tác với người dùng. Một ứng dụng có thể khởi tạo một đối tượng Service
và chạy, đối tượng Service
chạy liên tục ngay cả khi người dùng chuyển đổi sang ứng dụng khác, Service
cũng có thể được “gắn” với một ứng dụng và trao đổi qua lại với ứng dụng đó. Thường thì chúng ta dùng Service
để thực hiện các công việc như mở/truyền dữ liệu/đóng kết nối mạng, mở nhạc, đọc/ghi file…
Tuy nhiên bạn cần hiểu là Service
không phải là luồng (Thread),
tức là nó không phải là một tiến trình chạy song song với ứng dụng đã gọi nó, Service chỉ đơn giản là một đối tượng chạy ngầm dưới hệ điều hành, nói là chạy ngầm bởi vì chúng ta không thấy nó bằng mắt vì nó không có giao diện người dùng (UI). Tóm lại bạn có thể hiểu Service
là một Activity
nhưng không có file layout để tương tác với người dùng như Activity.
Service có 2 loại:
- Started
Service
loại này được tạo ra từ phương thứcstartService()
của một đối tượngActivity.
Khi đã được tạo ra thì đối tượngService
này có thể chạy vô thời hạn, ngay cả khi đối tượng đã tạo ra nó bị hủy. Thông thường thìService
sẽ thực hiện một công việc đơn lẻ nào đó rồi tự hủy chứ không trả kết quả nào về cho đối tượng đã gọi nó cả. Chẳng hạn nhưService
dùng để tải file/ upload file…- Bound
Service
loại này sẽ được “gắn” dính vào một đối tượng nào đó bằng phương thứcbindService().
Các đối tượngService
này cung cấp các phương thức để đối tượng đã được gắn vào nó có thể giao tiếp với đối tượngService
đó.Bound Service
chỉ chạy khi có ít nhất một đối tượng được gắn vào nó, nếu không có đối tượng nào được gắn vào nó thì nó sẽ bị hủy.
Ví dụ
Chúng ta tạo một project mới có sử dụng Service
được tạo ra từ lớp android.app.Service.
Trong file layout chính chúng ta thiết kế như sau:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center"> <Button android:id="@+id/btn1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="stopNewService" android:text="Stop Service"/> <Button android:id="@+id/btn2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="23dp" android:onClick="startNewService" android:text="Start Service" /> </LinearLayout>
Giao diện chính chỉ gồm 2 Button
để tạo và hủy một Service.
Tiếp theo là lớp MainActivity:
package com.phocode; import android.app.Activity; import android.os.Bundle; import android.content.Intent; import android.view.Menu; import android.view.View; public class MainActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } public void startNewService(View view) { startService(new Intent(this, NewService.class)); } public void stopNewService(View view) { stopService(new Intent(this, NewService.class)); } }
Trong lớp MainActivity,
chúng ta tạo một Service
bằng cách gọi phương thức startService(),
tham số của phương thức này là một đối tượng Intent.
Chúng ta hủy Service
bằng cách gọi phương thức stopService(),
phương thức này cũng nhận một đối tượng Intent.
Các đối tượng Intent
này tham chiếu tới đối tượng Service
mà chúng ta sẽ định nghĩa sau đây:
package com.phocode; import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.widget.Toast; public class NewService extends Service { public NewService() {} @Override public IBinder onBind(Intent intent) { throw new UnsupportedOperationException("Not yet implemented"); } @Override public void onCreate() { Toast.makeText(this, "Service created", Toast.LENGTH_SHORT).show(); } @Override public void onStart(Intent intent, int startID) { Toast.makeText(this, "Service started", Toast.LENGTH_SHORT).show(); } @Override public void onDestroy() { Toast.makeText(this, "Service destroyed", Toast.LENGTH_SHORT).show(); } }
Một đối tượng Service
sẽ kế thừa từ lớp android.app.Service,
và phải override các phương thức onCreate(), onStart()
và onDestroy().
Ở đây chúng ta chỉ in ra các câu thông báo Toast
đơn giản.
Android – Camera và SurfaceView
Trong Android, các lớp View
cơ bản như Button, TextView...
được “vẽ” trên một luồng và hầu như chỉ thay đổi hình dáng, màu sắc của chúng khi có tương tác với người dùng, chẳng hạn như khi chúng ta click vào Button
thì Button
đó sáng lên. Trong trường hợp chúng ta cần hiển thị một thứ gì đó thay đổi liên tục như màn hình hiển thị camera, hay đồ họa game… thì chúng ta nên sử dụng lớp SurfaceView,
lớp SurfaceView
cho phép chúng ta can thiệp đến từng pixel trên màn hình.
Trong phần này chúng ta sẽ sử dụng SurfaceView
để hiển thị camera lên màn hình.
AndroidManifest
Chúng ta tạo một project mới. Trong file AndroidManifest.xml
chúng ta khai báo 2 quyền như sau:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.phocode" android:versionCode="1" android:versionName="1.0"> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application android:label="@string/app_name" android:icon="@drawable/ic_launcher"> <activity android:name="MainActivity" android:label="@string/app_name" android:configChanges="orientation"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Quyền android.permission.CAMERA
là quyền được phép sử dụng camera, quyền android.permission.WRITE_EXTERNAL_STORAGE
cho phép chúng ta ghi file lên thẻ nhớ ngoài. chúng ta xin quyền này vì ở đây mình còn “chụp hình” nữa 🙂
Layout
Trong file layout chúng ta thiết kế như sau:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <SurfaceView android:id="@+id/cameraView" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1" android:orientation="horizontal" /> <LinearLayout android:id="@+id/capture" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:orientation="horizontal"> <Button android:id="@+id/button_ChangeCamera" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Switch" android:onClick="switchCamera"/> <Button android:id="@+id/button_capture" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Capture" android:onClick="captureImage"/> </LinearLayout> </LinearLayout>
Chúng ta thiết kế một SurfaceView,
1 LinearLayout
khác để chứa 2 Button
khác là Switch (đổi camera) và Capture (chụp hình).
MainActivity
Cuối cùng là file activity:
package com.phocode; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import android.app.Activity; import android.os.Bundle; import android.hardware.Camera; import android.hardware.Camera.CameraInfo; import android.hardware.Camera.PictureCallback; import android.hardware.Camera.ShutterCallback; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.OrientationEventListener; import android.widget.TextView; import android.widget.Toast; import android.content.res.Configuration; public class MainActivity extends Activity implements SurfaceHolder.Callback { private Camera camera; private SurfaceView surfaceView; private SurfaceHolder surfaceHolder; private PictureCallback rawCallback; private ShutterCallback shutterCallback; private PictureCallback captureImageCallback; private boolean frontCam; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); surfaceView = (SurfaceView) findViewById(R.id.cameraView); surfaceHolder = surfaceView.getHolder(); surfaceHolder.addCallback(this); captureImageCallback = new PictureCallback() { public void onPictureTaken(byte[] data, Camera camera) { FileOutputStream outStream = null; try { String fileName = String.format("/sdcard/%d.jpg", System.currentTimeMillis()); outStream = new FileOutputStream(fileName); outStream.write(data); outStream.close(); } catch(FileNotFoundException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } finally {} Toast.makeText(getApplicationContext(), "Picture saved", Toast.LENGTH_SHORT).show(); refreshCamera(); } }; frontCam = false; } public void refreshCamera() { if(surfaceHolder.getSurface() == null) return; try { camera.stopPreview(); } catch(Exception e) {} try { camera.setPreviewDisplay(surfaceHolder); camera.startPreview(); } catch(Exception e) {} } public void changeOrientation() { if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) camera.setDisplayOrientation(0); else camera.setDisplayOrientation(90); } public int isFrontCameraExisted() { int cameraId = -1; int numberOfCameras = Camera.getNumberOfCameras(); for(int i = 0 ; i < numberOfCameras ; i++) { CameraInfo info = new CameraInfo(); Camera.getCameraInfo(i, info); if(info.facing == CameraInfo.CAMERA_FACING_FRONT) { cameraId = i; break; } } return cameraId; } public int isBackCameraExisted() { int cameraId = -1; int numberOfCameras = Camera.getNumberOfCameras(); for(int i = 0 ; i < numberOfCameras ; i++) { CameraInfo info = new CameraInfo(); Camera.getCameraInfo(i, info); if(info.facing == CameraInfo.CAMERA_FACING_BACK) { cameraId = i; break; } } return cameraId; } public void switchCamera(View view) { if(frontCam) { int cameraId = isBackCameraExisted(); if(cameraId >= 0) { try { camera.stopPreview(); camera.release(); camera = Camera.open(cameraId); camera.setPreviewDisplay(surfaceHolder); camera.startPreview(); frontCam = false; changeOrientation(); } catch(RuntimeException e) {} catch(Exception e) {} Camera.Parameters param; param = camera.getParameters(); param.setPreviewSize(surfaceView.getWidth(), surfaceView.getHeight()); camera.setParameters(param); } } else { int cameraId = isFrontCameraExisted(); if(cameraId >= 0) { try { camera.stopPreview(); camera.release(); camera = Camera.open(cameraId); camera.setPreviewDisplay(surfaceHolder); camera.startPreview(); frontCam = true; changeOrientation(); } catch(RuntimeException e) {} catch(Exception e) {} Camera.Parameters param; param = camera.getParameters(); param.setPreviewSize(surfaceView.getWidth(), surfaceView.getHeight()); camera.setParameters(param); } } } public void captureImage(View view) throws IOException { camera.takePicture(null, null, captureImageCallback); } @Override public void surfaceCreated(SurfaceHolder holder) { try { camera = Camera.open(); changeOrientation(); } catch(RuntimeException e) { System.err.println(e); } Camera.Parameters param; param = camera.getParameters(); param.setPreviewSize(surfaceView.getWidth(), surfaceView.getHeight()); camera.setParameters(param); try { camera.setPreviewDisplay(surfaceHolder); camera.startPreview(); } catch(Exception e) { System.err.println(e); return; } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { refreshCamera(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { camera.stopPreview(); camera.release(); camera = null; } @Override public void onConfigurationChanged(Configuration config) { super.onConfigurationChanged(config); changeOrientation(); } }
Ngoài lớp SurfaceView
để hiển thị lên màn hình, Android còn cung cấp lớp SurfaceHolder
cho phép chúng ta thay đổi kích thước và định dạng của SurfaceView,
truy xuất đến từng pixel, quản lý những thay đổi diễn ra trên SurfaceView...
public class MainActivity extends Activity implements SurfaceHolder.Callback { ... }
Lớp SurfaceHolder
cung cấp giao diện Callback,
giao diện này có 3 phương thức trừu tượng cần được override là surfaceCreated(), surfaceChanged()
và surfaceDestroyed(),
3 phương thức này được gọi khi có sự thay đổi diễn ra trên đối tượng SurfaceView.
surfaceView = (SurfaceView) findViewById(R.id.cameraView); surfaceHolder = surfaceView.getHolder(); surfaceHolder.addCallback(this);
Chúng ta implement giao diện SurfaceHolder.Callback,
sau đó trong phương thức onCreate(),
chúng ta lấy tham chiếu đến đối tượng SurfaceView,
mỗi đối tượng SurfaceView
sẽ có một đối tượng SurfaceHolder
của riêng nó, chúng ta có thể lấy bằng phương thức getHolder().
Cuối cùng chúng ta gắn listener của giao diện Callback
vào đối tượng SurfaceHolder
bằng phương thức addCallback().
@Override public void surfaceCreated(SurfaceHolder holder) { try { camera = Camera.open(); changeOrientation(); } catch(RuntimeException e) { System.err.println(e); } Camera.Parameters param; param = camera.getParameters(); param.setPreviewSize(surfaceView.getWidth(), surfaceView.getHeight()); camera.setParameters(param); try { camera.setPreviewDisplay(surfaceHolder); camera.startPreview(); } catch(Exception e) { System.err.println(e); return; } }
Phương thức surfaceCreated()
sẽ được gọi khi ứng dụng bắt đầu chạy, trong phương thức này chúng ta khởi tạo đối tượng android.hardware.Camera
bằng phương thức Camera.open().
Ở đây chúng ta phải bắt lỗi RunTimeException
vì có những thiết bị không có camera (chẳng hạn như máy ảo). Phương thức changeOrientation()
sẽ thay đổi hướng của camera, chúng ta sẽ tìm hiểu ngay bên dưới.
Lớp Camera.Parameters
cho phép chúng ta thiết lập một số cấu hình trên đối tượng Camera,
ở đây chúng ta chỉ đơn giản là resize lại kích thước ảnh mà camera thu được thành kích thước của SurfaceView,
chúng ta resize bằng phương thức setPreviewSize().
Để thiết lập các thông số này lên đối tượng Camera
thì chúng ta gọi phương thức setParameters.
Phương thức setPreviewDisplay()
sẽ chỉ định cho camera biết ảnh thu được hiện lên đối tượng SurfaceView
nào, phương thức startPreview()
sẽ thông báo cho camera bắt đầu thu hình.
public void changeOrientation() { if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) camera.setDisplayOrientation(0); else camera.setDisplayOrientation(90); }
Trong phương thức changeOrientation(),
chúng ta thay đổi hướng của camera, lý do là vì người dùng có thể xoay ngang (LANDSCAPE)
hoặc xoay dọc (PORTRAIT)
thiết bị của họ nhưng camera thì không tự động xoay được nên ở đây chúng ta tự xoay bằng tay bằng cách gọi phương thức setDisplayOrientation().
public void refreshCamera() { if(surfaceHolder.getSurface() == null) return; try { camera.stopPreview(); } catch(Exception e) {} try { camera.setPreviewDisplay(surfaceHolder); camera.startPreview(); } catch(Exception e) {} }
Chúng ta định nghĩa phương thức refreshCamera()
để tái tạo trạng thái của Camera, bởi vì ở đây chúng ta có nút chụp hình, việc chụp hình rồi lưu file vào máy sẽ làm camera bị “đơ” nên chúng ta cần khởi động lại camera.
@Override public void surfaceDestroyed(SurfaceHolder holder) { camera.stopPreview(); camera.release(); camera = null; }
Trong phương thức surfaceDestroyed(),
chúng ta dừng việc thu hình bằng phương thức stopPreview()
và giải phóng camera bằng phương thức release().
Việc gọi các phương thức này là rất quan trọng, bởi vì khi chúng ta sử dụng camera thì camera đó sẽ bị khóa và các ứng dụng khác sẽ không thể sử dụng được camera đó, nếu chúng ta tắt ứng dụng mà quên không giải phóng camera thì các ứng dụng khác sẽ không thể sử dụng được.
public void onCreate(Bundle savedInstanceState) { ... captureImageCallback = new PictureCallback() { public void onPictureTaken(byte[] data, Camera camera) { FileOutputStream outStream = null; try { outStream = new FileOutputStream(String.format("/sdcard/%d.jpg", System.currentTimeMillis())); outStream.write(data); outStream.close(); } catch(FileNotFoundException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } finally {} Toast.makeText(getApplicationContext(), "Picture saved", Toast.LENGTH_SHORT).show(); refreshCamera(); } }; ... } ... public void captureImage(View view) throws IOException { camera.takePicture(null, null, captureImageCallback); }
Phương thức Camera.takePicture()
sẽ thực hiện việc chụp hình, tham số thứ 3 của phương thức này là một đối tượng PictureCallback,
đối tượng này có phương thức onPictureTaken()
sẽ được gọi mỗi khi chụp, tham số của phương thức onPictureTaken()
là dữ liệu ảnh chụp được lưu trong một mảng byte
và đối tượng camera đã thực hiện chụp hình. Tại đây chúng ta thực hiện lưu ảnh vào file trên thẻ nhớ ngoài.
public void switchCamera(View view) { if(frontCam) { int cameraId = isBackCameraExisted(); ... } else { int cameraId = isFrontcameraExisted(); ... } }
Chúng ta định nghĩa phương thức switchCamera()
để chuyển đổi qua lại giữa các camera có trong máy (như camera trước và camera sau), mỗi camera sẽ được định danh bằng một Id, ở đây chúng ta lấy Id đó rồi truyền vào trong phương thức Camera.open(),
nếu chúng ta không truyền vào Id nào thì phương thức này sẽ tự mở một camera sau mà nó tìm thấy.
public int isFrontCameraExisted() { int cameraId = -1; int numberOfCameras = Camera.getNumberOfCameras(); for(int i = 0 ; i < numberOfCameras ; i++) { CameraInfo info = new CameraInfo(); Camera.getCameraInfo(i, info); if(info.facing == CameraInfo.CAMERA_FACING_FRONT) { cameraId = i; break; } } return cameraId; }
Phương thức isFrontCameraExisted()
cho biết máy có tồn tại camera trước hay không. Phương thức Camera.getNumberOfCameras()
sẽ trả về số lượng camera có trong máy, chúng ta duyệt qua từng camera, với mỗi đối tượng Camera
chúng ta lấy đối tượng lớp CameraInfo,
lớp này lưu những thông tin về từng Camera,
trong đó có thuộc tính facing
cho biết Camera
đó hướng ra trước hay ra sau, chúng ta so sánh với hằng số CAMERA_FACING_FRONT
để biết. Nếu có tồn tại Camera
trước thì chúng ta trả về Id đó, nếu máy không tồn tại camera trước thì phương thức isFrontCameraExisted()
sẽ trả về -1. Tương tự chúng ta định nghĩa phương thức isBackCameraExisted()
để kiểm tra xem máy có tồn tại camera sau không.
@Override public void onConfigurationChanged(Configuration config) { super.onConfigurationChanged(config); changeOrientation(); }
Ngoài ra ở đây chúng ta override phương thức onConfigurationChanged(),
phương thức này được gọi khi có một số thông tin về ứng dụng bị thay đổi trong quá trình chạy, chẳng hạn như hướng màn hình, màn hình bật lên bàn phím ảo, hoặc thay đổi ngôn ngữ… ở đây chúng ta chỉ quan tâm đến sự thay đổi hướng màn hình, nếu người dùng xoay ngang-dọc máy thì chúng ta thay đổi hướng tương ứng của camera.
Android – Music Player
Android cung cấp lớp MediaPlayer
hỗ trợ play các file định dạng nhạc và phim… trong phần này chúng ta sẽ dùng lớp này đẻ xây dựng một ứng dụng nghe nhạc đơn giản.
Project
Chúng ta tạo project như bình thường. Sau đó trong thư mục res, chúng ta tạo một thư mục với tên là raw và đặt vào đó một file MP3 để sử dụng.
Layout
Bây giờ chúng ta thiết kế giao diện cho ứng dụng.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:id="@+id/songName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:text="songName"/> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/remainingTime" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20sp" android:text="00:00"/> <SeekBar android:id="@+id/seekBar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1"/> <TextView android:id="@+id/songDuration" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20sp" android:text="00:00"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="30dp" android:gravity="center_horizontal" android:orientation="horizontal"> <Button android:id="@+id/btnPlay" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="play" android:text="Play"/> <Button android:id="@+id/btnPause" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="pause" android:text="Pause"/> <Button android:id="@+id/btnStop" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="stop" android:text="Stop"/> </LinearLayout> </LinearLayout>
Ứng dụng sẽ có giao diện là 1 TextView
để hiển thị tên file nhạc, 2 TextView
để hiển thị thời gian của file nhạc và thời gian còn lại của file nhạc, 1 SeekBar
để biểu diễn thời lượng của nhạc, 3 Button
là Play, Pause và Stop.
Activity
Cuối cùng là lớp Activity.
package com.phocode; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.media.MediaPlayer; import android.view.View; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; import java.util.concurrent.TimeUnit; public class MainActivity extends Activity implements OnSeekBarChangeListener { private MediaPlayer mediaPlayer; public TextView songName, songDuration, remainingTime; private Handler updateHandler = new Handler(); private SeekBar seekBar; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); songName = (TextView) findViewById(R.id.songName); songName.setText("gregg_lehrman_combust.mp3"); mediaPlayer = MediaPlayer.create(this, R.raw.gregg_lehrman_combust); songDuration = (TextView)findViewById(R.id.songDuration); int minutes = (int)TimeUnit.MILLISECONDS.toMinutes(mediaPlayer.getDuration()); int seconds = (int)TimeUnit.MILLISECONDS.toSeconds(mediaPlayer.getDuration()) - minutes * 60; songDuration.setText(String.format("%02d:%02d",minutes, seconds)); remainingTime = (TextView)findViewById(R.id.remainingTime); seekBar = (SeekBar)findViewById(R.id.seekBar); seekBar.setMax((int) mediaPlayer.getDuration()); seekBar.setOnSeekBarChangeListener(this); updateHandler = new Handler(); updateHandler.postDelayed(update, 100); } public void play(View view) { mediaPlayer.start(); } public void pause(View view) { mediaPlayer.pause(); } public void stop(View view) { mediaPlayer.stop(); mediaPlayer = mediaPlayer.create(this, R.raw.gregg_lehrman_combust); } private Runnable update = new Runnable() { public void run() { long currentTime = mediaPlayer.getCurrentPosition(); seekBar.setProgress((int)currentTime); int minutes = (int)TimeUnit.MILLISECONDS.toMinutes(currentTime); int seconds = (int)TimeUnit.MILLISECONDS.toSeconds(currentTime) - minutes * 60; remainingTime.setText(String.format("%02d:%02d",minutes, seconds)); updateHandler.postDelayed(this, 100); } }; @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if(fromUser) mediaPlayer.seekTo(progress); } @Override public void onStartTrackingTouch(SeekBar seekBar) {} @Override public void onStopTrackingTouch(SeekBar seekBar) {} }
Lớp Activity
sẽ chịu trách nhiệm xử lý việc mở file, tạm dừng, đóng file và cập nhật thời lượng hiện tại của file nhạc.
mediaPlayer = mediaPlayer.create(this, R.raw.gregg_lehrman_combust);
Để tạo một đối tượng MediaPlayer
thì chúng ta dùng phương thức MediaPlayer.create(),
tham số thứ nhất là đối tượng Activity,
tham số thứ 2 là đối tượng file nhạc, ở đây chúng ta khai báo đường dẫn đến file nhạc nhưng không kèm theo đuôi .mp3.
songDuration = (TextView)findViewById(R.id.songDuration); int minutes = (int)TimeUnit.MILLISECONDS.toMinutes(mediaPlayer.getDuration()); int seconds = (int)TimeUnit.MILLISECONDS.toSeconds(mediaPlayer.getDuration()) - minutes * 60; songDuration.setText(String.format("%02d:%02d",minutes, seconds));
Bốn dòng code trên làm công việc lấy thời gian của file nhạc và gán vào TextView dưới dạng mm:ss (phút:giây).
seekBar = (SeekBar)findViewById(R.id.seekBar); seekBar.setMax((int) mediaPlayer.getDuration()); seekBar.setOnSeekBarChangeListener(this);
Tiếp theo chúng ta khởi tạo đối tượng SeekBar,
thiết lập thuộc tính max là thời lượng của file nhạc, sau đó gắn listener cho nó, phương thức getDuration()
sẽ trả về thời lượng nhạc theo mili giây và dữ liệu trả về là kiểu long
nên chúng ta ép qua kiểu
int
(tất nhiên bạn có thể dùng long
luôn cũng được).
updateHandler = new Handler(); updateHandler.postDelayed(update, 100);
Đáng tiếc là lớp MediaPlayer
không phát sinh sự kiện thay đổi thời lượng nhạc, vì thế nếu muốn biết thời lượng hiện tại là bao nhiêu thì chúng ta phải chạy tự gọi phương thức của lớp này để lấy dữ liệu, mà chúng ta lại muốn dùng dữ liệu đó để cập nhật lên SeekBar
thì phải chạy trong một luồng khác, ở đây chúng ta dùng lớp Handler
để làm việc này, cứ sau 1 giây thì chúng ta cập nhật lại SeekBar.
private Runnable update = new Runnable() { public void run() { long currentTime = mediaPlayer.getCurrentPosition(); seekBar.setProgress((int)currentTime); int minutes = (int)TimeUnit.MILLISECONDS.toMinutes(currentTime); int seconds = (int)TimeUnit.MILLISECONDS.toSeconds(currentTime) - minutes * 60; remainingTime.setText(String.format("%02d:%02d",minutes, seconds)); updateHandler.postDelayed(this, 100); } };
Lớp Handler
cho phép chúng ta thực thi các công việc bên trong đối tượng Runnable
hoặc Message trong các luồng khác mà không ảnh hưởng tới luồng chính. Ở đây chúng ta định nghĩa một đối tượng Runnable
tên là update,
đối tượng này sẽ làm công việc lấy thời lượng nhạc hiện tại và cập nhật lên SeekBar, đồng thời cập nhật lại thời gian đó trong các TextView
tương ứng.
public void play(View view) { mediaPlayer.start(); updateHandler.postDelayed(update, 100); }
Phương thức play()
sẽ được gọi khi người dùng click vào Button
“Play”. Tại đây chúng ta cho MediaPlayer
chạy file nhạc bằng cách gọi phương thức start().
public void pause(View view) { mediaPlayer.pause(); } public void stop(View view) { mediaPlayer.stop(); mediaPlayer = mediaPlayer.create(this, R.raw.gregg_lehrman_combust); }
Tương tự, muốn tạm dừng hoặc muốn dừng hẳn thì chúng ta gọi phương thức pause()
và phương thức stop()
tương ứng. Tuy nhiên sau khi gọi stop(),
nếu muốn mở lại nhạc thì chúng ta phải reset lại tất cả trạng thái của đối tượng MediaPlayer,
hoặc nhanh hơn là tạo lại một đối tượng MediaPlayer khác, như vậy mới có thể mở được.
@Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if(fromUser) mediaPlayer.seekTo(progress); }
Lớp MainActivity
của chúng ta có implement giao diện OnSeekBarChangeListener,
mục đích là để bắt sự kiện thay đổi tiến trình trên SeekBar,
bởi vì người dùng có thể dùng tay để điều khiển SeekBar
chứ không chỉ riêng đối tượng Handler,
nên ở đây chúng ta kiểm tra xem nếu người dùng kéo nút trên SeekBar
thì chúng ta cập nhật mới vị trí của nhạc.
Dịch và chạy project, chúng ta có được ứng dụng như hình dưới.
Bạn có thể kết hợp với trình duyệt file trong bài trước để nâng cấp ứng dụng lên cho hoàn thiện hơn.
Android – Trình duyệt File
Trong phần này chúng ta sẽ xây dựng một ứng dụng file explorer (duyệt file) đơn giản.
Chúng ta tạo project với tên FileExplorer,
file Activity
là MainActivity.java,
file layout cho Activity này là main.xml.
Main Layout
Trong file main.xml
chúng ta thiết kế như sau:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" androd:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:id="@+id/txtPath" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Path:" /> <Button android:id="@+id/btnBrowse" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Browse" android:onClick="onClicked"/> </LinearLayout>
Chúng ta thiết kế một TextView
và một Button, TextView
sẽ hiển thị đường dẫn tuyệt đối đến file mà người dùng chọn, Button
được dùng để mở một Activity
khác để duyệt file, phương thức xử lý sự kiện của Button
là onClicked().
MainActivity
Tiếp theo là file MainActivity.java:
package com.phocode; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.content.Intent; import android.widget.TextView; public class MainActivity extends Activity { private static final int FILE_CHOOSER_CODE = 1; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } public void onClicked(View view) { Intent fileChooserIntent = new Intent(this, FileChooserActivity.class); startActivityForResult(fileChooserIntent, FILE_CHOOSER_CODE); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if(requestCode == FILE_CHOOSER_CODE) if(resultCode == RESULT_OK) { String fullPath = data.getStringExtra("FullPath"); TextView path = (TextView) findViewById(R.id.txtPath); path.setText(fullPath); } } }
Lớp MainActivity
sẽ hiển thị màn hình của file layout main.xml
và chịu trách nhiệm mở lớp FileChooserActivity
để duyệt file.
private static final int FILE_CHOOSER_CODE = 1;
Chúng ta định nghĩa code để mở Activity
mới.
public void onClicked(View view) { Intent fileChooserIntent = new Intent(this, FileChooserActivity.class); startActivityForResult(fileChooserIntent, FILE_CHOOSER_CODE); }
Trong phương thức onClicked(),
chúng ta tạo một đối tượng Intent
mới và gọi phương thức startActivityForResult()
để mở lớp FileChooserActivity
mà chúng ta sẽ viết ở dưới.
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if(requestCode == FILE_CHOOSER_CODE) if(resultCode == RESULT_OK) { String fullPath = data.getStringExtra("FullPath"); TextView path = (TextView) findViewById(R.id.txtPath); path.setText(fullPath); } }
Phương thức onActivityResult()
sẽ nhận kết quả trả về của lớp FileChooserActivity
là đường dẫn tuyệt đối của file mà người dùng chọn khi duyệt file, chúng ta lấy đường dẫn đó gán vào TextView.
ListView Layout
Không giống như các bài trước, chúng ta sử dụng ListView
chỉ để hiển thị những chuỗi string đơn lẻ, ở đây mỗi item của ListView
sẽ hiển thị tên file hoặc tên thư mục đi kèm với kích thước file nếu có. Nên chúng chúng ta thiết kế các item này giống như thiết kế một giao diện cho Activity
vậy.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="5dp"> <TextView android:id="@+id/name" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textSize="20sp" android:padding="10dp"/> <TextView android:id="@+id/size" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textSize="10sp"/> </LinearLayout>
Chúng ta sẽ dùng 2 TextView,
một dùng để hiển thị tên file hoặc thư mục, một dùng để hiển thị kích thước file nếu có.
FileChooserActivity
Tiếp theo là lớp FileChooserActivity.
package com.phocode; import android.app.ListActivity; import android.os.Bundle; import android.content.Intent; import android.view.View; import android.widget.ArrayAdapter; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import java.io.File; import java.util.Arrays; import java.util.ArrayList; import java.util.List; import java.util.Collections; public class FileChooserActivity extends ListActivity { private ItemArrayAdapter la; private List<Item> itemArr; private String currentDirectory; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); currentDirectory = "/"; generateContent(currentDirectory); } @Override public void onListItemClick(ListView listView, View view, int position, long id) { Item selectedItem = (Item)getListAdapter().getItem(position); String newPath = ""; if(selectedItem.getName().equals("..")) newPath = new File(currentDirectory).getParent(); else { newPath = currentDirectory; if(currentDirectory.equals("/") == false) newPath += "/"; newPath += selectedItem.getName(); } File newFile = new File(newPath); if(newFile.isDirectory()) { currentDirectory = newPath; generateContent(currentDirectory); } else { Intent intent = new Intent(); intent.putExtra("FullPath", newPath); setResult(RESULT_OK, intent); finish(); } } private void generateContent(String path) { File f = new File(path); ArrayList<File> files = new ArrayList<File>(Arrays.asList(f.listFiles())); int fileSize = files.size(); itemArr = new ArrayList(); if(path.equals("/") == false) itemArr.add(new Item("..", 0)); for(int i = 0 ; i < fileSize ; i++) { String name = files.get(i).getName(); int size; if(files.get(i).isDirectory()) size = 0; else size = (int)files.get(i).length(); itemArr.add(new Item(name, size)); } la = new ItemArrayAdapter(this, itemArr); setListAdapter(la); } }
Lớp FileChooserActivity
sẽ có giao diện để người dùng chọn file hoặc thư mục.
currentDirectory = "/";
Chúng ta định nghĩa biến currentDirectory
dùng để lưu đường dẫn đến file hoặc thư mục được chọn. Mặc định khi chạy ứng dụng thì thư đây là đường dẫn đến thư mục gốc, thư mục gốc trong các hệ điều hành linux và Android là dấu "/"
.
private void generateContent(String path) { ... }
Khi Activity
này được khởi động, phương thức generateContent()
sẽ tìm danh sách các thư mục và file trong tham số path và đưa chúng lên màn hình.
File f = new File(path); ArrayList<File> files = new ArrayList<File>(Arrays.asList(f.listFiles()));
Đầu tiên chúng ta lấy danh sách các thư mục và file và lưu trong một đối tượng ArrayList<File>.
Phương thức File.listFiles()
sẽ trả về danh sách các thư mục và file hiện có dưới dạng mảng, phương thức Arrays.asList()
sẽ chuyển mảng thành kiểu List.
itemArr = new ArrayList(); if(path.equals("/") == false) itemArr.add(new Item("..", 0));
Chúng ta sẽ thực hiện lưu các đối tượng file/thư mục trong danh sách itemArr,
đây là một đối tượng List<Item>,
chúng ta sẽ viết lớp Item
ở dưới. Ngoài ra ở đây chúng ta còn kiểm tra xem thư mục hiện tại mà người dùng đang duyệt có phải là thư mục gốc hay không, nếu không thì chúng ta chèn thêm một đối tượng ".."
nữa, dấu ".."
nghĩa là lùi thư mục hiện tại về một cấp, chẳng hạn như chúng ta đang ở /sdcard/musics thì lùi một cấp sẽ thành /sdcard.
for(int i = 0 ; i < fileSize ; i++) { ... }
Trong vòng lặp for
chúng ta tiến hành kiểm tra xem các đối tượng File
là thư mục hay là file để tạo đối tượng Item
tương ứng. Phương thức length()
sẽ trả về kích thước của file, nếu đối tượng hiện tại là thư mục thì chúng ta gán kích thước là 0.
la = new ItemArrayAdapter(this, itemArr); setListAdapter(la);
Cuối cùng chúng ta thiết lập ArrayAdapter
để ListView
có thể nhận dữ liệu. Ở đây vì ListView
của chúng ta không chỉ hiển thị một phần tử đơn lẻ mà hiển thị nhiều phần tử cùng một lúc nên chúng ta phải tùy chỉnh lại lớp ArrayAdapter.
@Override public void onListItemClick(ListView listView, View view, int position, long id) { ... }
Phương thức onListItemClick()
sẽ tự động được gọi mỗi khi chúng ta click vào một item nào đó trên ListView.
Trong phương thức này chúng ta kiểm tra xem nếu người dùng click vào một thư mục thì chúng ta khởi tạo lại một đối tượng ArrayAdapter
mới, nếu người dùng click vào một file thì chúng ta tắt Activity
tại đây và gửi trả về đường dẫn đến file mà đó, nếu người dùng click vào nút ".."
thì chúng ta cho lùi thư mục về một cấp.
ItemArrayAdapter
package com.phocode; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; import java.util.List; public class ItemArrayAdapter extends ArrayAdapter<Item> { private Context context; private List<Item> values; public ItemArrayAdapter(Context context, List<Item> values) { super(context, R.layout.row, values); this.context = context; this.values = values; } @Override public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View row = inflater.inflate(R.layout.row, parent, false); TextView name = (TextView) row.findViewById(R.id.name); TextView size = (TextView) row.findViewById(R.id.size); Item currentItem = values.get(position); name.setText(currentItem.getName()); if(currentItem.getSize() > 0) size.setText(String.valueOf(currentItem.getSize()) + " bytes"); else size.setText(""); return row; } }
Lớp ItemArrayAdapter
sẽ kế thừa từ lớp ArrayAdapter.
@Override public View getView(int position, View convertView, ViewGroup parent) { ... }
Chúng ta tùy chỉnh cách mà các item sẽ được hiển thị trong phương thức getView().
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View row = inflater.inflate(R.layout.row, parent, false); TextView name = (TextView) row.findViewById(R.id.name); TextView size = (TextView) row.findViewById(R.id.size);
Chúng ta lấy 2 phần tử TextView
trong file layout.
name.setText(currentItem.getName()); if(currentItem.getSize() > 0) size.setText(String.valueOf(currentItem.getSize()) + " bytes"); else size.setText("");
Với mỗi item trên ListView,
nếu item đó là file thì chúng ta hiển thị tên file và kích thước file, còn nếu là thư mục thì chúng ta không cho hiển thị kích thước.
Item
Cuối cùng là lớp Item.
package com.phocode; public class Item { private String name; private int size; public Item() { this.name = ""; this.size = 0; } public Item(String name, int size) { this.name = name; this.size = size; } public String getName() { return this.name; } public int getSize() { return this.size; } public void setName(String name) { this.name = name; } public void setSize(int size) { this.size = size; } }
Lớp Item
là lớp dùng để lưu thông tin về từng Item trên ListView,
bao gồm tên file/thư mục và kích thước file đó.