Author Archives: Phở Code

Qt 5 C++ – QtWebEngine

Trong Qt có module QtWebEngine cung cấp các lớp C++ hỗ trợ dựng trang web từ mã HTML, XML, SVG, CSS, Javascript lên một chương trình Qt, dùng cho các hệ điều hành không có sẵn web engine. Đối với các hệ điều hành có sẵn web engine thì chúng ta có thể dùng module QtWebKit, tuy nhiên module này đã bị loại bỏ khỏi Qt 5.5, nên hầu như chúng ta sẽ dùng module QtWebEngine để làm việc với web.

Tổng quan về Qt WebEngine

Kiến trúc:

qtwebengine-architecture

Theo sơ đồ trên thì module QtWebEngine bao gồm những thành phần sau:

  • Qt WebEngine Widgets: cung cấp các thư viện tạo giao diện người dùng với Widget.
  • Qt WebEngine: cung cấp các lớp tạo giao diện người dùng với QtQuick.
  • Qt WebEngine Core: làm việc với Chromium.
  • Qt WebEngine Process: dựng trang, xử lý Javascript.

Module Qt WebEngine Core được xây dựng dựa trên Chromium. Các thành phần của Chromium chịu trách nhiệm trong việc xử lý kết nối mạng, xây dựng trang web, xử lý Javascript, HTML… Ngoài WebEngine thì Qt còn có module WebKit cũng có chức năng thao tác với trang web nhưng rất hạn chế, WebEngine nặng hơn, cung cấp nhiều chức năng thao tác trực tiếp với web hơn WebKit. Dù QtWebEngine được xây dựng trên Chromium nhưng không có nghĩa là module này có các dịch vụ hỗ trợ thêm như của trình duyệt Chrome do Google phát triển.

Cài đặt

Mặc định module QtWebEngine không có sẵn trong Qt Creator sử dụng trình biên dịch MinGW, bạn phải chuyển qua sử dụng Qt Creator dùng trình biên dịch MSVC hoặc sử dụng Add-In trong Visual Studio.

Capture Capture1

Hoặc có một cách khác là biên dịch lại toàn bộ mã nguồn Qt và Chromium sử dụng trình biên dịch MinGW, cách này rất phức tạp nên mình sẽ không đề cập ở đây.

Ngoài ra Visual Studio Add-In cũng không chấp nhận Visual Studio phiên bản Express (hoặc Community) nên cách này cũng khó khả thi (trừ khi bạn dùng crack). Nên tóm lại trong phần này mình sẽ sử dụng Qt Creator với trình biên dịch msvc từ Visual Studio 2015.

Lưu ý: tại thời điểm viết bài này trình biên dịch msvc đi kèm với Qt Creator khi download trên trang chủ không sử dụng được, mình phải cài Visual Studio 2015 Community rồi chỉ định Qt Creator sử dụng trình biên dịch của VS mới chạy được (khi cài Visual Studio 2015 bạn phải tích chọn cài 2 thành phần là Programming Languages→Visual C++Windows and Web Development→Universal Windows App Developments Tools vì đây là 2 thành phần cần thiết để biên dịch trong Qt nhưng không được cài mặc định).

Bây giờ chúng ta sẽ đi tìm hiểu một số chức năng có trong Qt WebEngine.

Mở trang web

Để mở một trang web và hiển thị thì chúng ta dùng lớp QWebEngineView. Chúng ta tạo một project mới.

...
QT += webenginewidgets
...

Trong file .pro chúng ta phải khai báo module QtWebEngine bằng dòng code trên.

#include <QMainWindow>

#include <QWebEngineView>

class Browser : public QMainWindow
{
    Q_OBJECT
public:
    explicit Browser(QWidget *parent = 0);

private:
    QWebEngineView *view;
};

Chúng ta định nghĩa lớp Browser kế thừa từ lớp QMainWindow, bên trong lớp này có một đối tượng QWebEngineView.

#include "browser.h"

Browser::Browser(QWidget *parent) : QMainWindow(parent)
{
    view = new QWebEngineView(this);
    view->load(QUrl("https://phocode.com"));
    setCentralWidget(view);
}

Để tải một trang web về thì chúng ta chỉ cần gọi phương thức load() và truyền vào một đối tượng QUrl. Phương thức setCentralWidget() sẽ thiết lập đối tượng QWebEngineView làm giao diện chính của cửa sổ.

#include "browser.h"
#include <QApplication>
#include <qtwebenginewidgetsglobal.h>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    Browser browser;
    browser.show();

    return a.exec();
}

Trong file main.cpp chúng ta chỉ tạo đối tượng Browser rồi gọi phương thức show() như bình thường.

Capture

Một số tính năng của QWebEngineView

Lớp QWebEngineView có sẵn một số tính năng cơ bản của một trình duyệt web. Chúng ta sẽ tìm hiểu qua ví dụ dưới đây:

#include <QMainWindow>
#include <QWebEngineView>
#include <QLineEdit>
#include <QUrl>
#include <QToolBar>

class Browser : public QMainWindow
{
    Q_OBJECT
public:
    explicit Browser(QWidget *parent = 0);

signals:

public slots:
    void adjustLocation();
    void changeLocation();
    void adjustTitle();
    void setProgress(int p);
private:
    QWebEngineView *view;
    QLineEdit   *locationEdit;
    int progress;
};

Chúng ta định nghĩa lại lớp Browser. Lớp QWebEngineView có một số Signal đặc biệt được phát đi trong quá trình duyệt trang web nên chúng ta có thể định nghĩa một số phương thức Slot và thuộc tính để bắt và xử lý các signal đó.

#include "browser.h"

Browser::Browser(QWidget *parent) : QMainWindow(parent)
{
    view = new QWebEngineView(this);
    view->load(QUrl("http://google.com"));
    connect(view, SIGNAL(loadFinished(bool)), this, SLOT(adjustLocation()));
    connect(view, SIGNAL(titleChanged(QString)), this, SLOT(adjustTitle()));
    connect(view, SIGNAL(loadProgress(int)), this, SLOT(setProgress(int)));

    locationEdit = new QLineEdit(this);
    locationEdit->setSizePolicy(QSizePolicy::Expanding, locationEdit->sizePolicy().verticalPolicy());
    connect(locationEdit, SIGNAL(returnPressed()), SLOT(changeLocation()));

    QToolBar *toolBar = addToolBar(tr("Navigation"));
    toolBar->addAction(view->pageAction(QWebEnginePage::Back));
    toolBar->addAction(view->pageAction(QWebEnginePage::Forward));
    toolBar->addAction(view->pageAction(QWebEnginePage::Reload));
    toolBar->addAction(view->pageAction(QWebEnginePage::Stop));
    toolBar->addWidget(locationEdit);

    setCentralWidget(view);
}

void Browser::adjustLocation()
{    
    locationEdit->setText(view->url().toString());
}

void Browser::changeLocation()
{
    QUrl url = QUrl::fromUserInput(locationEdit->text());
    view->load(url);
    view->setFocus();
}

void Browser::adjustTitle()
{
    if(progress <= 0 || progress >= 100)
        setWindowTitle(view->title());
    else
        setWindowTitle(QString("%1 (%2%)").arg(view->title()).arg(progress));
}

void Browser::setProgress(int p)
{
    progress = p;
    adjustTitle();
}

Trong file browser.cpp chúng ta xây dựng lớp Browser thành một trình duyệt web đơn giản.

connect(view, SIGNAL(loadFinished(bool)), this, SLOT(adjustLocation()));
connect(view, SIGNAL(titleChanged(QString)), this, SLOT(adjustTitle()));
connect(view, SIGNAL(loadProgress(int)), this, SLOT(setProgress(int)));

Tín hiệu loadFinished() được phát đi khi QWebEngineView đã tải xong trang web. Chúng ta xử lý bằng phương thức adjustLocation(). 

Tín hiệu titleChanged() được phát đi khi tiêu đề của trang web thay đổi, tức là gần như mỗi lần loadFinished() được phát đi thì titleChanged() cũng được phát đi cùng luôn.

Tín hiệu loadProgress() được phát đi khi mỗi phần tử của trang web được tải xong, ví dụ một trang web mang theo rất nhiều file ảnh, file javascript, file css… mỗi lần những file này được tải xong thì tín hiệu này được phát đi, tín hiệu này mang theo một con số từ 0 đến 100 có nghĩa là trang web đang được tải bao nhiêu %.

locationEdit = new QLineEdit(this);
locationEdit->setSizePolicy(QSizePolicy::Expanding, locationEdit->sizePolicy().verticalPolicy());
connect(locationEdit, SIGNAL(returnPressed()), SLOT(changeLocation()));

Ở đây chúng ta định nghĩa một đối tượng QLineEdit dùng làm thanh địa chỉ của trình duyệt web. Phương thức setSizePolicy() sẽ quy định kích thước của QLineEdit. Tín hiệu returnPressed() được phát đi khi chúng ta gõ phím Enter trên QLineEdit, chúng ta bắt tín hiệu này bằng slot changeLocation().

QToolBar *toolBar = addToolBar(tr("Navigation"));
toolBar->addAction(view->pageAction(QWebEnginePage::Back));
toolBar->addAction(view->pageAction(QWebEnginePage::Forward));
toolBar->addAction(view->pageAction(QWebEnginePage::Reload));
toolBar->addAction(view->pageAction(QWebEnginePage::Stop));
toolBar->addWidget(locationEdit);

Ở đây chúng ta tạo thêm một thanh toolbar, ngoài việc dùng để chứa thanh địa chỉ ở trên, chúng ta còn dùng thêm các đối tượng Action có sẵn của lớp QWebEnginePage, lớp này có thuộc tính QWebEnginePage::WebAction cung cấp một số đối tượng Action có các chức năng cơ bản của một trình duyệt web và thậm chí là có cả icon sẵn. Ví dụ ở trên Back là nút quay lại trang vừa xem, Forward là nút đi tới trang vừa mở, Reload là nút refresh trình duyệt, Stop là nút dừng tải trang.

void Browser::adjustLocation()
{    
    locationEdit->setText(view->url().toString());
}

Phương thức adjustLocation() chỉ đơn giản là thiết lập lại đoạn text trong thanh địa chỉ.

void Browser::changeLocation()
{
    QUrl url = QUrl::fromUserInput(locationEdit->text());
    view->load(url);
    view->setFocus();
}

Phương thức changeLocation() sẽ được gọi khi chúng ta gõ phím Enter trên thanh địa chỉ, lúc này chúng ta tiến hành tải lại trang web.

void Browser::adjustTitle()
{
    if(progress <= 0 || progress >= 100)
        setWindowTitle(view->title());
    else
        setWindowTitle(QString("%1 (%2%)").arg(view->title()).arg(progress));
}

Phương thức adjustTitle() sẽ được gọi khi trang web được tải xong, hoặc một phần của trang web được tải xong, vì ở đây chúng ta chèn thêm số % trang web đã được tải là bao nhiêu.

void Browser::setProgress(int p)
{
    progress = p;
    adjustTitle();
}

Trong lớp Browser chúng ta định nghĩa thuộc tính progress dùng để lưu số % trang web đã được tải. Phương thức setProgress() sẽ được gọi khi QWebEngineView phát tín hiệu loadProgress(), lúc này chúng ta thiết lập thông số mới cho thuộc tính progress, đồng thời gọi lại phương thức adjustTitle().

#include "browser.h"
#include <QApplication>
#include <qtwebenginewidgetsglobal.h>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    Browser browser;
    browser.show();

    return a.exec();
}

Trong file main.cpp chúng ta chỉ tạo đối tượng Browser và gọi phương thức show().

Untitled

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

Android – Menu

Menu có chức năng hiển thị một danh sách các nút lệnh có liên quan với nhau. Trong các hệ điều hành như Windows, Linux, MacOS… thì menu thường xuất hiện trên thanh menu ở đầu cửa sổ.

Trong Android có 3 loại menu: options (menu tùy chọn), context (menu ngữ cảnh) và menu popup. Chúng ta có thể định nghĩa menu trong file XML và dùng lớp MenuInflater để lấy dữ liệu từ file XML về, hoặc có thể code tay trong Java.

Menu tùy chọn – Options

Menu tùy chọn được hiển thị khi chúng ta bấm nút menu trên thiết bị, thường thì nút này có dạng 3 hoặc 4 dấu gạch ngang.

NKoyD

Ví dụ:

Trong ví dụ dưới đây, chúng ta sẽ tạo menu tùy chọn có 2 item, click vào các item thì dùng Toast để hiển thị thông báo tương ứ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:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="Options Menu" />
</LinearLayout>

File layout chính không có gì đặc biệt cả, chỉ có một TextView với dòng chữ mô tả đơn giản.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">MainActivity</string>    
    <string name="menu1">Settings</string>
    <string name="menu2">Tools</string>
</resources>

Trong file strings.xml chúng ta định nghĩa 2 biến String dùng làm tiêu đề cho từng menu item.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/settings" 
        android:title="@string/menu1" />
    <item android:id="@+id/tools" 
        android:title="@string/menu2" />
</menu>

Để tạo danh sách các item của menu thì chúng ta tạo trong một file xml riêng và đặt trong thư mục res/menu. Nếu chưa có thư mục này thì chúng ta tự tạo bằng tay. Bên trong file này chúng ta sử dụng thẻ menu để khai báo menu và thẻ item để khai báo các item trong menu đó.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;

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

    @Override
    public boolean onCreateOptionsMenu(Menu menu) 
    {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.options_menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) 
    {
        switch (item.getItemId()) 
        {
            case R.id.settings:
                Toast.makeText(MainActivity.this, "Settings", 
                    Toast.LENGTH_SHORT).show();
                return true;

            case R.id.tools:
                Toast.makeText(MainActivity.this, "Tools", 
                    Toast.LENGTH_SHORT).show();
                return true;

            default:
                return super.onOptionsItemSelected(item);
        }
    }
}

Để có thể sử dụng menu tùy chọn thì chúng ta phải override lại 2 phương thức của lớp ActivityonCreateOptionsMenu() và onOptionsItemSelected().

@Override
public boolean onCreateOptionsMenu(Menu menu) 
{
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.options_menu, menu);
    return true;
}

Phương thức onCreateOptionsMenu() sẽ thực hiện các công việc khởi tạo menu cho đối tượng Activity, ở đây chúng ta dùng phương thức inflate() của lớp android.view.MenuInflater để lấy dữ liệu của menu từ file options_menu.xml về sử dụng. Phương thức này nhận vào một đối tượng android.view.Menu.

@Override
public boolean onOptionsItemSelected(MenuItem item) 
{
...
}

Phương thức onOptionsItemSelected() sẽ xử lý sự kiện click menu. Phương thức này nhận vào một đối tượng android.view.MenuItem.

case R.id.settings:
    Toast.makeText(MainActivity.this, "Settings", 
        Toast.LENGTH_SHORT).show();
    return true;

Ở đây chúng ta chỉ đơn giản là dùng lớp android.widget.Toast để hiển thị thông báo item nào đã được click.

Screenshot_2016-05-23-11-03-44

Menu ngữ cảnh – Context

Menu ngữ cảnh hiển thị nổi lên trên màn hình nhưng được gắn với một đối tượng View nào đó chứ không liên quan gì tới Activity, menu ngữ cảnh sẽ hiện ra khi chúng ta click và giữ tay trên View đó trong một khoảng thời gian.

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">
    <ListView 
        android:id="@+id/lvId" 
        android:layout_width="fill_parent" 
        android:layout_height="fill_parent" />     
</LinearLayout>

Ở đây chúng ta sẽ gắn menu ngữ cảnh lên ListView.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/delId" 
        android:title="Delete"/>
    <item android:id="@+id/upId" 
        android:title="Lowercase"/>
    <item android:id="@+id/lowId" 
        android:title="Uppercase"/>
</menu>

Cách định nghĩa menu ngữ cảnh cũng không khác gì so với menu tùy chọn.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">MainActivity</string>
    <string-array name="languages">
        <item>Python</item>
        <item>Java</item>
        <item>Ruby</item>
        <item>C++</item>
    </string-array>
</resources>

Trong file strings.xml chúng ta định nghĩa danh sách các item dùng cho ListView.

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent" 
    android:padding="10dp" 
    android:textSize="20sp">
</TextView>

File row.xml sẽ định nghĩa cách các item trong ListView được hiển thị như thế nào.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter; 
import android.widget.ListView; 
import android.view.View;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.Toast;

import java.util.Arrays;
import java.util.ArrayList;

public class MainActivity extends Activity
{
    private ListView lv;
    private ArrayAdapter<String> la;
 
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        lv = (ListView)findViewById(R.id.lvId);
        String[] languages = getResources().getStringArray(R.array.languages);
        ArrayList<String> lst = new ArrayList<String>();
        lst.addAll(Arrays.asList(languages));
 
        la = new ArrayAdapter<String>(this, R.layout.row, lst);
        lv.setAdapter(la);
        registerForContextMenu(lv);
    }
 
    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) 
    {       
        super.onCreateContextMenu(menu, v, menuInfo);
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.context_menu, menu);
    }
 
    @Override
    public boolean onContextItemSelected(MenuItem item) 
    {
        AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
 
        int pos = info.position;
        String i = la.getItem(pos);

        switch (item.getItemId()) 
        {
        case R.id.delId:
            la.remove(i);
            return true;

        case R.id.upId: 
            String upln = i.toUpperCase();
            la.remove(i);
            la.insert(upln, pos); 
            return true;

        case R.id.lowId:
            String lpln = i.toLowerCase();
            la.remove(i);
            la.insert(lpln, pos); 
            return true;

        default:
            return super.onContextItemSelected(item);
        }
    }
}

Để có thể sử dụng menu ngữ cảnh thì chúng ta phải override 2 phương thức là onCreateContextMenu() và onContextItemSelected().

String[] languages = getResources().getStringArray(R.array.languages);
ArrayList<String> lst = new ArrayList<String>();
lst.addAll(Arrays.asList(languages));

Chúng ta lưu các item của ListView trong một đối tượng ArrayList.

registerForContextMenu(lv);

Muốn dùng menu ngữ cảnh cho View nào thì chúng ta gọi phương thức registerForContextMenu() rồi truyền vào đối tượng View đó.

@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) 
{
    super.onCreateContextMenu(menu, v, menuInfo);
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.context_menu, menu);
}

Phương thức onCreateContextMenu() cho menu ngữ cảnh cũng tương tự như phương thức onCreateOptionsMenu() cho menu tùy chọn vậy, ở đây chúng ta dùng lớp MenuInflater để lấy dữ liệu của menu từ file context_menu.xml.

@Override
public boolean onContextItemSelected(MenuItem item) 
{
...
}

Phương thức onContextItemSelected() sẽ làm nhiệm vụ xử lý sự kiện click trên từng item.

AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();

int pos = info.position;
String i = la.getItem(pos);

Lớp AdapterContextMenuInfo có thể lấy một vài thông tin từ lớp MenuItem, trong đoạn code trên chúng ta lấy ra số thứ tự của item trong danh sách và đoạn text của item đó.

case R.id.delId:
    la.remove(i);
    return true;

Trong câu lệnh switch...case, chúng ta kiểm tra xem người dùng đã click vào menu item nào, nếu click vào nút Delete thì chúng ta xóa item của ListView đi.

case R.id.upId:               
    String upln = i.toUpperCase();
    la.remove(i);
    la.insert(upln, pos); 
    return true;

Nếu click vào nút Uppercase thì chúng ta chuyển đoạn text của item trong ListView thành viết hoa, nút Lowercase là viết thường.

Screenshot_2016-05-23-14-25-12

Menu Popup

Menu Popup được hiển thị ngay tại vị trí của View.

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

Trong file main.xml chúng ta thiết kế một Button và một TextView. Button sẽ hiển thị menu Popup khi được click vào, phương thức xử lý sự kiện click của ButtononClick(). TextView chỉ làm nhiệm vụ hiển thị item nào của menu đã được click thôi.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">MainActivity</string>
    <string name="btn_label">Show menu</string>
    <string name="item1">Item 1</string>
    <string name="item2">Item 2</string>
</resources>

Trong file strings.xml chúng ta định nghĩa một vài biến để làm ID cho Button và làm chuỗi hiển thị menu item.

Menu Popup cũng được thiết kế từ các thẻ menu và item không khác gì với menu ngữ cảnh và menu tùy chọn, ở đây chúng ta thiết kế menu trong file popup_menu.xml, file này được đặt trong thư mục res/menu.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.MenuItem;
import android.widget.TextView;
import android.widget.PopupMenu;
import android.widget.PopupMenu.OnMenuItemClickListener;

public class MainActivity extends Activity
    implements OnMenuItemClickListener
{
    private TextView tv;
 
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        tv = (TextView) findViewById(R.id.tvId);
    }
 
    public void onClick(View v)
    { 
        PopupMenu pm = new PopupMenu(this, v);
        pm.getMenuInflater().inflate(R.menu.popup_menu, pm.getMenu());
        pm.setOnMenuItemClickListener(this);
        pm.show();
    }
 
    @Override
    public boolean onMenuItemClick(MenuItem item)
    {
        tv.setText(item.toString() + " selected");
        return true;
    }
}

Chúng ta dùng lớp android.widget.PopupMenu để thao tác với menu.

public void onClick(View v) 
{   
    PopupMenu pm = new PopupMenu(this, v);
    pm.getMenuInflater().inflate(R.menu.popup_menu, pm.getMenu());
    pm.setOnMenuItemClickListener(this);
    pm.show();
}

Trong phương thức onClick(), chúng ta sẽ hiển thị popup menu bằng lớp MenuInflater giống như menu ngữ cảnh và menu tùy chọn. Ngoài ra ở đây chúng ta phải gắn listener cho đối tượng PopupMenu này thông qua phương thức setOnMenuItemClickListener(), khác với menu ngữ cảnh và menu tùy chọn là 2 loại menu này đã có sẵn trong ActivityView nên chúng ta không cần gọi trực tiếp ra như đối tượng menu popup. Sau khi tạo PopupMenu, chúng ta phải gọi phương thức show() nếu muốn hiện menu này ra.

@Override
public boolean onMenuItemClick(MenuItem item) 
{           
    tv.setText(item.toString() + " selected");
    return true;  
}

Bên trong phương thức onMenuItemClick(), chúng ta thiết lập giá trị của TextView là giá trị của menu item đã được click.

Screenshot_2016-05-23-15-53-05

Android – Picker

Trong phần này chúng ta sẽ tìm hiểu về Picker. Picker là lớp View cho phép chúng ta chọn một giá trị nào đó trong một tập các giá trị cho trước, có 2 loại giá trị là số và giờ/ngày tháng.

NumberPicker

NumberPicker cho phép chúng ta chọn một giá trị số trong một tập giá trị cho trướ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">
    
    <NumberPicker android:id="@+id/npId" 
        android:layout_marginTop="5dp" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" />

    <TextView android:id="@+id/tvId" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:layout_marginTop="5dp" 
        android:layout_marginLeft="5dp" 
        android:text="0" android:textSize="30sp" />              
</LinearLayout>

Trong file layout chúng ta khai báo thẻ NumberPicker và thẻ TextView, TextView sẽ hiển thị giá trị đang được chọn trong NumberPicker. 

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.widget.NumberPicker;
import android.widget.TextView;
import android.widget.NumberPicker.OnValueChangeListener;

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

        NumberPicker np = (NumberPicker) findViewById(R.id.npId);

        np.setOnValueChangedListener(new OnValueChangeListener()
        {
            public void onValueChange(NumberPicker picker, int oldVal, 
                int newVal)
            {
                tv.setText(String.valueOf(newVal)); 
            }        
        });

        np.setMaxValue(100);
        np.setMinValue(0);
    }
}

NumberPicker hiển thị giá trị và 2 nút cộng trừ để tăng giảm giá trị.

np.setOnValueChangedListener(new OnValueChangeListener()
{
    public void onValueChange(NumberPicker picker, int oldVal, 
        int newVal)
    {
        tv.setText(String.valueOf(newVal)); 
    }        
});

Khi chúng ta click vào 2 nút cộng trừ thì NumberPicker sẽ giải phóng sự kiện, chúng ta bắt sự kiện này bằng lớp OnValueChangeListener. Ở đoạn code trên chúng ta gắn một đối tượng listener vào picker và sử dụng phương thức onValueChange(), phương thức này sẽ được gọi khi chúng ta click vào 2 nút cộng trừ, tham số của phương thức này là đối tượng picker đã giải phóng sự kiện, giá trị mới sau khi click nút cộng/trừ và giá trị cũ trước khi click nút cộng/trừ.

np.setMaxValue(100);
np.setMinValue(0);

Hai dòng code trên thiết lập khoảng giá trị cho NumberPicker là từ 0 đến 100.

Screenshot_2016-05-22-15-18-08

TimePicker

TimePicker thì hiển thị giá trị giờ, TimePicker có 2 chế độ hiển thị là hiển thị theo dạng 24h hoặc dùng kí hiệu AM/PM. Ngoài ra còn có lớp DatePicker là chọn ngày thá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" >
    
    <TimePicker android:id="@+id/tpId" 
        android:layout_marginTop="5dp" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" />

    <TextView android:id="@+id/tvId" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:layout_marginTop="5dp" 
        android:layout_marginLeft="5dp" 
        android:textSize="30sp" />  
        
</LinearLayout>

Trong file main.xml chúng ta thiết kế một TimePicker và một TextView để hiển thị giá trị của TimePicker đó.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TimePicker;
import android.widget.TextView;
import android.widget.TimePicker.OnTimeChangedListener;

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);
 
        TimePicker tp = (TimePicker) findViewById(R.id.tpId);
 
        int h = tp.getCurrentHour();
        int m = tp.getCurrentMinute();
 
        StringBuilder tm = new StringBuilder();
        tm.append(h + " h " + m + " m");
        tv.setText(tm);
 
        tp.setOnTimeChangedListener(new OnTimeChangedListener()
        {
            public void onTimeChanged(TimePicker view, int hour, int minute)
            {
                StringBuilder tm = new StringBuilder();
                tm.append(hour + " h " + minute + " m "); 
                tv.setText(tm);
            }
        });      
    }
}

Lớp TimePicker lắng nghe sự kiện bằng lớp OnTimeChangedListener với phương thức onTimeChanged().

tp.setOnTimeChangedListener(new OnTimeChangedListener()
{
    public void onTimeChanged(TimePicker view, int hour, 
        int minute)
    {
        StringBuilder tm = new StringBuilder();
        tm.append(hour + " h " + minute + " m "); 
        tv.setText(tm);
    }        
});

Bên trong phương thức onTimeChanged() chúng ta tạo một chuỗi từ giá trị giờ và phút được gửi tới, sau đó gán làm text cho TextView.

TimePicker tp = (TimePicker) findViewById(R.id.tpId);
 
int h = tp.getCurrentHour();
int m = tp.getCurrentMinute();

StringBuilder tm = new StringBuilder();
tm.append(h + " h " + m + " m");
tv.setText(tm);

Ngoài ra trước đó chúng ta cũng dùng chính lớp TimePicker này để lấy giờ hiện tại của hệ điều hành và gán vào TextView trước.

Screenshot_2016-05-22-15-38-19

Android – Một số View cơ bản – Phần 2

Trong phần này chúng ta tiếp tục tìm hiểu một số lớp View cơ bản là ProgressBarListView.

ProgressBar

ProgressBar hiển thị một thanh ngang hoặc một hình tròn biểu diễn tiến trình hoạt động của một hành động nào đó.

Ví dụ 1:

Chúng ta sẽ thiết kế một ProgressBar  và một TextView hiển thị tiến trình hoạt động theo %.

<?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">
    <ProgressBar 
       android:id="@+id/pbId" 
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       style="?android:attr/progressBarStyleHorizontal" 
       android:layout_margin="10dp" />       
    <TextView 
        android:id="@+id/tvId" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:layout_margin="10dp" />
</LinearLayout>

Trong file main.xml chúng ta thiết kế một thẻ ProgressBar và một TextView. Thuộc tính style quy định kiểu “vẽ” của ProgressBar lên màn hình, ở đây progressBarStyleHorizontal nghĩa là vẽ kiểu thanh ngang. Mặc định thì ProgressBar sẽ vẽ theo kiểu hình tròn.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.util.Log;

public class MainActivity extends Activity
{
    private ProgressBar pb;
    private TextView tv;
    private int prg = 0;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        pb = (ProgressBar) findViewById(R.id.pbId);
        tv = (TextView) findViewById(R.id.tvId);
 
        new Thread(myThread).start();
    }

    private Runnable myThread = new Runnable()
    { 
        @Override
        public void run() 
        {
            while (prg < 100)
            {
                try
                {
                    hnd.sendMessage(hnd.obtainMessage());
                    Thread.sleep(100);
                }
                catch(InterruptedException e) 
                {  
                    Log.e("ERROR", "Thread was Interrupted");
                }
            }

            runOnUiThread(new Runnable() { 
                public void run() {
                    tv.setText("Finished");
                }
            });          
        }
    
        Handler hnd = new Handler()
        {    
            @Override
            public void handleMessage(Message msg) 
            {
                prg++;
                pb.setProgress(prg);

                String perc = String.valueOf(prg).toString();
                tv.setText(perc+"% completed");
            }
        };
    };
}

Chúng ta sẽ điều khiển tiến trình hoạt động của ProgressBar trong một luồng khác.

new Thread(myThread).start();

Ở đây chúng ta sử dụng một luồng khác để thực hiện công việc tính toán, còn luồng chính sẽ cập nhật ProgressBar, lý do sử dụng 2 luồng khác nhau là để chúng có thể chạy song song với nhau.

@Override
public void run() 
{
    while (prg < 100)
    {
        try
        {
            hnd.sendMessage(hnd.obtainMessage());
            Thread.sleep(100);
        }
        catch(InterruptedException e) 
        {  
            Log.e("ERROR", "Thread was Interrupted");
        }
    }

    runOnUiThread(new Runnable() { 
        public void run() {
            tv.setText("Finished");
        }
    });          
}

Công việc tính toán ở đây chỉ đơn giản là tăng giá trị của một biến từ 0 đến 100, mỗi lần tăng thì nghỉ 100 mili giây.

Cơ chế của hệ điều hành Android có hơi khác các hệ điều hành khác là những biến nào được khai báo trong một Activity chỉ có thể truy xuất được trong Activity đó, biến myThread tuy thuộc lớp Activity chính nhưng thực chất nó chạy một luồng của riêng nó, không đụng chạm gì tới Activity chính cả, do đó chúng ta không thể thao tác trực tiếp với các biến prg hay biến TextView tv bên trong phương thức run() được. Để có thể thao tác với các biến của một đối tượng Activity từ một luồng khác, chúng ta phải gọi chúng bên trong phương thức runOnUiThread() hoặc bên trong một đối tượng Handler.

runOnUiThread(new Runnable() { 
    public void run() {
        tv.setText("Finished");
    }
});   

Biến thuộc luồng nào thì chỉ có thể truy xuất được từ luồng đó, ở đây chúng ta chỉnh sửa biến tv nên chúng ta truy xuất nó bên trong phương thức runOnUiThread().

Handler hnd = new Handler()
{    
    @Override
    public void handleMessage(Message msg) 
    {
        prg++;
        pb.setProgress(prg);

        String perc = String.valueOf(prg).toString();
        tv.setText(perc+"% completed");
    }
};

Như đã nói, ngoài cách dùng phương thức runOnUiThread(), chúng ta cũng có thể truy xuất các biến đó thông qua một đối tượng Handler. Ở đây chúng ta cập nhật biến prg và thiết lập giá trị của biến đó cho ProgressBar.

Screenshot_2016-05-21-16-00-23

Ví dụ 2:

Trong ví dụ này, chúng ta sẽ hiển thị ProgressBar ở dạng hình tròn.

<?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" >
    <ProgressBar 
        android:id="@+id/pbId" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content"/>    
    <TextView 
        android:id="@+id/tvId" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="Please wait..." />        
</LinearLayout>

Ở đây chúng ta không thiết lập biến style cho ProgressBar nữa và như thế ProgressBar sẽ hiển thị hình tròn thay vì thanh ngang. Ngoài ra TextView cũng không hiển thị số % tiền trình hoạt động nữa nên chúng ta thiết lập “cứng” thuộc tính text luôn.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.view.View;
import android.util.Log;

public class MainActivity extends Activity
{
    private ProgressBar pb;
    private TextView tv;
    private int prg = 0;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        pb = (ProgressBar) findViewById(R.id.pbId);
        tv = (TextView) findViewById(R.id.tvId);
 
        new Thread(myThread).start();
    }

    private Runnable myThread = new Runnable()
    { 
        @Override
        public void run() 
        {
            while (prg < 100)
            {
                try
                {
                    hnd.sendMessage(hnd.obtainMessage());
                    Thread.sleep(100);
                }
                catch(InterruptedException e) 
                {  
                    Log.e("ERROR", "Thread was Interrupted");
                }
            }

            runOnUiThread(new Runnable() { 
                public void run() {
                    tv.setText("Finished");
                    pb.setVisibility(View.GONE); 
                }
            });          
        }
    
        Handler hnd = new Handler()
        {    
            @Override
            public void handleMessage(Message msg) 
            {
                prg++;
                pb.setProgress(prg);
            }
        };
    };
}

Đoạn code trong file MainActivity.java cũng tương tự như ví dụ trước.

runOnUiThread(new Runnable() { 
    public void run() {
        tv.setText("Finished");
        pb.setVisibility(View.GONE); 
    }
}); 

Chỉ khác là ở đây chúng ta không cập nhật text cho TextView theo % mà chỉ canh khi nào tiến trình hoàn tất thì mới cập nhật thôi. Ngoài ra sau khi hoàn tất công việc, chúng ta cho ẩn ProgressBar đi bằng phương thưc setVisibility().

Screenshot_2016-05-21-15-06-18

ListView

ListView hiển thị dữ liệu dưới dạng một danh sách các item, chúng ta có thể dùng tay cuộn danh sách lên xuống nếu danh sách quá dài. ListView sử dụng dữ liệu được cung cấp bởi lớp Adapter.

Ví dụ 1:

<?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">  
     
    <ListView 
        android:id="@+id/lvId" 
        android:layout_width="fill_parent" 
        android:layout_height="fill_parent" />  
    
</LinearLayout> 

Trong file main.xml chúng ta khai báo một thẻ ListView.

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent" 
    android:padding="10dp" android:textSize="20sp">
</TextView>

File row.xml sẽ định nghĩa cách các hàng trong ListView được hiển thị như thé nào, ví dụ như các hàng có độ lớn bao nhiêu, kích cỡ chữ bao nhiêu. Ở đây sp là đơn vị dùng cho kích thước font trong Android.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">MainActivity</string>
    <string-array name="languages">
        <item>Python</item>
        <item>Java</item>
        <item>Ruby</item>
        <item>C++</item>
    </string-array>        
</resources>

Trong file strings.xml chúng ta định nghĩa các item sẽ được dùng cho ListView trong biến string-array.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;  
import android.widget.ListView;  

public class MainActivity extends Activity
{
    private ListView lv;  
    private ArrayAdapter<String> la; 

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        setupUI();
    }

    public void setupUI()
    { 
        lv = (ListView) findViewById(R.id.lvId);  
        String[] languages = getResources().getStringArray(R.array.languages); 
        lv.setAdapter(new ArrayAdapter<String>(this, R.layout.row, languages));
    }
}

Trong file MainActivity.java chúng ta kết nối tất cả những thứ trên lại với nhau.

String[] languages = getResources().getStringArray(R.array.languages);

Dòng code trên sẽ lấy dữ liệu của các item trong file strings.xml đưa vào mảng languages.

lv.setAdapter(new ArrayAdapter<String>(this, R.layout.row, languages));

Lớp ArrayAdapter có nhiệm vụ kết nối file row.xml, dữ liệu trong mảng languages vào đối tượng ListView.

Screenshot_2016-05-21-15-23-13

Ví dụ 2:

Trong ví dụ này chúng ta sử dụng lớp ListActivity thay cho lớp Activity thường. Lớp ListActivity là lớp Activity nhưng có sẵn một đối tượng ListView. Bởi vì ListView rất thường được dùng để hiển thị riêng trong một màn hình Activity, do đó Android cho ra đời lớp ListActivity để đơn giản hóa việc thiết kế cho các coder. Cũng chính vì vậy ở đây chúng ta cũng sẽ không dùng đến file main.xml để thiết kế.

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent" 
    android:padding="10dp" android:textSize="20sp">
</TextView>

File row.xml định nghĩa cách ListView hiển thị, giống như ví dụ trước.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">MainActivity</string>
</resources>

File strings.xml chỉ lưu tiêu đề của ứng dụng chứ không còn lưu danh sách các item nữa.

package com.phocode;

import android.app.ListActivity;
import android.os.Bundle;
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;

public class MainActivity extends ListActivity 
     implements OnItemClickListener, OnItemSelectedListener
{       
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
      
        ArrayAdapter<String> la = new ArrayAdapter<String>(this, R.layout.row);  
        la.add("Python");
        la.add("Java");
        la.add("Ruby");
        la.add("C++");

        setListAdapter(la);

        ListView lv = getListView();
        lv.setOnItemClickListener(this);
        lv.setOnItemSelectedListener(this);
    }

    public void onItemClick(AdapterView<?> parent, View view,
        int position, long id) 
    {        
        String language = ((TextView) view).getText().toString();
        setTitle(language);
    }

    public void onItemSelected(AdapterView<?> parent, View view,
        int position, long id) 
    {        
        String language = ((TextView) view).getText().toString();
        setTitle(language);
    }

    public void onNothingSelected(AdapterView<?> parent) 
    {        
       
    }
}

Thay vào đó chúng ta sẽ thiết kế các item cho ListView trong Java. Ngoài ra chúng ta bắt sự kiện click vào item trên ListView nữa.

public class MainActivity extends ListActivity 
     implements OnItemClickListener, OnItemSelectedListener

Lớp MainActivity giờ đây sẽ kế thừa lớp ListActivity và implement 2 giao diện là OnItemClickListenerOnItemSelectedListener, tổng cộng có 3 phương thức trừu tượng cần được code.

@Override
public void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    ...
}

Ở đây phương thức onCreate() sẽ không có dòng setContentView() vì mặc định lớp ListActivity đã có sẵn View của riêng nó là ListView rồi, ListView này có kích thước phủ toàn bộ màn hình.

ArrayAdapter<String> la = new ArrayAdapter<String>(this, R.layout.row);  
la.add("Python");
la.add("Java");
la.add("Ruby");
la.add("C++");

Chúng ta tạo đối tượng ArrayAdapter như bình thường để truyền dữ liệu vào ListView.

setListAdapter(la);

Phương thức setListAdapter() sẽ gắn dữ liệu của đối tượng Adapter với đối tượng ListView.

ListView lv = getListView();
lv.setOnItemClickListener(this);
lv.setOnItemSelectedListener(this);

Chúng ta thiết lập các listener cho ListView.

public void onItemClick(AdapterView<?> parent, View view,
    int position, long id) 
{        
    String language = ((TextView) view).getText().toString();
    setTitle(language);
}

Giao diện OnItemClickListener có phương thức trừu tượng là onItemClick(), ở đây chúng ta làm công việc là thiết lập đoạn text trên thanh tiêu đề là text của item được click.

public void onItemSelected(AdapterView<?> parent, View view,
    int position, long id) 
{        
    String language = ((TextView) view).getText().toString();
    setTitle(language);
}

public void onNothingSelected(AdapterView<?> parent) 
{        
    
}

Giao diện OnItemSelectedListener có 2 phương thức trừu tượng, tuy nhiên chúng ta chỉ code phương thức onItemSelected() thôi, ở đây chúng ta cũng chỉ đơn giản là thiết lập lại tiêu đề của ứng dụng bằng tiêu đề của item được chọn trong ListView.

Screenshot_2016-05-21-15-47-17

Android – Một số View cơ bản – Phần 1

Trong phần này chúng ta sẽ tìm hiểu một số lớp View cơ bản.

Spinner

SpinnerView cho phép chúng ta chọn một item trong một danh sách các item.

Ví dụ 1:

<?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" >
    <Spinner android:id="@+id/spn" 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" 
        android:entries="@array/dlangs" 
        android:layout_marginTop="10dip" 
        android:prompt="@string/spn_title" />

   <TextView android:id="@+id/tvId" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:layout_marginTop="10dip" />        
        
</LinearLayout>

Trong file main.xml, chúng ta khai báo thẻ Spinner và thẻ TextView. Danh sách các item của Spinner sẽ được khai báo trong file strings.xml.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Spinner</string>
    <string name="spn_title">Choose a language</string>
    
    <string-array name="dlangs">
        <item>Python</item>
        <item>Java</item>
        <item>C++</item>      
        <item>Ruby</item>
    </string-array>    
    
</resources>

Trong file strings.xml chúng ta khai báo tên biến spn_title dùng làm tiêu đề cho Spinner, biến dlangs có kiểu string-array là danh sách các item cho Spinner.

package com.phocode;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Spinner;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;


public class MainActivity extends Activity implements OnItemSelectedListener 
{
    private TextView tv;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

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

        Spinner spn = (Spinner) findViewById(R.id.spn);
        spn.setOnItemSelectedListener(this);
    }

    @Override
    public void onItemSelected(AdapterView<?> parent, View v, int pos, long id) 
    {
      String item = parent.getItemAtPosition(pos).toString();
      tv.setText(item);
    }

    @Override
    public void onNothingSelected(AdapterView<?> arg0) 
    {      
      tv.setText("");
    }
}

Item được chọn trong Spinner sẽ được dùng làm text của TextView.

public class MainActivity extends Activity implements OnItemSelectedListener 

Khi có Item nào được chọn thì Spinner sẽ giải phóng sự kiện, chúng ta implement giao diện OnItemSelectedListerner để bắt lấy sự kiện đó, có 2 phương thức phải override lại là onItemSelected()onNothingSelected().

Spinner spn = (Spinner) findViewById(R.id.spn);
spn.setOnItemSelectedListener(this);

Để các phương thức trên có thể bắt sự kiện thì chúng ta dùng phương thức setOnItemSelectedListener() của Spinner và truyền vào đối tượng có override các phương thức đó.

@Override
public void onItemSelected(AdapterView<?> parent, View v, int pos, long id) 
{
    String item = parent.getItemAtPosition(pos).toString();
    tv.setText(item);
}

Trong phương thức onItemSelected() chúng ta lấy đối tượng item đang được chọn bằng phương thức onItemAtPosition(), sau đó chuyển thành String rồi gán làm text cho TextView.

Capture

Ví dụ 2

Trong ví dụ này chúng ta sẽ thiết kế lại như ví dụ trên, chỉ khác là các item sẽ được thêm vào trong Spinner từ file java chứ không nhập cứng từ file resource nữa.

<?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" >
    
    <Spinner android:id="@+id/spnId"
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:layout_marginTop="10dip" 
        android:prompt="@string/spn_title" />    
      
    <TextView android:id="@+id/tvId"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:layout_marginTop="10dip" />
    
</LinearLayout>

Trong file main.xml chúng ta có 2 ViewSpinnerTextView. Spinner ở đây không được thiết lập thuộc tính src nữa.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Spinner2</string>
    <string name="spn_title">Choose a language</string>
</resources>

Trong file strings.xml chúng ta bỏ danh sách item đi, chỉ còn biến lưu tiêu đề của Spinner.

package com.phocode;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Spinner;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;

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

        tv = (TextView)findViewById(R.id.tvId);
 
        spn = (Spinner)findViewById(R.id.spn);
 
        List<String> lst = new ArrayList<String>();
        lst.add("Python");
        lst.add("Java");
        lst.add("C++");
        lst.add("Ruby");
        ArrayAdapter<String> da = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, lst);        
 
        spn.setAdapter(da);
 
        spn.setOnItemSelectedListener(this);    
    }       

    @Override
    public void onItemSelected(AdapterView<?> parent, View v, int pos, long id) 
    {
      String item = parent.getItemAtPosition(pos).toString();
      tv.setText(item);
    }

    @Override
    public void onNothingSelected(AdapterView<?> arg0) 
    {      
      tv.setText("");
    }
}

Trong file MainActivity.java chúng ta thiết lập dữ liệu cho Spinner và override các phương thức lắng nghe sự kiện như ví dụ trên.

List<String> lst = new ArrayList<String>();
lst.add("Python");
lst.add("Java");
lst.add("C++");
lst.add("Ruby");

Đầu tiên chúng ta sử dụng List để tạo một danh sách các item sẽ được lưu vào Spinner.

ArrayAdapter<String> da = new ArrayAdapter<String>(this,
        android.R.layout.simple_spinner_item, lst);

Tiêp theo chúng ta sử dụng một đối tượng ArrayAdapter, đối tượng này có tác dụng liên kết dữ liệu giữa SpinnerList<String>, mỗi khi List có sự thay đổi, chẳng hạn như thêm hoặc xóa bớt item thì Spinner cũng sẽ tự động thêm/bớt các item đó.

spn.setAdapter(da);

Có đối tượng ArrayAdapter rồi thì chúng ta phải thiết lập adapter đó cho Spinner.Capture

SeekBar

SeekBar hiển thị một thanh trượt và một cái nút trên thanh trượt đó cho phép chúng ta kéo qua kéo lại trong một khoảng giá trị số nào đó, mỗi khi nút trên thanh trượt thay đổi thì SeekBar sẽ giải phóng sự kiện và chúng ta bắt sự kiện đó bằng cách dùng giao diện OnSeekBarChangeListener.

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" >  
    <SeekBar android:id="@+id/sbId" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:layout_margin="10dp" 
        android:max="100" 
        android:progress="50" />
    <TextView android:id="@+id/tvId" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:layout_marginLeft="10dp" />     
</LinearLayout>

Ở đây chúng ta thiết kế một SeekBar và một TextView, TextView được dùng để hiển thị giá trị của SeekBar. Khoảng giá trị mặc định của SeekBar là từ 0 đến 100. Trong đó chúng ta có thể thiết lập giá trị max bằng thuộc tính android:max, tuy nhiên chúng ta không thể thiết lập giá trị min được.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">SeekBar</string>
    <string name="textview_value">50</string>
</resources>

Trong file strings.xml chúng ta khai báo biến textview_value làm giá trị khởi tạo ban đầu cho TextView.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;

public class MainActivity extends Activity implements 
    OnSeekBarChangeListener
{
    TextView tv;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        SeekBar sb = (SeekBar) findViewById(R.id.sbId);
        sb.setOnSeekBarChangeListener(this);

        tv = (TextView) findViewById(R.id.tvId);
        String val = this.getString(R.string.textview_value);
        tv.setText(val);
    }

   @Override
   public void onProgressChanged(SeekBar seekBar, int progress,
     boolean fromUser) 
   {
       tv.setText(String.valueOf(progress));
   }

   @Override
   public void onStartTrackingTouch(SeekBar seekBar) 
   {
       
   }

   @Override
   public void onStopTrackingTouch(SeekBar seekBar) 
   {
       
   }
}

Đoạn text trong TextView dùng chung với giá trị của SeekBar.

public class MainActivity extends Activity implements 
    OnSeekBarChangeListener

Lớp MainActivity implement giao diện OnSeekBarChangeListener. Giao diện này có 3 phương thức cần phải override là onProgressChanged(), onStartTrackingTouch() và onStopTrackingTouch(). Ở đây chúng ta chỉ cần dùng đến phương thức đầu tiên.

SeekBar sb = (SeekBar) findViewById(R.id.sbId);
sb.setOnSeekBarChangeListener(this);

Chúng ta lấy đối tượng SeekBar và gắn listener cho nó là đối tượng Activity hiện tại vì đối tượng này đã implement giao diện OnSeekBarChangeListener.

tv = (TextView) findViewById(R.id.tvId);
String val = this.getString(R.string.textview_value);
tv.setText(val);

Sau đó chúng ta lấy giá trị của biến textview_value rồi dùng làm text của TextView.

@Override
public void onProgressChanged(SeekBar seekBar, int progress,
    boolean fromUser) 
{
    tv.setText(String.valueOf(progress));
}

Khi chúng ta kéo nút trên SeekBar, phương thức onProgressChanged() sẽ được gọi, giá trị thay đổi được truyền trong tham số progress, chúng ta dùng tham số này làm text của TextView.

Capture

Android – Quản lý Layout

Trong phần này chúng ta sẽ tìm hiểu một số loại layout trong Android.

Layout sẽ quy định kích thước cũng như sự sắp xếp của các View trên màn hình. Android có rất nhiều lớp layout, LinearLayout sẽ sắp xếp các View trên một hàng hoặc một cột, FrameLayout chỉ hiển thị một View, RelativeLayout sắp xếp các View theo mối quan hệ giữa chúng, GridLayout sắp xếp các View theo dạng bảng.

Hiển thị ảnh với FrameLayout

Bên trong thư mục res của project có các thư mục drawable để chúng ta đặt các file tài nguyên vào trong đó và chúng ta có thể tham chiếu đến chúng trong file layout dễ dàng. Chẳng hạn ở đây mình đặt một file ảnh với tên zamok.jpg trong thư mục drawable-hdpi.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_gravity="top" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" >
    
    <ImageView android:layout_height="match_parent" 
        android:layout_width="match_parent" 
        android:src="@drawable/zamok" />        
</FrameLayout>

Trong file main.xml, chúng ta sử dụng FrameLayout làm ViewGroup chính, bên trong FrameLayout này chúng ta đặt một thẻ ImageView.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_gravity="top" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" >

Thuộc tính layout_gravity sẽ bố trí FrameLayout ở các vị trí khác nhau, ở đây top tức là đưa lên đầu màn hình, ngoài ra còn có các giá trị khác như bottom, left, right, center... Thuộc tính layout_widthlayout_height với giá trị wrap_content sẽ quy định kích thước layout vừa đủ để bọc lấy các thành phần bên trong nó.

<ImageView android:layout_height="match_parent" 
    android:layout_width="match_parent"
    android:src="@drawable/zamok" />

Lớp ImageView sẽ hiển thị ảnh, đường dẫn đến file ảnh được truyền vào thuộc tính android:src.

Capture

LinearLayout

Trong ví dụ dưới đây, chúng ta sẽ thiết kế một hàng các Button.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="horizontal" 
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    
  <Button 
        android:layout_height="wrap_content" 
        android:layout_width="wrap_content" 
        android:text="Button1" />

  <Button 
        android:layout_height="wrap_content" 
        android:layout_width="wrap_content" 
        android:text="Button2" />
      
  <Button 
        android:layout_height="wrap_content" 
        android:layout_width="wrap_content"
        android:text="Button3" />
      
  <Button 
        android:layout_height="wrap_content" 
        android:layout_width="wrap_content" 
        android:text="Button4" />      
      
</LinearLayout>

Lớp LinearLayout sẽ sắp xếp các View theo hàng hoặc theo cột.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="horizontal" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" >

Thuộc tính widthheight có giá trị match_parent tức là vừa khít với kích thước của thiết bị.

<Button android:layout_height="wrap_content" 
    android:layout_width="wrap_content" 
    android:text="Button1" />

Mỗi Buttonwidthheightwrap_content, tức là kích thước của chúng vừa đủ để bọc lấy đoạn text bên trong nó.

Capture

Thiết kế bằng Java

Ngoài việc thiết kế trong file layout, chúng ta cũng có thể thiết kế trong file Activity.

package com.phocode;

import android.app.Activity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.LinearLayout;

public class MainActivity extends Activity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
 
        Button btn1 = new Button(this);
        btn1.setText("Button");
 
        Button btn2 = new Button(this);
        btn2.setText("Button");
 
        Button btn3 = new Button(this);
        btn3.setText("Button");
 
        Button btn4 = new Button(this);
        btn4.setText("Button");
 
        LinearLayout ll = new LinearLayout(this);
        ll.setOrientation(LinearLayout.HORIZONTAL);
 
        ll.addView(btn1);
        ll.addView(btn2);
        ll.addView(btn3);
        ll.addView(btn4);
 
        setContentView(ll);
    }
}

Ở đây chúng ta cũng thiết kế lại giống như ví dụ trước là đặt 4 Button nằm trên cùng một hàng.

Button btn1 = new Button(this);
btn1.setText("Button");

Để tạo Button thì chúng ta dùng tới lớp android.widget.Button, phương thức setText() sẽ thiết lập nội dung Button.

LinearLayout ll = new LinearLayout(this);
ll.setOrientation(LinearLayout.HORIZONTAL);

Tương tự với lớp android.widget.LinearLayout.

ll.addView(btn1);
ll.addView(btn2);
ll.addView(btn3);
ll.addView(btn4);

Phương thức addView() sẽ thêm các View vào layout.

setContentView(ll);

Phương thức setContentView() sẽ nhận đối tượng LinearLayout thay vì nhận ID của file XML như trước.

Kết hợp các Layout

Chúng ta có thể lồng các layout vào nhau để kết hợp chúng lại.

<?xml version="1.0" encoding="utf-8"?>  
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:layout_gravity="center" >  
    
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
        android:orientation="vertical" 
        android:layout_width="match_parent" 
        android:layout_height="match_parent" >

        <Button 
            android:layout_height="wrap_content" 
            android:layout_width="wrap_content" 
            android:text="Button" />    
        
        <Button 
            android:layout_height="wrap_content" 
            android:layout_width="wrap_content" 
            android:text="Button" />    
        
        <Button 
            android:layout_height="wrap_content" 
            android:layout_width="wrap_content" 
            android:text="Button" />    
        
        <Button 
            android:layout_height="wrap_content" 
            android:layout_width="wrap_content" 
            android:text="Button" />               
    </LinearLayout>
</FrameLayout>   

Trong ví dụ này chúng ta lồng LinearLayout vào bên trong Framelayout.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:layout_gravity="center" > 

FrameLayout có kích thước vừa đủ để bọc lấy các phần tử bên trong nó. Các phần tử bên trong FrameLayout sẽ nằm chính giữa màn hình theo thuộc tính layout_gravitycenter.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" >

LinearLayout bên trong FrameLayout chứa 4 Button được sắp xếp theo chiều dọc.

Capture

RelativeLayout

RelativeLayout sắp xếp các View dựa trên vị trí của chúng với nhau hoặc với View cha.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" >
    
    <EditText 
        android:id="@+id/etId" 
        android:layout_marginTop="10dp" 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" />  
        
    <Button 
        android:id="@+id/btn_sendId" 
        android:layout_below="@+id/etId" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:text="Send" />          
          
    <Button 
       android:id="@+id/btn_clearId" 
       android:layout_below="@+id/etId" 
       android:layout_toRightOf="@+id/btn_sendId" 
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content" 
       android:text="Clear" />    
     
</RelativeLayout>  

Trong ví dụ này chúng ta hiển thị 1 EditText và 2 Button.

<EditText 
    android:id="@+id/etId" 
    android:layout_marginTop="10dp" 
    android:layout_width="match_parent" 
    android:layout_height="wrap_content" /> 

EditText sẽ có bề ngang chiếm toàn bộ màn hình, chiều cao chỉ vừa đủ bọc lấy đoạn text bên trong. Ngoài ra thuộc tính marginTop quy định EditText cách cạnh trên của màn hình 10dp.

<Button 
    android:id="@+id/btn_sendId" 
    android:layout_below="@+id/etId" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:text="Send" />   

Chúng ta có 2 Button, trong đó Button “Send” sẽ được đặt phía dưới EditText nhờ vào thuộc tính layout_below, thuộc tính này nhận id của EditText.

<Button 
    android:id="@+id/btn_clearId" 
    android:layout_below="@+id/etId" 
    android:layout_toRightOf="@+id/btn_sendId" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:text="Clear" />   

Button “Clear” được đặt phía dưới EditText và nằm phía bên phải Button “Send”, ở đây chúng ta dùng thêm thuộc tính layout_toRightOf để chỉ định View nằm phía bên phải của View nào.Capture

GridLayout

GridLayout sắp xếp các View con theo dạng bảng, bảng bao gồm nhiều hàng và cột, hàng và cột giao nhau tạo thành các ô, một View có thể chiếm một hoặc nhiều ô. Thuộc tính gravity cho biết View sẽ được đặt ở đâu trong bảng.

<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" >
    
    <Button 
        android:text="(0, 0)" 
        android:layout_row="0" 
        android:layout_column="0" />    
       
    <Button 
        android:layout_row="0" 
        android:layout_column="1" 
        android:layout_columnSpan="2" 
        android:layout_gravity="fill_horizontal" />       
      
    <Button 
        android:text="(0, 3)" 
        android:layout_row="0" 
        android:layout_column="3" />       
    
    <Button 
        android:text="(0, 4)" 
        android:layout_row="0" 
        android:layout_column="4" />       
      
    <Button 
        android:layout_row="1" 
        android:layout_column="0" 
        android:layout_rowSpan="3" 
        android:layout_columnSpan="5" 
        android:layout_gravity="fill" />   
      
    <Button 
        android:text="Center" 
        android:layout_row="4" 
        android:layout_column="0" 
        android:layout_columnSpan="5" 
        android:layout_gravity="center_horizontal" />      
      
    <Button 
        android:text="Right" 
        android:layout_row="5" 
        android:layout_column="0" 
        android:layout_columnSpan="5" 
        android:layout_gravity="right" />           
    
</GridLayout>

Trong ví dụ này chúng ta có một GridLayout chứa một nhóm các Button.

<Button 
    android:text="(0, 0)" 
    android:layout_row="0" 
    android:layout_column="0" />  

Thuộc tính layout_row biểu thị số hàng còn thuộc tính layout_column biểu thị số cột, ở đây hàng 0 cột 0 tức là Button này nằm ở góc trái phía trên bảng.

<Button 
    android:layout_row="0" 
    android:layout_column="1" 
    android:layout_columnSpan="2" 
    android:layout_gravity="fill_horizontal" /> 

Button ở đoạn code trên nằm ở ô (0,1) nhưng có chiều ngang dài thêm 2 ô do được thiết lập trong thuộc tính columnSpan nhưng chúng ta phải khai báo thêm thuộc tính layout_gravityfill_horizontal, nếu không thì Button này vẫn sẽ có kích thước như cũ.

<Button 
    android:layout_row="1" 
    android:layout_column="0" 
    android:layout_rowSpan="3" 
    android:layout_columnSpan="5" 
    android:layout_gravity="fill" /> 

Tương tự Button ở đoạn code trên nằm ở hàng 1 cột 0, thuộc tính layout_rowSpan quy định chiều cao của Button này dài thêm 3 hàng, thuộc tính columnSpan quy định Button này dãn kích thước theo chiều ngang thêm 5 cột nữa, thuộc tính layout_gravity là fill sẽ lấp đầy khoảng trống được dãn ra đó.

<Button 
    android:text="Center" 
    android:layout_row="4" 
    android:layout_column="0" 
    android:layout_columnSpan="5" 
    android:layout_gravity="center_horizontal" />  

Button ở đoạn code trên nằm ở giữa cột nhờ vào thuộc tính layout_gravitycenter_horizontal.

Capture

Android – Permission

Hệ điều hành Android bảo vệ chính nó và thông tin riêng của người dùng bằng cách chạy các ứng dụng Android trên một môi trường ảo riêng (thuật ngữ gọi là Sandbox), môi trường ảo này có tài nguyên riêng, không đụng chạm gì tới tài nguyên của hệ điều hành, nếu ứng dụng muốn sử dụng tài nguyên bên ngoài Sandbox thì ứng dụng phải xin permission – quyền sử dụng. Tùy thuộc vào loại tài nguyên mà ứng dụng muốn truy cập, hệ điều hành sẽ cấp quyền sử dụng tự động hoặc sẽ phải hỏi ý kiến của người dùng thì mới được sử dụng.

Khai báo Permission

Tùy vào loại tài nguyên cần sử dụng mà ứng dụng sẽ phải khai báo permission cho phù hợp. Các permission sẽ được khai báo trong file AndroidManifest.xml. Tùy vào loại dữ liệu có mức độ riêng tư đến mức nào mà hệ điều hành sẽ tự động cấp quyền hoặc phải xin ý kiến người dùng, chẳng hạn như quyền sử dụng đèn pin sẽ được cấp ngay, trong khi quyền truy cập danh sách số điện thoại liên lạc sẽ phải hỏi ý kiến người dùng… Cách người dùng cấp quyền cũng khác nhau theo từng phiên bản Android, chẳng hạn như đối với phiên bản Android 5.1 trở về trước thì người dùng sẽ cấp quyền trong quá trình cài đặt ứng dụng, còn ở phiên bản Android 6.0 trở lên thì người dùng sẽ cấp quyền khi ứng dụng đang chạy.

Xác định loại quyền cần dùng

Thông thường thì ứng dụng sẽ cần dùng đến các loại dữ liệu mà bản thân nó không thể tự tạo ra được, hay các hành động có thể làm ảnh hưởng đến hành vi của smartphone hoặc các ứng dụng khác. Chẳng hạn như quyền truy cập Internet, quyền sử dụng Camera, quyền tắt/bật Wifi…

Các quyền lại được chia làm nhiều cấp độ, trong đó 2 cấp độ cao nhất là bình thường (normal) và nguy hiểm (dangerous). Quyền bình thường là các quyền sử dụng tài nguyên mà ít có rủi ro đối với sự riêng tư của người dùng, loại quyền này sẽ được hệ điều hành tự động cấp. Dưới đây là danh sách các quyền bình thường có trong phiên bản API 23:

  • ACCESS_LOCATION_EXTRA_COMMANDS
  • ACCESS_NETWORK_STATE
  • ACCESS_NOTIFICATION_POLICY
  • ACCESS_WIFI_STATE
  • BLUETOOTH
  • BLUETOOTH_ADMIN
  • BROADCAST_STICKY
  • CHANGE_NETWORK_STATE
  • CHANGE_WIFI_MULTICAST_STATE
  • CHANGE_WIFI_STATE
  • DISABLE_KEYGUARD
  • EXPAND_STATUS_BAR
  • GET_PACKAGE_SIZE
  • INSTALL_SHORTCUT
  • INTERNET
  • KILL_BACKGROUND_PROCESSES
  • MODIFY_AUDIO_SETTINGS
  • NFC
  • READ_SYNC_SETTINGS
  • READ_SYNC_STATS
  • RECEIVE_BOOT_COMPLETED
  • REORDER_TASKS
  • REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
  • REQUEST_INSTALL_PACKAGES
  • SET_ALARM
  • SET_TIME_ZONE
  • SET_WALLPAPER
  • SET_WALLPAPER_HINTS
  • TRANSMIT_IR
  • UNINSTALL_SHORTCUT
  • USE_FINGERPRINT
  • VIBRATE
  • WAKE_LOCK
  • WRITE_SYNC_SETTINGS

Quyền nguy hiểm là quyền sử dụng các loại tài nguyên có liên quan đến sự riêng tư của người dùng hoặc có thể ảnh hưởng đến hệ điều hành và các ứng dụng khác. Loại quyền này cần được sự cho phép của người dùng. Dưới đây là danh sách các quyền nguy hiểm có trong phiên bản API 23:

  • READ_CALENDAR
  • WRITE_CALENDAR
  • CAMERA
  • READ_CONTACTS
  • WRITE_CONTACTS
  • GET_ACCOUNTS
  • ACCESS_FINE_LOCATION
  • ACCESS_COARSE_LOCATION
  • RECORD_AUDIO
  • READ_PHONE_STATE
  • CALL_PHONE
  • READ_CALL_LOG
  • WRITE_CALL_LOG
  • ADD_VOICEMAIL
  • USE_SIP
  • PROCESS_OUTGOING_CALLS
  • BODY_SENSORS
  • SEND_SMS
  • RECEIVE_SMS
  • READ_SMS
  • RECEIVE_WAP_PUSH
  • RECEIVE_MMS
  • READ_EXTERNAL_STORAGE
  • WRITE_EXTERNAL_STORAGE

Ứng dụng chỉ cần xin quyền để nó có thể sử dụng trực tiếp tài nguyên, nếu trong quá trình chạy mà ứng dụng có sử dụng dữ liệu lấy từ một ứng dụng khác thì chỉ có ứng dụng khác mới cần xin quyền.

Khai báo trong file AndroidManifest.xml

Để khai báo quyền thì chúng ta sử dụng thẻ <user-permissions> trong thẻ <manifest>. Ví dụ:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.phocode">
    <uses-permission android:name="android.permission.SEND_SMS"/>
    <application ...>
        ...
    </application>
</manifest>

Ở đây chúng ta xin quyền được gửi tin SMS. Đây là loại quyền nguy hiểm.

Xin quyền trong quá trình chạy

Đối với loại quyền nguy hiểm thì quá trình xin quyền sẽ khác ở từng phiên bản hệ điều hành và từng phiên bản SDK. Ví dụ:

  • Nếu thiết bị chạy Android phiên bản 5.1 trở xuống và ứng dụng sử dụng API 22 trở xuống thì người dùng sẽ phải tự tay cấp quyền trong quá trình cài đặt ứng dụng, nếu không cấp thì ứng dụng sẽ không được cài đặt.
  • Nếu thiết bị chạy Android 6.0 trở lên và ứng dụng sử dụng API 23 trở lên thì ứng dụng sẽ được cài nhưng khi chạy thì ứng dụng sẽ lần lượt xin từng quyền từ người dùng, người dùng có thể cấp quyền này, bỏ quyền kia và ứng dụng sẽ vẫn chạy nhưng giới hạn với những quyền không được cấp.

Kể từ phiên bản Android 6.0 (API 23), người dùng có thể lấy lại quyền của ứng dụng, ví dụ như chúng ta có một ứng dụng cần sử dụng camera, và hôm nay người dùng đã cho phép quyền sử dụng camera thì không có nghĩa là ngày hôm sau ứng dụng vẫn còn có quyền đó, do đó trước khi chạy chúng ta nên kiểm tra xem ứng dụng của chúng ta có quyền hay không đã.

Ví dụ

Chúng ta viết một ứng dụng cần có quyền đọc danh sách số điện thoại liên lạc.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
    package="com.phocode.sharedpreferences">

    <uses-permission android:name="android.permission.READ_CONTACTS"/>
    <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>

Trong file AndroidManifest.xml chúng ta khai báo quyền trong thẻ <uses-permission>. 

package com.phocode;

import android.Manifest;
import android.content.pm.PackageManager;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.widget.Toast;

public class MainActivity extends Activity {

    private static final int MY_PERMISSIONS_REQUEST_READ_CONTACTS = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) 
                                != PackageManager.PERMISSION_GRANTED) 
        {
            ActivityCompat.requestPermissions(this, 
                                new String[]{Manifest.permission.READ_CONTACTS}, 
                                READ_CONTACTS_CODE);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
            case READ_CONTACTS_CODE: 
            {
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(getApplicationContext(), "Contacts permission granted", Toast.LENGTH_SHORT).show();
                } 
                else 
                {                   
                    Toast.makeText(getApplicationContext(), "Contacts permission denied", Toast.LENGTH_SHORT).show();
                }
                return;
            }
        }
    }
}

Trong file MainActivity.java chúng ta thực hiện các công việc xin quyền đọc danh sách liên lạc.

private static final int READ_CONTACTS_CODE = 1;

Hằng số READ_CONTACTS_CODE là một hằng số do chúng ta tự định nghĩa, hằng số này có tác dụng giống như ID để phân biệt các lần xin quyền.

if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) 
                                != PackageManager.PERMISSION_GRANTED) 

Phương thức checkSelfPermission() của lớp android.support.v4.ContextCompat sẽ kiểm tra xem ứng dụng đã được cấp quyền đó rồi hay chưa, lớp này được định nghĩa trong thư viện support v4. Chúng ta sẽ tìm hiểu về các thư viện support này sau.

ActivityCompat.requestPermissions(this, 
                                  new String[]{Manifest.permission.READ_CONTACTS}, 
                                  READ_CONTACTS_CODE);

Nếu quyền chưa được cấp thì chúng ta xin quyền bằng cách gọi phương thức ActivityCompat.requestPermission(), phương thức này nhận vào đối tượng Activity hiện tại, danh sách các quyền trong một mảng String và ID mà chúng ta đã định nghĩa ở trên, kết quả trả về là PackageManager.PERMISSION_GRANTED nếu được chấp nhận, ngược lại là PackageManager.PERMISSION_DENIED.

@Override
public void onRequestPermissionsResult(int requestCode, 
                                       String permissions[], 
                                       int[] grantResults) 
{
...
}

Phương thức requestPermission() sẽ hiển thị một hộp thoại xin cấp quyền cho người dùng, kết quả trả về sẽ được truyền vào lời gọi phương thức onRequestPermission(), phương thức này nhận ID của quyền được xin, danh sách các quyền trong mảng permissions và danh sách kết quả của từng quyền trong mảng grantResults.

switch (requestCode) {
    case READ_CONTACTS_CODE: 
    {
    ...
    }

Câu lệnh switch() sẽ thực thi từng câu lệnh tương ứng với từng ID.

if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) 
{
    Toast.makeText(getApplicationContext(), "Contacts permission granted", Toast.LENGTH_SHORT).show();
} 
else 
{   
    Toast.makeText(getApplicationContext(), "Contacts permission denied", Toast.LENGTH_SHORT).show();
}

Ở đây chúng ta chỉ xin cấp 1 quyền là READ_CONTACTS, do đó các mảng trả về chỉ có 1 phần tử, chúng ta kiểm tra xem kết quả xin quyền READ_CONTACTS có thành công hay không bằng cách so sánh với hằng số PERMISSION_GRANTED. Nếu có thì hiện một câu thông báo thành công, ngược lại thì báo thất bại.

Nếu ứng dụng đã từng bị từ chối cấp quyền bởi người dùng thì các lần xin quyền tiếp theo hộp thoại sẽ hiện một checkbox đề “Never ask again”, nếu người dùng check vào thì ứng dụng sẽ không bao giờ có thể xin quyền được nữa.

Capture

Android – Giao tiếp với ứng dụng khác – Phần 2

Khi chúng ta chạy một Activity khác thì chúng ta không những có thể chạy được Activity đó mà còn có thể nhận những giá trị trả về của Activity đó, chẳng hạn như chúng ta mở Camera và nhận về ảnh chụp từ Camera đó, hay mở danh sách số điện thoại liên lạc và lấy về danh sách đó…v.v. Để làm được điều này thì chúng ta sẽ cần phải sử dụng đến phương thức startActivityForResult() thay vì dùng phương thức startActivity(), khi người dùng hoàn tất công việc và tắt Activity thì dữ liệu sẽ được gửi về qua phương thức onActivityResult(), chúng ta phải override phương thức này.

Ví dụ

Chúng ta sẽ viết ứng dụng lấy số điện thoại của một item trong danh sách liên lạc.

Đầu tiên chúng ta thiết kế main layout đơn giản như thế này:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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" 
    tools:context="com.phocode.MainActivity">
    
    <TextView android:id="@+id/textView" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" />
</RelativeLayout>

Layout này chỉ có một TextView làm nhiệm vụ hiển thị số điện thoại sau khi đã chọn xong.

Tiếp theo là file Activity:

package com.phocode;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class MainActivity extends Activity {

    private static final int PICK_CONTACT_REQUEST = 1;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent pickContactIntent = new Intent(Intent.ACTION_PICK, Uri.parse("content://contacts"));
        pickContactIntent.setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE);
        startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        if(requestCode == PICK_CONTACT_REQUEST)
            if(resultCode == RESULT_OK)
            {
                Uri contactUri = data.getData();
                String[] projection = {ContactsContract.CommonDataKinds.Phone.NUMBER};

                Cursor cursor = getContentResolver().query(contactUri, projection, null, null, null);
                cursor.moveToFirst();

                TextView tv = (TextView)findViewById(R.id.textView);

                int column = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
                String number = cursor.getString(column);

                tv.setText(number);
            }
    }
}

Ở đây chúng ta mở một Activity mới bằng phương thức startActivityForResult().

private static final int PICK_CONTACT_REQUEST = 1;
...
startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST);

Phương thức startActivityForResult() ngoài việc nhận một đối tượng Intent còn nhận thêm một tham số là một số nguyên, tham số này sẽ được gán vào đối tượng Intent và có tác dụng giống như một ID dành cho mỗi đối tượng Intent vậy, bởi vì nhiều khi ứng dụng của chúng ta có thể có nhiều đối tượng Intent khác nhau nên chúng ta cần một ID để phân biệt các đối tượng Intent.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
   ...
}

Khi người dùng chọn xong một số điện thoại liên lạc và màn hình Activity của danh sách liên lạc tắt đi thì ứng dụng sẽ gọi đến phương thức onActivityResult() và truyền vào các tham số là các kết quả trả về của Activity đó, ở đây có 3 tham số là requestCode tức là ID mà chúng ta đã khai báo ở trên, resultCode là kết quả của hành động mà người dùng đã thực hiện (có 2 giá trị là RESULT_OK tức là người dùng chọn thành công và RESULT_CANCELED tức là thất bại, chẳng hạn như người dùng bấm nút lùi, tắt máy…v.v), tham số data là dữ liệu mà người dùng đã chọn.

if(requestCode == PICK_CONTACT_REQUEST)
    if(resultCode == RESULT_OK)
    {
        Uri contactUri = data.getData();
        String[] projection = {ContactsContract.CommonDataKinds.Phone.NUMBER};

        Cursor cursor = getContentResolver().query(contactUri, projection, null, null, null);
        cursor.moveToFirst();

        TextView tv = (TextView)findViewById(R.id.textView);

        int column = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
        String number = cursor.getString(column);

        tv.setText(number);
    }

Để có thể xử lý dữ liệu trả về thì bạn phải biết định dạng của dữ liệu đó. Chẳng hạn như định dạng của một danh sách liên lạc sẽ khác với định dạng của một tấm ảnh trả về bởi camera. Trong đoạn code trên chúng ta kiểm tra ID và kết quả trả về rồi lấy dữ liệu gán vào TextView. Việc lấy dữ liệu được thực hiện thông qua các ContentProvider, chúng ta sẽ tìm hiểu về chủ đề này trong các bài sau.

Screenshot_2016-05-16-13-10-18

Chạy ứng dụng, ban đầu ứng dụng sẽ chạy Activity danh sách liên lạc, khi chúng click vào một số điện thoại nào thì số điện thoại đó sẽ được gửi tới Activity chính của chúng ta.

Screenshot_2016-05-16-13-10-33