Category Archives: Android-Lập trình Android

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à:

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ý.

Screenshot_2016-08-15-09-32-38

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()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:

Screenshot_2016-06-07-10-38-58

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.

Capture

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 ActivityMainActivity đượ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ả:

Screenshot_2016-06-03-18-38-45

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ức startService() của một đối tượng Activity. Khi đã được tạo ra thì đối tượng Service 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ức bindService(). Các đối tượng Service 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ượng Service đó. 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()onDestroy(). Ở đây chúng ta chỉ in ra các câu thông báo Toast đơn giản.

Screenshot_2016-06-01-14-48-56

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()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.

Screenshot_2016-05-31-13-40-53

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.

Capture

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 ButtonPlay, PauseStop.

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.

Screenshot_2016-05-29-14-49-28

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 ActivityMainActivity.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 ButtononClicked().

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 đó.

Screenshot_2016-05-28-17-51-26 Screenshot_2016-05-28-17-51-35

 

Android – Đồ họa

Android cũng hỗ trợ các thư viện cho phép vẽ các đối tượng đồ họa 2D lên màn hình.

Để vẽ thì chúng ta gọi các phương thức vẽ bên trong phương thức onDraw() của lớp android.view.View, mỗi đối tượng View có một đối tượng Canvas của riêng nó để vẽ các đối tượng hình học như hình tròn, hình vuông, điểm… hoặc chúng ta có thể định nghĩa “cứng” ngay trong một file XML.

Ví dụ 1

Chúng ta sẽ vẽ hình tròn trên một đối tượng View, ở đây hình tròn sẽ được định nghĩa bên trong một file XML.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" 
    android:shape="oval">
    
    <solid 
        android:color="#666666"/>
    <size 
        android:width="70dp"
        android:height="70dp"/>
</shape> 

Chúng ta tạo một file có tên circle.xml và bên trong một thư mục drawable nào đó, chẳng hạn như thư mục drawable-hdpi. Trong file này chúng ta khai báo một thẻ shape, thẻ này sẽ định nghĩa một đối tượng hình học nào đó như hình tròn, hình vuông… chúng ta thiết lập đối tượng hình học đó trong thuộc tính android:shape, ở đây giá trị oval tức là vẽ hình tròn, ngoài ra còn 3 loại nữa là rectangle (hình chữ nhật), line (đường thẳng) và ring (hình vòng bánh xe). Thẻ solid quy định màu vẽ, thẻ size quy định kích thước của đối tượng hình học.

<?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">        

    <View 
        android:layout_width="100dp"
        android:layout_height="100dp" 
        android:layout_marginTop="5dp" 
        android:background="@drawable/oval"/>      
</LinearLayout>

Trong file main.xml. Chúng ta khai báo một thẻ View và định nghĩa thuộc tính android:background là tên file circle.xml, Android sẽ tự động tìm file này trong tất cả các thư mục drawable của nó.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;

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);
    }
}

Trong file Activity chúng ta không cần chỉnh sửa gì cả.

Screenshot_2016-05-25-08-34-58

Ví dụ 2

Trong ví dụ này chúng ta sẽ vẽ hình chữ nhật trên một lớp View. Công đoạn vẽ sẽ được thực hiện trong phương thức onDraw() của lớp View chứ không được định nghĩa trước trong file XML nữa.

package com.phocode;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;

public class Graphics extends View
{
    Paint p = new Paint();
 
    public Graphics(Context context)
    {
        super(context);
        p.setColor(Color.GREEN);
    }
 
    @Override
    public void onDraw(Canvas canvas)
    {
        canvas.drawRect(30, 30, 100, 100, p);
    }
}

Đầu tiên chúng ta định nghĩa lớp Graphics kế thừa từ lớp View. File Graphics.java nằm cùng thư mục với file Activity.

Paint p = new Paint();

Trong lớp này chúng ta định nghĩa một đối tượng Paint. Đối tượng này sẽ định nghĩa màu vẽ.

p.setColor(Color.GREEN);

Dòng code trên sẽ chuyển màu dùng để vẽ của đối tượng Paint sang màu xanh lá.

@Override
public void onDraw(Canvas canvas) 
{
    canvas.drawRect(30, 30, 100, 100, p);
}

Các phương thức vẽ được gọi trong phương thức onDraw(). Ở đây phương thức drawRect() sẽ vẽ một hình chữ vuông lên bề mặt của đối tượng View.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.graphics.Color;

public class MainActivity extends Activity
{
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
 
        Graphics view = new Graphics(this);
        view.setBackgroundColor(Color.WHITE);
        setContentView(view);
    }
}

Trong file MainActivity.java, chúng ta tạo một đối tượng View riêng (bằng lớp Graphics) rồi truyền vào phương thức setContentView() chứ không phải là một file layout nào khác.

view.setBackgroundColor(Color.WHITE);

Phương thức setBackgroundColor() sẽ thiết lập màu nền cho đối tượng View.

Screenshot_2016-05-25-08-54-41

Android – Hộp thoại

Hộp thoại (thuật ngữ là Dialog) trong Window là một cửa sổ hiện ra khi cần thông báo cho người dùng, ngoài ra hộp thoại còn có thể nhận dữ liệu, thao tác với dữ liệu đó… hộp thoại trong Android cũng không khác mấy. Trong Android chúng ta dùng lớp AlertDialog để hiển thị hộp thoại, tuy nhiên chúng ta không tạo đối tượng AlertDialog trực tiếp từ lớp này mà dùng một lớp kế thừa từ lớp này là Builder.

Hiển thị thông báo

Chúng ta sẽ sử dụng lớp AlertDialog để hiển thị hộp thoại thông báo.

Ví dụ:

<?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:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:layout_marginTop="10dip" 
        android:onClick="onClicked" 
        android:text="Show" />
        
</LinearLayout>

Trong file main.xml, chúng ta thiết kế một Button, khi click Button này thì hiển thị hộp thoại, phương thức xử lý sự kiện click là onClicked().

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.app.AlertDialog;
import android.content.DialogInterface;

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 onClicked(View view)
    { 
        AlertDialog ad = new AlertDialog.Builder(this).create();
 
        ad.setTitle("Dialog Example");
        String msg = String.format("Hello World");
        ad.setMessage(msg);
        ad.setIcon(android.R.drawable.ic_dialog_info);
 
        ad.setButton("OK", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which)
            {
                 dialog.cancel();
            }
        });
 
        ad.show();
    } 
}

Chúng ta dùng lớp AlertDialog để hiển thị một hộp thoại thông báo đơn giản.

AlertDialog ad = new AlertDialog.Builder(this).create();

Chúng ta tạo đối tượng AlertDialog từ lớp con của nó là Builder.

ad.setTitle("Dialog Example");
String msg = "Hello World";
ad.setMessage(msg);
ad.setIcon(android.R.drawable.ic_dialog_info);

Tiếp theo chúng ta thiết lập tiêu đề cho hộp thoại bằng phương thức setTitle(), thiết lập đoạn text thông báo bằng phương thức setMessage(), thiết lập icon cho hộp thoại bằng phương thức setIcon().

ad.setButton("OK", new DialogInterface.OnClickListener() {
    public void onClick(DialogInterface dialog, int which) {
        dialog.cancel();
    }
});

Mặc định hộp thoại được tạo ra sẽ không có nút nào để bấm cả, muốn thêm nút bấm thì chúng ta gọi phương thức setButton() với tham số đầu tiên là tên nút, tham số thứ 2 là đối tượng giao diện DialogInterface, giao diện này có lớp OnClickListener với phương thức onClick() dùng để lắng nghe sự kiện click Button, ở đây chúng ta chỉ làm công việc là hủy hộp thoại bằng phương thức cancel().

ad.show();

Khi nào cần hiển thị hộp thoại thì chúng ta gọi phương thức show().

Screenshot_2016-05-24-08-56-48

Nhận dữ liệu từ hộp thoại

Chúng ta cũng có thể dùng hộp thoại để nhận dữ liệu từ người dùng.

Ví dụ:

<?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/btnId" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:layout_marginTop="10dip"
        android:onClick="onClicked" 
        android:text="Show" />   
        
    <TextView 
        android:id="@+id/tvId" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" />
        
</LinearLayout>

Trong file main.xml chúng ta thiết kế một Button và một TextView, click vào Button sẽ hiển thị hộp thoại có EditText để người dùng nhập văn bản vào, TextView sẽ hiển thị đoạn văn bản mà người dùng nhập vào.

package com.phocode;
import android.app.Activity;
import android.os.Bundle;
import android.app.AlertDialog;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.content.DialogInterface;

public class MainActivity extends Activity
{
    private TextView tv;
 
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        tv = (TextView) findViewById(R.id.tvId);
    }

    public void onClicked(View view)
    {
        AlertDialog.Builder ad = new AlertDialog.Builder(this);

        ad.setTitle("Dialog Example");
        ad.setMessage("Enter your name");

        final EditText input = new EditText(this);
        ad.setView(input);

        ad.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dlg, int which) {
            String val = input.getText().toString();
            String msg = String.format("Hello %s!", val);
            tv.setText(msg);
          }
        });

        ad.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
          public void onClick(DialogInterface dlg, int which) {
            dlg.cancel();
          }
        });

        ad.show();
    }
}

Click vào Button sẽ hiển thị hộp thoại có ô EditText để người dùng nhập văn bản.

final EditText input = new EditText(this);
ad.setView(input);

Chúng ta khai báo EditText để hiển thị trên hộp thoại, chúng ta sẽ cần tham chiếu đến đối tượng này trong lớp nội nên phải khai báo final. Để hiển thị một View lên hộp thoại thì chúng ta dùng phương thức setView().

ad.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dlg, int which) {
    String val = input.getText().toString();
    String msg = String.format("Hello %s!", val);
    tv.setText(msg);
    }
});

ad.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
    public void onClick(DialogInterface dlg, int which) {
    dlg.cancel();
    }
});

Hộp thoại trong Android chỉ hiển thị được 1 hoặc 2 hoặc 3 Button chứ không hiển thị được nhiều hơn. Mặc định phương thức setButton() của lớp AlertDialog chỉ thiết lập 1 Button duy nhất, muốn hiển thị 2 hoặc 3 Button thì chúng ta phải truyền vào hằng số AlertDialog.BUTTON_POSITIVE, AlertDialog.BUTTON_NEGATIVE hoặc AlertDialog.BUTTON_NEUTRAL, 3 hằng số này thực ra cũng không mang nhiều ý nghĩa lắm, chúng tượng trưng cho các nút Yes, No, Cancel hay đại loại thế. Tuy nhiên ở đây chúng ta không dùng lớp AlertDialog mà dùng lớp Builder. Mặc định lớp này không có phương thức setButton() mà có 2 phương thức setPositiveButton()setNegativeButton() để thiết lập các Button. Tham số của 2 phương thức này cũng giống như với phương thức setButton(). Ở đây chúng ta sẽ lấy chuỗi trong EditText và gán vào TextView khi click nút Ok, hoặc hủy hộp thoại nếu click nút Cancel.

Screenshot_2016-05-24-09-16-28