Xây dựng ứng dụng quay số nhanh Android (p1)

Chào các bạn, mình là Vũ.

Hôm nay nhân dịp tí nữa đo đường vì tội vừa đi xe máy vừa gọi điện thoại, mà bản chất cũng chỉ vì sự rắc rối và lóng ngóng khi tìm contact để call. Mình sẽ xây dựng 1 loạt bài tutorial về ứng dụng quay số nhanh trên Android.

Chức năng chủ yếu của ứng dụng này là cho phép tạo ra 1 widget ngoài màn hình home của điện thoại chứa những contact mà ta hay liên lạc. Đồng thời widget này cũng chứa lịch sử cuộc gọi và bàn phím số. Tóm lại nó giúp người dùng đơn giản hóa tối đa thao tác để liên lạc với người khác. Sau đây là video demo của ứng dụng.

[toc]

Chức năng và luồng chạy của ứng dụng

Chức năng chi tiết

Các chức năng của ứng dụng mà mình sẽ làm bao gồm:

  • Thêm, bớt 1 contact vào danh sách quay số nhanh (trên app)
  • Xem danh sách quay số nhanh (trên app), cho phép gọi, nhắn tin (đến 1 hoặc nhiều người).
  • Tạo widget ngoài màn hình home, widget này chứa
    • Danh sách quay số nhanh, cho phép gọi điện thoại khi click vào contact
    • Lịch sử cuộc gọi, cho phép gọi điện thoại khi click vào 1 lịch sử
    • Bàn phím số, cho phép nhập số điện thoại để gọi

Khá đơn giản, đúng không nào.

Luồng chạy của ứng dụng

  • Ban đầu bạn cần vào app và định nghĩa ra danh sách quay số nhanh. Bằng cách chọn lựa các contact từ danh bạ điện thoại. Danh sách quay số nhanh này có thể thêm bớt, tùy chỉnh về sau.
  • Sau khi có danh sách quay số nhanh, bạn có thể kéo widget của ứng dụng ra ngoài màn hình home. Widget này có chứa danh sách bạn đã tạo trên app, lịch sử cuộc gọi, bàn phím, cho phép make a phone call 1 cách dễ dàng nhất.

Luồng chạy cũng không có gì phức tạp, nhỉ?

Khởi tạo dự án

Các thư viện sử dụng

Mình sẽ sử dụng những thư viện sau đây để code:

//BUtter Knife dùng để bind view    
compile "com.jakewharton:butterknife:$rootProject.butterKnifeVersion"
//SDP android, thư viện chứa các dimensions, hỗ trợ làm layout đa màn hình
compile "com.intuit.sdp:sdp-android:$rootProject.ext.sdpAndroidVersion"
//Material dialog, giao diện dialog material cho android đời thấp
compile 'com.afollestad.material-dialogs:core:0.9.4.4'
//Thư viện log
compile "com.orhanobut:logger:$rootProject.loggerVersion"
//ImageView bo góc, mình lười nên dùng lib luôn
compile 'com.makeramen:roundedimageview:2.3.0'
//HIệu ứng ripple khi click vào các view
compile 'com.balysv:material-ripple:1.0.2'
//Gson dùng để parse json 
compile 'com.google.code.gson:gson:2.4'
//Thư viện danh bạ điện thoại
compile 'com.github.tamir7.contacts:contacts:1.1.7'
//Thư viện expandable recyclerview, thư viện này mình import từ 1 module trong project
compile project(':libs:expandablerecyclerview')

Tạo base cho dự án

Permission

Ứng dụng cần quyền truy cập danh bạ, lịch sử cuộc gọi, gọi điện thoại và gửi tin nhắn. Vì vậy ta cần thêm các dòng sau vào file AndroidManifest.xml:

<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.CALL_PHONE"></uses-permission>
<uses-permission android:name="android.permission.READ_CALL_LOG"></uses-permission>
<uses-permission android:name="android.permission.SEND_SMS"></uses-permission>

BaseActivity

Ở ứng dụng này, trên App ta cần xây dựng 2 màn hình:

  • Danh sách quay số nhanh
  • Màn hình Chọn lựa contact từ danh bạ để thêm vào danh sách quay số nhanh.

Mình sẽ sử dụng 2 Activity để xây dựng 2 màn hình này. Để code được ngắn gọn và tường minh, mình sẽ viết 1 lớp BaseActivity.java. Lớp này chứa 1 số phương thức chung có thể sử dụng cho tất cả các Activity trong dự án. Mọi Activity sẽ kế thừa từ lớp BaseActivity này:

public abstract class BaseActivity extends AppCompatActivity {

    protected Gson gson;
    protected DBHelper dbHelper;
 
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getLayoutId());
        gson = new Gson();
        dbHelper = QuickDialApplication.getInstance().getDbHelper();
        ButterKnife.bind(this);
        createView();
    }
    //Hàm abstract trả về layout của activity
    protected abstract int getLayoutId();
 
    //Hàm set sự kiện cho các view trong activity
    protected abstract void createView();
 
    //Hàm show thông báo toast
    public void showToast(String msg) {
        if (msg != null) {
            Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "null", Toast.LENGTH_SHORT).show();
        }
    }
 
    //Hàm chuyển activity
    public void showActivity(Class t) {
        Intent intent = new Intent(this, t);
        startActivity(intent);
    }
 
    ////Hàm chuyển activity kèm theo bundle
    public void showActivity(Class t, Bundle bundle) {
        Intent intent = new Intent(this, t);
        intent.putExtra(Constant.KEY_EXTRA, bundle);
        startActivity(intent);
    }
}

Màn hình danh sách quay số nhanh QuickDialActivity

Khai báo layout

Sau khi tạo xong base dự án ta sẽ bắt  tay vào màn hình đầu tiên, là màn hình Danh sách quay số nhanh. Mình đặt tên Activity tương ứng là QuickDialActivity. Mình tạo 1 file activity_quick_dial.xml làm layout cho activity này:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            android:contentInsetEnd="0dp"
            android:contentInsetLeft="0dp"
            android:contentInsetRight="0dp"
            android:contentInsetStart="0dp"
            app:contentInsetEnd="0dp"
            app:contentInsetLeft="0dp"
            app:contentInsetRight="0dp"
            app:contentInsetStart="0dp"
            app:popupTheme="@style/AppTheme.PopupOverlay">

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <ImageView
                    android:id="@+id/iv_icon"
                    android:layout_width="@dimen/_27sdp"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginLeft="@dimen/_10sdp"
                    android:adjustViewBounds="true"
                    android:src="@drawable/ic_icon" />

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginLeft="@dimen/_10sdp"
                    android:layout_toRightOf="@id/iv_icon"
                    android:text="@string/app_name"
                    android:textColor="@android:color/white"
                    android:textSize="16sp"
                    android:textStyle="bold" />

                <ImageView
                    android:id="@+id/bt_add"
                    android:layout_width="@dimen/_26sdp"
                    android:layout_height="wrap_content"
                    android:layout_alignParentRight="true"
                    android:layout_centerVertical="true"
                    android:layout_marginRight="@dimen/_10sdp"
                    android:adjustViewBounds="true"
                    android:padding="@dimen/_5sdp"
                    android:src="@drawable/ic_add_people" />

                <ImageView
                    android:id="@+id/bt_remove"
                    android:layout_width="@dimen/_26sdp"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginRight="@dimen/_10sdp"
                    android:layout_toLeftOf="@+id/bt_add"
                    android:adjustViewBounds="true"
                    android:padding="@dimen/_5sdp"
                    android:src="@drawable/ic_edit" />

                <TextView
                    android:id="@+id/bt_confirm_remove"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentRight="true"
                    android:layout_centerVertical="true"
                    android:layout_marginRight="@dimen/_10sdp"
                    android:text="@string/action_remove"
                    android:textColor="@android:color/white"
                    android:textSize="15sp"
                    android:textStyle="bold"
                    android:visibility="gone" />

                <TextView
                    android:id="@+id/bt_send_message"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginRight="@dimen/_10sdp"
                    android:layout_toLeftOf="@+id/bt_confirm_remove"
                    android:text="@string/action_message"
                    android:textColor="@android:color/white"
                    android:textSize="15sp"
                    android:textStyle="bold"
                    android:visibility="gone" />
            </RelativeLayout>


        </android.support.v7.widget.Toolbar>

    </android.support.design.widget.AppBarLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:showIn="@layout/activity_quick_dial">

        <GridView
            android:id="@+id/gv_quick_dial"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:columnWidth="@dimen/_54sdp"
            android:horizontalSpacing="@dimen/_10sdp"
            android:numColumns="auto_fit"
            android:verticalSpacing="@dimen/_10sdp"></GridView>

        <LinearLayout
            android:id="@+id/ll_add"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:gravity="center"
            android:orientation="vertical"
            android:visibility="gone">

            <ImageView
                android:layout_width="@dimen/_40sdp"
                android:layout_height="wrap_content"
                android:adjustViewBounds="true"
                android:src="@drawable/ic_layout_add" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/_10sdp"
                android:gravity="center"
                android:text="@string/no_contact"
                android:textSize="16sp" />
        </LinearLayout>

        <android.support.design.widget.FloatingActionButton xmlns:app="http://schemas.android.com/apk/res-auto"
            android:id="@+id/bt_setting"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_alignParentRight="true"
            android:layout_margin="@dimen/_10sdp"
            android:src="@drawable/ic_setting"
            app:backgroundTint="@color/colorPrimaryDark" />
    </RelativeLayout>
</android.support.design.widget.CoordinatorLayout>

Layout khi danh sách quay số nhanh đã có contact trông sẽ thế này:

Về cơ bản, layout này chứa:

  • GridView để hiển thị danh sách quay số nhanh. (gv_quick_dial)
  • Layout thông báo chưa có liên lạc nào trong danh sách, click vào layout này sẽ chuyển sang màn hình chọn liên lạc (ll_add)
  • 1 vài button chức năng khác (ta chưa cần quan tâm)

Vậy 2 chức năng đầu tiên mà ta cần xây dựng là:

  • Lấy được danh sách quay số nhanh
  • HIển thị được danh bạ điện thoại để chọn liên lạc.

Danh sách quay số nhanh

Các liên hệ trong danh sách quay số nhanh cần được lưu trữ trong 1 database để ta có thể đem ra sử dụng. Ở đây mình dùng luôn SQLite. Vậy công việc đầu tiên của ta là xây dựng 1 database bằng SQLite cho phép thêm, xóa 1 contact vào trong database. Mình sẽ tạo 1 package db và tạo file DBHelper.java. Lớp DBHelper này sẽ quản lý việc thêm, xóa dữ liệu về contact vào database. Cụ thể:

  • Lưu dữ liệu từ đối tượng Contact vào bảng QContact (thuộc thư viện danh bạ mà mình đã nêu ra ban đầu, đối tượng này chứa các thông tin của 1 contact trong danh bạ điện thoại)
  • Lấy các dữ liệu đã lưu theo dạng List các đối tượng WidgetContactModel để hiển thị ra ở trên Widget ngoài màn hình home.

Ok, vậy giờ mình sẽ định nghĩa đối tượng WidgetContactModel trước. Mình tạo 1 package models, và file WidgetContactModel.class:

public class WidgetContactModel implements Parcelable {

    int id;
    String displayName;
    String numbers;
    String avatar;

    byte[] avatarBitmap;

    public WidgetContactModel(int id, String displayName, String numbers, String avatar) {
        this.id = id;
        this.displayName = displayName;
        this.numbers = numbers;
        this.avatar = avatar;
    }

    public WidgetContactModel(int id, String displayName, String numbers, String avatar, byte[] avatarBitmap) {
        this.id = id;
        this.displayName = displayName;
        this.numbers = numbers;
        this.avatar = avatar;
        this.avatarBitmap = avatarBitmap;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getDisplayName() {
        return displayName;
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    public String getNumbers() {
        return numbers;
    }

    public void setNumbers(String numbers) {
        this.numbers = numbers;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    public byte[] getAvatarBitmap() {
        return avatarBitmap;
    }

    public void setAvatarBitmap(byte[] avatarBitmap) {
        this.avatarBitmap = avatarBitmap;
    }

    public WidgetContactModel(Parcel in) {
        id = in.readInt();
        displayName = in.readString();
        numbers = in.readString();
        avatar = in.readString();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeInt(id);
        parcel.writeString(displayName);
        parcel.writeString(numbers);
        parcel.writeString(avatar);
    }

    public static final Creator CREATOR = new Creator() {
        @Override
        public Object createFromParcel(Parcel parcel) {
            return new WidgetContactModel(parcel);
        }

        @Override
        public WidgetContactModel[] newArray(int i) {
            return new WidgetContactModel[i];
        }
    };
}

Xong, giờ mình sẽ quay lại viết nội dung cho DBHelper thực hiện các tác vụ của 1 database:

public class DBHelper extends SQLiteOpenHelper {

    private static final String DB_NAME = "quick-dial-db";
    private static final int DB_VERSION = 3;

    private static final String CREATE_TABLE_CONTACT = "create table QContact(id integer primary key autoincrement not null, display_name text, numbers text, avatar text, avatar_bitmap blob);";


    public DBHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_TABLE_CONTACT);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int i, int i1) {
        db.execSQL("drop table if exists QContact");
        onCreate(db);
    }

    public ArrayList<WidgetContactModel> getListContact() {
        String query = "select * from QContact";
        Cursor cursor = getReadableDatabase().rawQuery(query, null);
        ArrayList<WidgetContactModel> listContact = new ArrayList<>();
        if (cursor.getCount() > 0) {
            cursor.moveToFirst();
            do {
                WidgetContactModel contact;
                contact = new WidgetContactModel(cursor.getInt(0), cursor.getString(1), cursor.getString(2), cursor.getString(3), cursor.getBlob(4));
                listContact.add(contact);
            } while (cursor.moveToNext());
        }
        cursor.close();
        return listContact;
    }

    public boolean checkContactSaved(ContactModel contact, int position) {
        Cursor cursor = getReadableDatabase().query("QContact", new String[]{"id", "display_name", "numbers", "avatar"}, "display_name = ? and numbers = ?", new String[]{contact.getTitle(), contact.getItems().get(position).getPhoneNumber()}, null, null, null);
        boolean saved = false;
        if (cursor.getCount() > 0) {
            cursor.moveToFirst();
            do {
                boolean firstCondition = cursor.getString(cursor.getColumnIndex("display_name")).equalsIgnoreCase(contact.getTitle());
                if (!firstCondition) {
                    continue;
                }
                boolean secondCondition = false;
                String numbers = cursor.getString(cursor.getColumnIndex("numbers"));
                secondCondition = numbers.equalsIgnoreCase(contact.getItems().get(position).getPhoneNumber());
                saved = firstCondition && secondCondition;
                if (saved) {
                    break;
                }
            } while (cursor.moveToNext());
            cursor.close();
            return saved;
        } else {
            cursor.close();
            return saved;
        }
    }

    public boolean checkContactSaved(Contact contact, int position) {
        Cursor cursor = getReadableDatabase().query("QContact", new String[]{"id", "display_name", "numbers", "avatar"}, "display_name = ? and numbers = ?", new String[]{contact.getDisplayName(), contact.getPhoneNumbers().get(position).getNumber()}, null, null, null);
        boolean saved = false;
        if (cursor.getCount() > 0) {
            cursor.moveToFirst();
            do {
                boolean firstCondition = cursor.getString(cursor.getColumnIndex("display_name")).equalsIgnoreCase(contact.getDisplayName());
                if (!firstCondition) {
                    continue;
                }
                boolean secondCondition = false;
                String numbers = cursor.getString(cursor.getColumnIndex("numbers"));
                secondCondition = numbers.equalsIgnoreCase(contact.getPhoneNumbers().get(position).getNumber());
                saved = firstCondition && secondCondition;
                if (saved) {
                    break;
                }
            } while (cursor.moveToNext());
            cursor.close();
            return saved;
        } else {
            cursor.close();
            return saved;
        }
    }


    public long saveContact(ContactModel contact, int numberPosition, byte[] avatarBitmap) {
        if (!checkContactSaved(contact, numberPosition)) {
            LogUtils.d("contact save true");
            ContentValues contentValues = new ContentValues();
            contentValues.put("display_name", contact.getTitle());
            StringBuffer numbersBuffer = new StringBuffer();
            numbersBuffer.append(contact.getItems().get(numberPosition).getPhoneNumber());
            contentValues.put("numbers", numbersBuffer.toString());
            contentValues.put("avatar", (contact.getUriAvatar() != null && !contact.getUriAvatar().isEmpty()) ? contact.getUriAvatar() : "");
            contentValues.put("avatar_bitmap", avatarBitmap);
            return getWritableDatabase().insert("QContact", null, contentValues);
        } else {
            LogUtils.d("contact save false");
            return -1;
        }
    }

    public long removeContact(ContactModel contact, int numberPosition) {
        return getWritableDatabase().delete("QContact", "display_name = ? and numbers = ?",
                new String[]{contact.getTitle(), contact.getItems().get(numberPosition).getPhoneNumber()});
    }

    private long removeContact(WidgetContactModel contact) {
        return getWritableDatabase().delete("QContact", "display_name = ? and numbers = ?",
                new String[]{contact.getDisplayName(), contact.getNumbers()});
    }

    public List<Boolean> removeContacts(List<WidgetContactModel> listContact) {
        List<Boolean> listResult = new ArrayList<>();
        for (WidgetContactModel contactModel : listContact) {
            listResult.add(removeContact(contactModel) > 0);
        }
        return listResult;
    }
}

Như vậy là đã xong các phương thức quản lý dữ liệu. Tiếp theo ta sẽ làm đến bước hiển thị chúng ra màn hình.

Hiển thị danh sách quay số nhanh

Ta sẽ xây dựng adapter hiển thị danh sách quay số nhanh. adapter này sẽ được sử dụng sau khi lấy được danh sách quay số nhanh từ cơ sở dữ liệu. Mình tạo 1 package adapter và tạo file ListQuickDialAdapter.java.

Adapter này sẽ có 2 dạng item:

  • Item hiển thị contact
  • Item hiển thị button Add Contact

Vậy mình sẽ phải xây dựng layout cho 2 dạng item này. Với item hiển thị contact, mình tạo 1 file item_quick_dial.xml như sau:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="@dimen/_54sdp"
    android:layout_height="wrap_content">

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/layout_item_widget"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="@dimen/_7sdp">

        <com.makeramen.roundedimageview.RoundedImageView xmlns:app="http://schemas.android.com/apk/res-auto"
            android:id="@+id/iv_avatar"
            android:layout_width="@dimen/_40sdp"
            android:layout_height="@dimen/_40sdp"
            android:layout_gravity="center_horizontal"
            android:scaleType="fitXY"
            app:riv_mutate_background="true"
            app:riv_oval="true" />
        

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="@dimen/_3sdp"
            android:gravity="center"
            android:lines="2"
            android:maxLines="2"
            android:text="BH"
            android:textSize="11sp"
            android:textStyle="bold" />
    </LinearLayout>

    <CheckBox
        android:layout_alignParentRight="true"
        android:id="@+id/cb_remove"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
         />
</RelativeLayout>

Đơn giản là chứa ImageView hiển thị avatar, TextView hiển thị tên và 1 CheckBox để đánh dấu chọn contact (chức năng này mình sẽ nói sau)

Đối với item hiển thị Button add thêm contact, mình tạo 1 file item_add_more.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <LinearLayout
        android:id="@+id/layout_item_widget"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="@dimen/_7sdp">

        <com.makeramen.roundedimageview.RoundedImageView xmlns:app="http://schemas.android.com/apk/res-auto"
            android:id="@+id/iv_avatar"
            android:layout_width="@dimen/_40sdp"
            android:layout_height="@dimen/_40sdp"
            android:layout_gravity="center_horizontal"
            android:scaleType="fitXY"
            android:src="@drawable/ic_add"
            app:riv_mutate_background="true"
            app:riv_oval="true" />
        

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="@dimen/_3sdp"
            android:gravity="center"
            android:lines="2"
            android:maxLines="2"
            android:text="BH"
            android:textSize="11sp"
            android:textStyle="bold"
            android:visibility="invisible" />
    </LinearLayout>

</RelativeLayout>

Đã xong phần layout, giờ ta sẽ viết nội dung cho ListQuickDialAdapter.java.

public class ListQuickDialAdapter extends BaseAdapter {

    Context context;
    List<WidgetContactModel> listContact;
    LayoutInflater layoutInflater;
    boolean selectionMode = false;
    List<WidgetContactModel> listToRemove;

    public ListQuickDialAdapter(Context context, List<WidgetContactModel> listContact) {
        this.context = context;
        this.listContact = listContact;
        layoutInflater = LayoutInflater.from(context);
        listToRemove = new ArrayList<>();
    }

    @Override
    public int getCount() {
        return listContact.size() + 1;
    }

    @Override
    public WidgetContactModel getItem(int i) {
        return listContact.get(i);
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public int getItemViewType(int position) {
        if (position < listContact.size()) {
            return 0;
        }
        return 1;
    }

    @Override
    public int getViewTypeCount() {
        return 2;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        QuickDialViewHolder quickDialViewHolder = null;
        AddMoreViewHolder addMoreViewHolder = null;
        if (convertView == null) {
            if (getItemViewType(position) == 0) {
                convertView = layoutInflater.inflate(R.layout.item_quick_dial, parent, false);
                quickDialViewHolder = new QuickDialViewHolder(convertView);
                convertView.setTag(quickDialViewHolder);
            } else {
                convertView = layoutInflater.inflate(R.layout.item_add_more, parent, false);
                addMoreViewHolder = new AddMoreViewHolder(convertView);
                convertView.setTag(addMoreViewHolder);
            }
        } else {
            if (getItemViewType(position) == 0) {
                quickDialViewHolder = (QuickDialViewHolder) convertView.getTag();
            } else {
                addMoreViewHolder = (AddMoreViewHolder) convertView.getTag();
            }
        }
        if (getItemViewType(position) == 0) {
            quickDialViewHolder.setData(listContact.get(position), position);
        }
        return convertView;
    }

    class QuickDialViewHolder {

        @Bind(R.id.iv_avatar)
        RoundedImageView ivAvatar;
        @Bind(R.id.tv_name)
        TextView tvName;
        @Bind(R.id.cb_remove)
        CheckBox cbRemove;

        public QuickDialViewHolder(View itemView) {
            ButterKnife.bind(this, itemView);
        }


        public void setData(WidgetContactModel widgetContactModel, int position) {
            if (widgetContactModel.getAvatar() != null && !widgetContactModel.getAvatar().isEmpty()) {
                ivAvatar.setImageURI(Uri.parse(widgetContactModel.getAvatar()));
            } else {
                ivAvatar.setImageResource(R.drawable.default_avatar);
            }
            tvName.setText(widgetContactModel.getDisplayName());
            if (selectionMode) {
                cbRemove.setVisibility(View.VISIBLE);
                if (listToRemove.contains(widgetContactModel)) {
                    cbRemove.setChecked(true);
                } else {
                    cbRemove.setChecked(false);
                }
            } else {
                cbRemove.setVisibility(View.GONE);
                cbRemove.setChecked(false);
            }
            cbRemove.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if (listToRemove.contains(widgetContactModel)) {
                        cbRemove.setChecked(false);
                        listToRemove.remove(widgetContactModel);
                    } else {
                        cbRemove.setChecked(true);
                        listToRemove.add(widgetContactModel);
                    }
                }
            });

        }
    }

    public void resetMode(boolean selectionMode) {
        this.selectionMode = selectionMode;
        if (selectionMode) {
            listToRemove.clear();
        }
    }


    class AddMoreViewHolder {

        public AddMoreViewHolder(View itemView) {
            ButterKnife.bind(this, itemView);
        }

    }

    public List<WidgetContactModel> getListContact() {
        return listContact;
    }

    public void setListContact(List<WidgetContactModel> listContact) {
        this.listContact = listContact;
    }

    public List<WidgetContactModel> getListToRemove() {
        return listToRemove;
    }

    public void setListToRemove(List<WidgetContactModel> listToRemove) {
        this.listToRemove = listToRemove;
    }

    public boolean isSelectionMode() {
        return selectionMode;
    }

    public void setSelectionMode(boolean selectionMode) {
        this.selectionMode = selectionMode;
    }
}

Lớp này sẽ lấy list dữ liệu từ

List<WidgetContactModel> listContact

để hiển thị lên App.

Ở đây, mình có định nghĩa thêm 1 list nữa:

List<WidgetContactModel> listToRemove

List này là để sau này mình làm chức năng remove 1 contact từ danh sách quay số nhanh, mình sẽ dùng nó để lưu trữ các contact cần remove.

Ok, quay lại QuickDialActivity, ta viết hàm lấy dữ liệu từ database và đổ lên gridView:

    @Override
    protected void onResume() {
        super.onResume();
        if (getQuickDialTask != null) {
            getQuickDialTask.cancel(true);
            getQuickDialTask = null;
        }
        getQuickDialTask = new AsyncTask<Void, Void, List<WidgetContactModel>>() {
            @Override
            protected List<WidgetContactModel> doInBackground(Void... voids) {
                return dbHelper.getListContact();
            }

            @Override
            protected void onPreExecute() {
                super.onPreExecute();
                showLoadingDialog(R.string.app_name, R.string.action_search);
            }

            @Override
            protected void onPostExecute(List<WidgetContactModel> widgetContactModels) {
                super.onPostExecute(widgetContactModels);
                for (WidgetContactModel widgetContactModel : widgetContactModels) {
                    LogUtils.d("[" + getClass().getSimpleName() + "]" + widgetContactModel.getDisplayName() + " with " + widgetContactModel.getNumbers());
                }
                if (adapter == null) {
                    adapter = new ListQuickDialAdapter(QuickDialActivity.this, widgetContactModels);
                    gvQuickDial.setAdapter(adapter);
                } else {
                    adapter.setListContact(widgetContactModels);
                    adapter.notifyDataSetChanged();
                }
                if (!widgetContactModels.isEmpty()) {
                    gvQuickDial.setVisibility(View.VISIBLE);
                    llAdd.setVisibility(View.GONE);
                } else {
                    gvQuickDial.setVisibility(View.GONE);
                    llAdd.setVisibility(View.VISIBLE);
                }
                hideLoadingDialog();
            }
        }.execute();
    }

Tuy nhiên, từ đầu tới giờ ta mới xây dựng module hiển thị danh sách quay số nhanh mà chưa làm module thêm contact vào danh sách quay số nhanh. Chính vì vậy cho tới hiện tại thì database của chúng ta vẫn là database rỗng. Vậy mình sẽ đi tiếp sang chức năng add 1 contact từ danh bạ vào danh sách quay số nhanh.

List contact từ danh bạ mình sẽ hiển thị trong lớp MainActivity. Để cho đầy đủ thì ở QuickDialActivity, mình sẽ set sự kiện click cho ll_add để chuyển qua lớp MainActivity. Đây sẽ là nơi chúng ta implement các phương thức thêm, xóa contact vào database.

    @OnClick(R.id.bt_add)
    public void onClickAdd() {
        showActivity(MainActivity.class);
    }

Hết bài 1

Như vậy bài này mình đã trình bày xong về:

  • Xây dựng database để lưu trữ contact
  • Xây dựng màn hình hiển thị danh sách quay số nhanh

Bài tiếp theo mình sẽ trình bày về cách lấy danh sách liên lạc từ danh bạ và đổ vào cơ sở dữ liệu. Đồng thời hiển thị lên màn hình danh sách quay số nhanh.

Phần widget ngoài màn hình home khá phức tạp, mình sẽ dành ra 2 bài cuối để trình bày. Các bạn theo dõi nhé!

Xây dựng ứng dụng chụp ảnh và filter camera trên Android (p3)

Hello, hôm nay mình sẽ trình bày nốt về các tính năng cuối cùng trong loạt bài về ứng dụng filter ảnh này. Các tính năng sẽ xây dựng sau khi hoàn thành bài viết:

  • Load ảnh đã chụp từ camera
  • Load ảnh từ bộ nhớ
  • Chỉnh sửa ảnh
  • Cắt ảnh
  • Chia sẻ ảnh

[toc]

CameraResultActivity

Yeah, ở bài trước, mình đã tạo 1 CameraActivity trống để xử lý các dữ liệu truyền sang từ màn hình chụp ảnh (MainActivity). Sẽ có 2 trường hợp:

  • Xử lý ảnh chụp từ camera
    @OnClick({R.id.bt_take_picture})
    public void onTakePictureClick() {
        showToast("Đang chụp ảnh...");
        cameraView.takeShot(new CameraRecordGLSurfaceView.TakePictureCallback() {
            @Override
            public void takePictureOK(Bitmap bmp) {
                //Xử lý lưu file, truyền dữ liệu sang màn hình chỉnh ảnh
                .....
            }
        });
    }
  • Chọn ảnh từ thư viện
    @OnClick(R.id.iv_pick_image)
    public void pickImage() {
        Bundle bundle = new Bundle();
        showActivity(CameraResultActivity.class, bundle);
    }

Với trường hợp này, ta đơn giản chỉ hiển thị CameraResultActivity và truyền cho nó 1 bundle rỗng. Ok, và nhiệm vụ của ta là sẽ phải xử lý các logic tương ứng với dữ liệu từ MainActivity truyền sang.

Xây dựng layout

Ta sẽ xây dựng layout cho màn hình chỉnh sửa ảnh. Mình tạo 1 file activity_camera_result.xml như sau:

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

    <RelativeLayout
        android:id="@+id/tb_camera"
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:background="@color/toolbar_color">

        <ImageView
            android:id="@+id/bt_close"
            android:layout_width="@dimen/_30sdp"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginLeft="@dimen/_4sdp"
            android:adjustViewBounds="true"
            android:padding="@dimen/_8sdp"
            android:src="@drawable/ic_close_camera" />

        <ImageView
            android:id="@+id/bt_done"
            android:layout_width="@dimen/_30sdp"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_marginLeft="@dimen/_4sdp"
            android:layout_marginRight="@dimen/_4sdp"
            android:adjustViewBounds="true"
            android:padding="@dimen/_6sdp"
            android:src="@drawable/ic_done_camera" />

        <TextView
            style="@style/UTM_AvoBold"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="CAMERA"
            android:textColor="#3c3837"
            android:textSize="14sp" />

    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/rl_image"
        android:layout_width="match_parent"
        android:layout_height="@dimen/_100sdp"
        android:layout_below="@+id/tb_camera">

        <com.isseiaoki.simplecropview.CropImageView
            android:id="@+id/iv_crop"
            android:layout_width="match_parent"
            android:layout_height="@dimen/_100sdp" />

        <TextView
            android:id="@+id/tv_loading_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Đang tải ảnh..."
            android:textSize="17sp"
            android:textStyle="bold" />

        <!--<ProgressBar
            android:visibility="gone"
            android:id="@+id/pb_loading_image"
            android:layout_width="@dimen/_50sdp"
            android:layout_height="@dimen/_50sdp"
            android:layout_centerInParent="true"
            android:indeterminate="true"
            android:indeterminateTint="@color/colorPrimary"
            android:indeterminateTintMode="src_atop"
            android:progressDrawable="@drawable/progress_color_media" />-->
    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/rl_action"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:background="#e2e2e2">

        <TextView
            android:id="@+id/bt_back"
            style="@style/Roboto_Regular"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_margin="@dimen/_8sdp"
            android:text="@string/back"
            android:textColor="#3c3837"
            android:textSize="14sp" />

        <TextView
            android:id="@+id/bt_continue"
            style="@style/Roboto_Regular"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_margin="@dimen/_8sdp"
            android:text="@string/next"
            android:textColor="#3c3837"
            android:textSize="14sp" />
    </RelativeLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_filter"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/rl_action"
        android:background="@color/toolbar_color">

    </android.support.v7.widget.RecyclerView>
</RelativeLayout>

Giao diện này cũng tương tự như giao diện màn hình chụp ảnh (MainActivity), khác mỗi cái là ta thay thế CameraView bằng 1 đối tượng CropImageView. Đây là 1 đối tượng ImageView đặc biệt, cho phép chúng ta lựa chọn vùng ảnh và cắt (crop). Màn hình này cũng chứa 1 list các hiệu ứng cho ta filter lên ImageView. Ok, đơn giản vậy thôi. Hãy tập trung vào việc xây dựng logic!

Xử lý ảnh chụp từ camera

Trước tiên, mình sẽ khai báo 1 biến String để lưu đường dẫn ảnh được truyền sang từ MainActivity (nếu có). 2 biến Bitmap để lưu bitmap của ảnh trước và sau khi filter. Sau đó mình viết 1 hàm get dữ liệu từ MainActivity truyền sang:

String imagePath;
Bitmap resource;
Bitmap finalResource;
    private void getData() {
        Bundle bundle = getIntent().getBundleExtra(Constant.KEY_EXTRA);
        if (bundle != null) {
            imagePath = bundle.getString(Constant.KEY_IMAGE_PATH);
            targetScreen = bundle.getString(Constant.KEY_TARGET_SCREEN, "");
        }
    }

Ta đồng thời cũng bind các view từ layout sang activity (sử dụng Butter Knife):

    @Bind(R.id.rv_filter)
    RecyclerView rvFilter;
    @Bind(R.id.iv_crop)
    CropImageView ivCrop;
    @Bind(R.id.rl_image)
    RelativeLayout rlImage;
    @Bind(R.id.tv_loading_image)
    TextView tvLoadingImage;

Tiếp theo sẽ gọi hàm getData vừa viết trong hàm onCreate, đồng thời ta sẽ chỉnh sửa lại kích thước các view 1 chút cho đẹp mắt:

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getData();
        screenWidth = DeviceUtils.getScreenWidth(this);
        rlImage.getLayoutParams().height = screenWidth;
        ivCrop.getLayoutParams().height = screenWidth;
        
    }

Cuối cùng là viết logic xử lý ảnh chụp từ Camera (thực chất là hiển thị file ảnh có đường dẫn lưu trong biến imagePath:

        if (imagePath != null && !imagePath.isEmpty()) {
            Glide.with(this).load(new File(imagePath)).asBitmap().format(DecodeFormat.PREFER_ARGB_8888).into(new SimpleTarget<Bitmap>() {
                @Override
                public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
                    CameraResultActivity.this.resource = resource;
                    ivCrop.setImageBitmap(resource);
                    ivCrop.setCustomRatio(screenWidth * 2 / 3, screenWidth * 2 / 3);
                    tvLoadingImage.setVisibility(View.GONE);
                }
            });
        }

Vậy là khi imagePath có giá trị, ta sẽ hiển thị ảnh lên ivCrop, đồng thời tạo sẵn 1 vùng crop mặc định (là toàn bộ ảnh). Logic này khá đơn giản đúng không.

Xử lý logic chọn ảnh từ thư viện

Trường hợp này được nhận biết bởi việc imagePath không có giá trị (MainActivity truyền 1 bundle rỗng sang). Nên ta sẽ xử lý như sau:

        if (imagePath != null && !imagePath.isEmpty()) {
            //Xử lý ảnh chụp từ camera
            .....
        } else {
            Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
            photoPickerIntent.setType("image/*");
            startActivityForResult(photoPickerIntent, RC_PICK_IMAGE);
        }

Sau đó, viết lại hàm onActivityResult như thế này:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == RC_PICK_IMAGE && resultCode == RESULT_OK) {
            Uri selectedImageUri = data.getData();
            Glide.with(this).load(new File(getRealPathFromURI(selectedImageUri))).asBitmap().format(DecodeFormat.PREFER_ARGB_8888).into(new SimpleTarget<Bitmap>() {
                @Override
                public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
                    CameraResultActivity.this.resource = resource;
                    ivCrop.setImageBitmap(resource);
                    ivCrop.setCustomRatio(screenWidth * 2 / 3, screenWidth * 2 / 3);
                    tvLoadingImage.setVisibility(View.GONE);
                }
            });
        } else {
            finish();
        }


    }

Trong trường hợp người dùng có chọn ảnh từ thư viện, ta load ảnh đó lên ivvCrop (xử lý tương tự như trường hợp load ảnh chụp từ Camera). Nếu người dùng không chọn ảnh, ta finish activity này liền. Logic cũng khá đơn giản phải không. Ở đây, mình có viết thêm 1 hàm getRealPathFromURI để lấy đường dẫn ảnh mà người dùng chọn:

    private String getRealPathFromURI(Uri contentURI) {
        String result;
        Cursor cursor = getContentResolver().query(contentURI, null, null, null, null);
        if (cursor == null) { // Source is Dropbox or other similar local file path
            result = contentURI.getPath();
        } else {
            cursor.moveToFirst();
            int idx = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
            result = cursor.getString(idx);
            cursor.close();
        }
        return result;
    }

Vậy là mình đã trình bày xong logic hiển thị ảnh. Giờ sẽ chuyển qua phần xử lý filter cho ảnh.

Chỉnh sửa ảnh

Chức năng này hoàn toàn tương tự như chức năng filter camera mình đã nói ở bài trước. Ta cũng sẽ tạo 1 hàm để hiển thị danh sách các filter lên như sau:

    private void setUpListFilterEffect() {
        rvFilter.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
        //create list filter
        List<FilterData> listFilter = new ArrayList<>();
        int[] imageFilterId = {R.drawable.original_1, R.drawable.natural_1, R.drawable.natural_2
                , R.drawable.pure_1, R.drawable.pure_2, R.drawable.pinky_1
                , R.drawable.pinky_2, R.drawable.pinky_3, R.drawable.pinky_4
                , R.drawable.warm_1, R.drawable.warm_2, R.drawable.cool_1
                , R.drawable.cool_2, R.drawable.mood, R.drawable.bw};
        for (int i = 0; i < EFFECT_CONFIGS.length; i++) {
            //listFilter.add(new FilterData(EFFECT_CONFIGS[i], imageFilterId[i ]));
            if (i == 0) {
                listFilter.add(new FilterData("Original", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 1) {
                listFilter.add(new FilterData("Natural 1", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 2) {
                listFilter.add(new FilterData("Natural 2", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 3) {
                listFilter.add(new FilterData("Pure 1", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 4) {
                listFilter.add(new FilterData("Pure 2", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 5) {
                listFilter.add(new FilterData("Pinky 1", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 6) {
                listFilter.add(new FilterData("Pinky 2", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 7) {
                listFilter.add(new FilterData("Pinky 3", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 8) {
                listFilter.add(new FilterData("Pinky 4", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 9) {
                listFilter.add(new FilterData("Warm 1", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 10) {
                listFilter.add(new FilterData("Warm 2", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 11) {
                listFilter.add(new FilterData("Cool 1", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 12) {
                listFilter.add(new FilterData("Cool 2", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 13) {
                listFilter.add(new FilterData("Mood", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 14) {
                listFilter.add(new FilterData("B&W", EFFECT_CONFIGS[i], imageFilterId[i]));
            }
        }
        ListFilterAdapter filterAdapter = new ListFilterAdapter(listFilter);
        rvFilter.setAdapter(filterAdapter);
    }

Ta tiếp theo ta sẽ  viết 1 asynctask thực hiện công việc apply filter lên ivCrop:

    private class FilterBitmapTask extends AsyncTask<String, Void, Bitmap> {
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            showProgressDialog("filtering");
        }

        @Override
        protected void onPostExecute(Bitmap resultBitmap) {
            super.onPostExecute(resultBitmap);
            ivCrop.setImageBitmap(resultBitmap);
            String logData = "FilterBitmapTask " + " bimap null:" + (resultBitmap == null);
            if (resultBitmap != null) {
                logData = logData + "bitmap width = " + resultBitmap.getWidth() + " - bitmap height = " + resultBitmap.getHeight();
            }
            hideProgressDialog();
        }

        @Override
        protected Bitmap doInBackground(String... params) {
            finalResource = CGENativeLibrary.cgeFilterImage_MultipleEffects(resource, params[0], 1f);
            return finalResource;
        }
    }

Cuối cùng, trong hàm setupListFilterEffect, ta thêm đoạn code set sự kiện listener cho item của recycler view:

        filterAdapter.setOnFilterSelect(new ListFilterAdapter.OnFilterSelect() {
            @Override
            public void onSelect(FilterData filterData) {
                new FilterBitmapTask().execute(filterData.getRule());
            }
        });

Vậy là đã hoàn tất việc filter cho ảnh.

Crop ảnh

Giờ ta sẽ làm đến chức năng crop ảnh. Bản thân CropImageView đã cho phép ta chọn vùng ảnh để crop, công việc của ta bây giờ chỉ là xử lý vùng ảnh được chọn đó thôi. Mình sẽ viết hàm xử lý sự kiện xác nhận crop ảnh (khi ấn vào nút continue hoặc done):

    @OnClick({R.id.bt_continue, R.id.bt_done})
    public void cropImage() {
        ivCrop.startCrop(null, new CropCallback() {
            @Override
            public void onSuccess(Bitmap cropped) {
                File file = new File(Environment.getExternalStorageDirectory() + "/FeedyPhoto/Filtered");
                if (!file.exists()) {
                    file.mkdirs();
                }
                String imagePath = ImageUtil.saveBitmap(cropped, file.getAbsolutePath() + "/" + System.currentTimeMillis() + ".jpg");
                Bundle bundle = new Bundle();
                if (imagePath != null) {
                    bundle.putString(Constant.KEY_IMAGE_PATH, imagePath);
                    showActivity(FinalImageActivity.class, bundle);
                }

            }

            @Override
            public void onError(Throwable e) {

            }
        }, new SaveCallback() {
            @Override
            public void onSuccess(Uri uri) {

            }

            @Override
            public void onError(Throwable e) {

            }
        });
    }

Đơn giản là sử dụng hàm startCrop của CropImageView để cắt ảnh, sau đó lưu vào thư mục FeedyPhoto/Filtered. Cuối cùng là truyền đường dẫn ảnh đã lưu sang FinalImageActivity. Đây sẽ là activity có chức năng hiển thị ảnh cuối cùng sau khi chỉnh sửa (filter, crop) và chia sẻ ảnh.

FinalImageActivity

Trước hết mình sẽ tạo 1 activity trống như thế này:

public class FinalImageActivity extends BaseActivity {

    @Override
    protected int getLayoutId() {
        return 0;
    }

    @Override
    protected void createView() {
        
    }

}

Sau đó khai báo nó trong AndroidManifest.xml

<activity
    android:name=".FinalImageActivity"
    android:screenOrientation="portrait">
</activity>

Xây dựng layout

Mình tạo 1 file layout đặt tên là activity_final_image.xml như sau:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <RelativeLayout
            android:id="@+id/tb_camera"
            android:layout_width="match_parent"
            android:layout_height="45dp"
            android:background="@color/toolbar_color">

            <ImageView
                android:id="@+id/bt_close"
                android:layout_width="@dimen/_30sdp"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_marginLeft="@dimen/_4sdp"
                android:adjustViewBounds="true"
                android:padding="@dimen/_8sdp"
                android:src="@drawable/ic_close_camera" />

            <ImageView
                android:id="@+id/bt_post"
                android:layout_width="@dimen/_30sdp"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:layout_marginLeft="@dimen/_4sdp"
                android:layout_marginRight="@dimen/_4sdp"
                android:adjustViewBounds="true"
                android:padding="@dimen/_6sdp"
                android:src="@drawable/ic_done_camera" />

            <TextView
                style="@style/UTM_AvoBold"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="CAMERA"
                android:textColor="#3c3837"
                android:textSize="14sp" />

        </RelativeLayout>

        <EditText
            android:id="@+id/et_caption"
            android:layout_width="match_parent"
            android:layout_height="@dimen/_100sdp"
            android:layout_margin="@dimen/_12sdp"
            android:background="@drawable/bg_et_image_caption"
            android:gravity="top|left"
            android:hint="Nhập chú thích cho bức hình..."
            android:inputType="textMultiLine"
            android:lines="100"
            android:maxLines="100"
            android:paddingBottom="@dimen/_25sdp"
            android:paddingLeft="@dimen/_5sdp"
            android:paddingRight="@dimen/_5sdp"
            android:paddingTop="@dimen/_5sdp"
            android:textColorHint="#B0B0B0" />

        <ImageView
            android:id="@+id/iv_picture"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </LinearLayout>

</RelativeLayout>

Activity này cơ bản là có 1 EditText để viết caption, 1 ImageView để hiển thị ảnh (sau khi chỉnh sửa), và 1 button để chia sẻ ảnh.

Hiển thị ảnh sau khi chỉnh sửa

Đầu tiên mình sẽ khai báo các view sử dụng trong activity trước:

    @Bind(R.id.iv_picture)
    ImageView ivPicture;
    @Bind(R.id.et_caption)
    EditText etCaption;
    String imagePath;

Đơn giản là 1 ImageView và 1 EditText thôi. Thêm vào đó là 1 biến String để chứa đường dẫn ảnh truyền sang từ màn hình CameraResultActivity.

Mình viết 1 hàm getData để lấy dữ liệu truyền sang từ CameraResultActivity:

    private void getData() {
        Bundle bundle = getIntent().getBundleExtra(Constant.KEY_EXTRA);
        if (bundle != null) {
            imagePath = bundle.getString(Constant.KEY_IMAGE_PATH);
        }
    }

Hàm này sẽ get được đường dẫn ảnh cần hiển thị. Sau khi lấy được đường dẫn rồi thì hiển thị ra thôi:

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getData();
        if (imagePath != null && !imagePath.isEmpty()) {
            Glide.with(this).load(new File(imagePath)).diskCacheStrategy(DiskCacheStrategy.ALL).into(ivPicture);
        }
    }

Vậy là xong, cùng đi sang chức năng chia sẻ nào.

Chia sẻ ảnh

Mình sẽ bắt sự kiện click cho button có id bt_post:

    @OnClick(R.id.bt_post)
    public void post() {
        //share
        final Intent intent = new Intent(android.content.Intent.ACTION_SEND);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(imagePath)));
        intent.putExtra(Intent.EXTRA_TEXT, etCaption.getText().toString());
        intent.setType("image/png");
        startActivity(Intent.createChooser(intent, "Share image via"));
    }

Tổng kết

Vậy là mình đã trình bày xong toàn bộ các chức năng của app filter ảnh và camera. Toàn bộ source code các bạn lấy tại https://github.com/nanashi1111/ImageFilterDemo. Hãy đọc code và chỉnh sửa cũng như thêm mới các chức năng để hiểu rõ hơn nữa nhé. Nếu có vấn đề gì chưa rõ hãy comment, mình sẽ giải thích.

Xây dựng ứng dụng chụp ảnh và filter camera trên Android (p2)

Hello, mình là Dương Vũ. Đây là bài viết thứ 2 trong loạt bài hướng dẫn xây dựng ứng dụng chụp ảnh và filter camera trên Android. Ở bài viết trước mình đã hướng dẫn xây dựng màn hình Splash có chức năng hiện ra dòng giới thiệu về ứng dụng, đồng thời xin cấp các permission sử dụng trong ứng dụng. Bài này, ta sẽ cùng nhau xây dựng module chính của ứng dụng: module camera và chụp ảnh. Sau khi hoàn thành bài viết này, ứng dụng của ta sẽ có thêm các chức năng:

  • Camera kèm theo hiệu ứng (filter)
  • Hiển thị danh sách filter để lựa chọn khi chụp ảnh
  • Lưu trữ ảnh trên bộ nhớ thiết bị
  • Các chức năng cơ bản của camera: quay trước, quay sau, on/off chế độ chụp flash.

Nào, bắt đầu nhé!

[toc]

Màn hình Main

Ok, mình sẽ sử dụng luôn lớp MainActivity mà IDE tạo ra cho  lúc khởi tạo dự án để viết chức năng camera. Màn hình này bao gồm 1 camera view có kích thước chiều rộng full màn hình, chiều cao bằng chiều rộng (tỉ lệ 1:1). Có 1 button để chụp ảnh, 1 recyclerView để hiển thị danh sách hiệu ứng, các button chức năng của camera (on/off flash, đổi camera). Ngoài ra màn hình còn có thêm 1 button cho phép chọn ảnh từ gallery. Ok, ta hãy bắt tay vào xây dựng layout cho nó.

Xây dựng layout

Ta chỉnh sửa lại file activity_main.xml như sau:

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

    <RelativeLayout
        android:id="@+id/tb_camera"
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:background="@color/toolbar_color">

        <ImageView
            android:id="@+id/bt_close"
            android:layout_width="@dimen/_30sdp"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginLeft="@dimen/_4sdp"
            android:adjustViewBounds="true"
            android:padding="@dimen/_8sdp"
            android:src="@drawable/ic_close_camera" />

        <ImageView
            android:layout_width="@dimen/_30sdp"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_marginLeft="@dimen/_4sdp"
            android:layout_marginRight="@dimen/_4sdp"
            android:adjustViewBounds="true"
            android:padding="@dimen/_6sdp"
            android:src="@drawable/ic_done_camera" />

        <TextView
            style="@style/UTM_AvoBold"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="CAMERA"
            android:textColor="#3c3837"
            android:textSize="14sp" />

    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/rl_camera_area"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/tb_camera">

        <org.wysaid.view.CameraRecordGLSurfaceView
            android:id="@+id/c_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <SeekBar
            android:id="@+id/seekBar"
            android:layout_width="match_parent"
            android:layout_height="80dp"
            android:layout_alignParentTop="true"
            android:layout_gravity="center_horizontal|top"
            android:visibility="gone" />

        <ImageView
            android:id="@+id/bt_rotate_camera"
            android:layout_width="@dimen/_40sdp"
            android:layout_height="@dimen/_40sdp"
            android:layout_alignParentBottom="true"
            android:padding="@dimen/_10sdp"
            android:src="@drawable/ic_rotate_camera" />

        <ImageView
            android:id="@+id/bt_flash_mode"
            android:layout_width="@dimen/_40sdp"
            android:layout_height="@dimen/_40sdp"
            android:layout_alignParentBottom="true"
            android:layout_alignParentRight="true"
            android:padding="@dimen/_10sdp"
            android:src="@drawable/ic_turn_on_flash" />

        <ImageView
            android:layout_width="@dimen/_55sdp"
            android:layout_height="@dimen/_55sdp"
            android:layout_centerInParent="true"
            android:src="@drawable/ic_focus_camera" />

    </RelativeLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_filter"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:background="@color/toolbar_color">

    </android.support.v7.widget.RecyclerView>

    <RelativeLayout
        android:id="@+id/rl_camera_action"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/rv_filter"
        android:background="@color/toolbar_color">

        <com.makeramen.roundedimageview.RoundedImageView xmlns:app="http://schemas.android.com/apk/res-auto"
            android:id="@+id/iv_pick_image"
            android:layout_width="@dimen/_40sdp"
            android:layout_height="@dimen/_40sdp"
            android:layout_centerVertical="true"
            android:layout_margin="@dimen/_10sdp"
            android:scaleType="fitXY"
            android:src="@drawable/default_avatar"
            app:riv_border_color="@android:color/white"
            app:riv_border_width="3dp"
            app:riv_corner_radius="@dimen/_5sdp"
            app:riv_mutate_background="true"
            app:riv_oval="false" />

        <ImageView
            android:id="@+id/bt_take_picture"
            android:layout_width="@dimen/_50sdp"
            android:layout_height="@dimen/_50sdp"
            android:layout_centerInParent="true"
            android:layout_margin="@dimen/_10sdp"
            android:scaleType="fitXY"
            android:src="@drawable/ic_take_picture" />

    </RelativeLayout>

</RelativeLayout>

Bạn đừng lo lắng khi nhìn sang preview thấy layout của ta như 1 mớ shit. Ta sẽ chỉnh sửa lại kích thước cho chúng ở phần code Java. Ở đây, ta chỉ cần lưu ý tới 1 view đặc biệt, đó là org.wysaid.view.CameraRecordGLSurfaceView. Đây chính là view thuôc thư viện xử lý ảnh mà mình đã trình bày ở bài đầu tiên. Nó sẽ có nhiệm vụ hiển thị camera preview cho chúng ta.

MainActivity.java

Khai báo layout đã xong, giờ ta sẽ chuyển tới phần Java code. Việc đầu tiên là ta sẽ bắt MainActivity kế thừa BaseActivity ta đã tạo ra (chứ không phải kế thừa AppCompatActivity như mặc định), sau đó sẽ là bindView cho nó:

package fbphoto.thou.com.myapplication;

import android.support.v7.widget.RecyclerView;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import com.makeramen.roundedimageview.RoundedImageView;
import org.wysaid.view.CameraRecordGLSurfaceView;
import butterknife.Bind;

/**
 * Created by Computer on 2/12/2018.
 */

public class MainActivity extends BaseActivity {

    @Bind(R.id.rl_camera_area)
    RelativeLayout rlCameraView;
    @Bind(R.id.c_view)
    CameraRecordGLSurfaceView cameraView;
    @Bind(R.id.bt_flash_mode)
    ImageView btFlashMode;
    @Bind(R.id.rv_filter)
    RecyclerView rvFilter;
    @Bind(R.id.iv_pick_image)
    RoundedImageView ivPickImage;

    @Override
    protected int getLayoutId() {
        return R.layout.activity_main;
    }

    @Override
    protected void createView() {

    }
}

Setup camera

Tiếp theo, mình sẽ viết 1 phương thức setUpCameraView, phương thức này sẽ chỉnh sửa lại kích thước của cameraView, do ở phần layout xml, kích thước của cameraView chưa đúng tỉ lệ 1:1. Ngoài ra, ta cũng sẽ bắt các sự kiện cho các button on/off flash và config 1 số thuộc tính của cameraview:

    private void setUpCameraView() {
        int screenWidth = DeviceUtils.getScreenWidth(this);
        int screenHeight = DeviceUtils.getScreenHeight(this);
        //set vùng camera thành vùng vuông
        rlCameraView.getLayoutParams().height = screenWidth;
        cameraView.getLayoutParams().height = screenWidth;
        cameraView.presetCameraForward(true);
        cameraView.presetRecordingSize(screenHeight, screenHeight);
        cameraView.setPictureSize(screenHeight, screenHeight, true); // > 4MP

        cameraView.setZOrderOnTop(false);
        cameraView.setZOrderMediaOverlay(true);
        btFlashMode.setOnClickListener(new View.OnClickListener() {
            int flashIndex = 0;
            String[] flashModes = {
                    Camera.Parameters.FLASH_MODE_TORCH,
                    Camera.Parameters.FLASH_MODE_AUTO,
            };

            @Override
            public void onClick(View v) {
                cameraView.setFlashLightMode(flashModes[flashIndex]);
                ++flashIndex;
                flashIndex %= flashModes.length;
                if (flashIndex == 0) {
                    btFlashMode.setImageResource(R.drawable.ic_turn_on_flash);
                } else {
                    btFlashMode.setImageResource(R.drawable.ic_turn_off_flash);
                }
            }
        });
        
    }

Ok, việc setup cho cameraview đến đây gần như đã xong. Tuy nhiên còn 1 bước nữa. Như đã giới thiệu ở bài trước, mình có sử dụng những file LUT chứa trong thư mục assets để tạo ra hiệu ứng cho camera. Vì vậy, ta cần khai báo (load) các file LUT này cho đối tượng cameraView của chúng ta. Trước hết, hãy tạo 1 callback theo dõi việc load các file LUT success hay failed:

    private CGENativeLibrary.LoadImageCallback mLoadImageCallback = new CGENativeLibrary.LoadImageCallback() {

        @Override
        public Bitmap loadImage(String name, Object arg) {

            Log.i(Common.LOG_TAG, "Loading file: " + name);
            AssetManager am = getAssets();
            InputStream is;
            try {
                is = am.open(name);
            } catch (IOException e) {
                Log.e(Common.LOG_TAG, "Can not open file " + name);
                return null;
            }

            return BitmapFactory.decodeStream(is);
        }

        @Override
        public void loadImageOK(Bitmap bmp, Object arg) {
            Log.i(Common.LOG_TAG, "Loading bitmap over, you can choose to recycle or cache");
            bmp.recycle();
        }
    };

Sau khi tạo callback xong, ta thêm câu lệnh sau vào phương thức setUpCameraView để load các file LUT:

CGENativeLibrary.setLoadImageCallback(mLoadImageCallback, null);

Ok, vậy là các bước config cho cameraView đã hoàn thành. Ta sẽ gọi hàm setUpCameraView vừa viết ở trong hàm createView:

    @Override
    protected void createView() {
        setUpCameraView();
    }

và không quên tắt camera khi rời khỏi activity hiện tại:

    @Override
    public void onResume() {
        super.onResume();
        cameraView.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();
        CameraInstance.getInstance().stopCamera();
        cameraView.release(null);
        cameraView.onPause();
    }

 

Xây dựng list filter

Bây giờ việc tiếp theo của ta là hiển thị list các filter để cho người dùng chọn trong lúc chụp ảnh. Ta sẽ dùng recyclerView để hiển thị các filter này. Việc đầu tiên chúng ta cần làm để xây dựng list filter là định nghĩa 1 đối tượng chứa các thông tin về filter đó.

Đối tượng FilterData.

Mình sẽ tạo 1 package model. Bên trong chứa lớp FilterData có nội dung như sau:

package fbphoto.thou.com.model;

import java.io.Serializable;

/**
 * Created by Computer on 2/8/2018.
 */

public class FilterData implements Serializable {

    String name;
    String rule;
    int imageId;

    public FilterData(String name, String rule, int imageId) {
        this.name = name;
        this.rule = rule;
        this.imageId = imageId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getRule() {
        return rule;
    }

    public void setRule(String rule) {
        this.rule = rule;
    }

    public int getImageId() {
        return imageId;
    }

    public void setImageId(int imageId) {
        this.imageId = imageId;
    }
}

Như các bạn thấy lớp này chỉ là 1 lớp Java thuần túy, không có gì đặc biệt cả. Nó chứa 3 trường:

  • name: Tên hiển thị trên app của filter.
  • rule: config của filter để thư viện xử lý ảnh thực hiện theo. Thực chất đây là tên các file LUT. Nếu các bạn tìm hiểu sâu thêm về thư viện xử lý ảnh này, các bạn sẽ thấy rằng có thể viết các config để thực hiện bất kỳ hiệu ứng nào ta mong muốn (chỉ cần có giá trị tham số). Tuy nhiên mình sẽ viết ở 1 bài khác. Ở bài này ta chỉ cần quan tâm đến tên config chứa file LUT mà thôi.
  • imageId: Id ảnh preview của filter. Mình sẽ đặt sẵn các ảnh preview ứng với từng filter trong thư mục drawable. Và trường imageId này sẽ có giá trị là id của các ảnh đó để hiện thị lên trên app.

Như vậy là ta đã xây dựng xong đối tượng chứa các thông tin về 1 filter. Giờ ta sẽ xây dựng adapter hiển thị list filter.

Xây dựng List Filter Adapter

Đầu tiên ta sẽ tạo layout cho các item của adapter, đặt tên là item_list_filter.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_filter"
    android:layout_width="@dimen/_80sdp"
    android:layout_height="wrap_content"
    android:layout_marginBottom="@dimen/_6sdp"
    android:layout_marginLeft="@dimen/_3sdp"
    android:layout_marginRight="@dimen/_3sdp"
    android:background="@drawable/bg_item_filter_unselected"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/iv_filter_image"
        android:layout_width="@dimen/_65sdp"
        android:layout_height="@dimen/_65sdp"
        android:layout_marginTop="@dimen/_2sdp"
        android:scaleType="fitXY" />

    <TextView
        android:id="@+id/tv_filter_name"
        style="@style/Roboto_Regular"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/_3sdp"
        android:textColor="#3c3837"
        android:textSize="14sp" />

</LinearLayout>

Hãy để ý tới dòng:

android:background="@drawable/bg_item_filter_unselected"

Khi 1 filter được chọn, nó sẽ có 1 background riêng, để dễ phân biệt với các filter còn lại. Ta sẽ viết 2 file xml để thể hiện 2 trạng thái (được chọn và không được chọn) của filter. Trong thư mục drawable, tạo file bg_item_filter_selected.xml tương ứng với trạng thái filter đang được chọn:

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

    <stroke
        android:width="1dp"
        android:color="@color/colorPrimary"></stroke>

</shape>

Tạo thêm 1 file bg_item_filter_unselected.xml

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

</shape>

Như các bạn thấy, ở trạng thái không được chọn, ta sẽ set cho filter đó có 1 background trống trơn.

Ok, mọi thứ liên quan đến layout đã xong, giờ ta sẽ viết adapter cho list filter. Ta tạo package adapter và định nghĩa lớp ListFilterAdapter như sau:

package fbphoto.thou.com.adapter;

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.List;

import butterknife.Bind;
import butterknife.ButterKnife;
import fbphoto.thou.com.model.FilterData;
import fbphoto.thou.com.myapplication.R;

/**
 * Created by Computer on 2/8/2018.
 */

public class ListFilterAdapter extends RecyclerView.Adapter<ListFilterAdapter.FilterViewHolder> {

    List<FilterData> listFilter;
    int currentPosition = 0;

    public ListFilterAdapter(List<FilterData> listFilter) {
        this.listFilter = listFilter;
    }

    @Override
    public FilterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_list_filter, parent, false);
        return new FilterViewHolder(view);
    }

    @Override
    public void onBindViewHolder(FilterViewHolder holder, int position) {
        holder.setData(listFilter.get(position), position);
    }

    @Override
    public int getItemCount() {
        return listFilter.size();
    }

    class FilterViewHolder extends RecyclerView.ViewHolder {
        @Bind(R.id.ll_filter)
        View llFilter;
        @Bind(R.id.iv_filter_image)
        ImageView ivFilterImage;
        @Bind(R.id.tv_filter_name)
        TextView tvFilterName;

        public FilterViewHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
        }

        public void setData(final FilterData filterData, final int position) {
            ivFilterImage.setImageResource(filterData.getImageId());
            tvFilterName.setText(filterData.getName());
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (onFilterSelect != null) {
                        int oldFocusPosition = currentPosition;
                        currentPosition = position;
                        notifyItemChanged(oldFocusPosition);
                        notifyItemChanged(position);
                        onFilterSelect.onSelect(filterData);
                    }
                }
            });
            if (position == currentPosition) {
                llFilter.setBackgroundResource(R.drawable.bg_item_filter_selected);
            } else {
                llFilter.setBackgroundResource(R.drawable.bg_item_filter_unselected);
            }
        }
    }

    public interface OnFilterSelect {
        void onSelect(FilterData filterData);
    }

    OnFilterSelect onFilterSelect;

    public OnFilterSelect getOnFilterSelect() {
        return onFilterSelect;
    }

    public void setOnFilterSelect(OnFilterSelect onFilterSelect) {
        this.onFilterSelect = onFilterSelect;
    }

    public int getCurrentPosition() {
        return currentPosition;
    }

    public void setCurrentPosition(int currentPosition) {
        this.currentPosition = currentPosition;
    }
}

Adapter này cực kỳ đơn giản, nó chỉ chứa 1 interface xử lý sự kiện chọn filter, các bạn hãy tìm hiểu nhé. Ok, như vậy ta đã xây dựng xong adapter, giờ sẽ hiển thị nó lên app.

Hiển thị list filter

Quay trở lại MainActivity, mình sẽ tạo ra 1 mảng các String, mảng này chứa tên các file LUT mà mình sẽ truyền vào các đối tượng FilterData:

public static final String EFFECT_CONFIGS[] = {
            "@adjust lut original.png",
            "@adjust lut natural01.png",
            "@adjust lut natural02.png",
            "@adjust lut pure01.png",
            "@adjust lut pure02.png",
            "@adjust lut lovely01.png",
            "@adjust lut lovely02.png",
            "@adjust lut lovely03.png",
            "@adjust lut lovely04.png",
            "@adjust lut warm01.png",
            "@adjust lut warm02.png",
            "@adjust lut cool01.png",
            "@adjust lut cool02.png",
            "@adjust lut vintage.png",
            "@adjust lut gray.png",
    };

Và bây giờ sẽ tạo 1 phương thức setUpListFilterEffect để hiển thị list các filter lên recyclerview:

    FilterData seletedFilterData = new FilterData("None", EFFECT_CONFIGS[0], 0);
    private void setUpListFilterEffect() {
        //list danh sách hiệu ứng nằm ngang
        rvFilter.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
        //create list filter
        List<FilterData> listFilter = new ArrayList<>();
        int[] imageFilterId = {R.drawable.original_1, R.drawable.natural_1, R.drawable.natural_2
                , R.drawable.pure_1, R.drawable.pure_2, R.drawable.pinky_1
                , R.drawable.pinky_2, R.drawable.pinky_3, R.drawable.pinky_4
                , R.drawable.warm_1, R.drawable.warm_2, R.drawable.cool_1
                , R.drawable.cool_2, R.drawable.mood, R.drawable.bw};
        for (int i = 0; i < EFFECT_CONFIGS.length; i++) {
            //listFilter.add(new FilterData(EFFECT_CONFIGS[i], imageFilterId[i]));
            if (i == 0) {
                listFilter.add(new FilterData("Original", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 1) {
                listFilter.add(new FilterData("Natural 1", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 2) {
                listFilter.add(new FilterData("Natural 2", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 3) {
                listFilter.add(new FilterData("Pure 1", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 4) {
                listFilter.add(new FilterData("Pure 2", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 5) {
                listFilter.add(new FilterData("Pinky 1", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 6) {
                listFilter.add(new FilterData("Pinky 2", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 7) {
                listFilter.add(new FilterData("Pinky 3", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 8) {
                listFilter.add(new FilterData("Pinky 4", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 9) {
                listFilter.add(new FilterData("Warm 1", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 10) {
                listFilter.add(new FilterData("Warm 2", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 11) {
                listFilter.add(new FilterData("Cool 1", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 12) {
                listFilter.add(new FilterData("Cool 2", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 13) {
                listFilter.add(new FilterData("Mood", EFFECT_CONFIGS[i], imageFilterId[i]));
            } else if (i == 14) {
                listFilter.add(new FilterData("B&W", EFFECT_CONFIGS[i], imageFilterId[i]));
            }
        }
        ListFilterAdapter filterAdapter = new ListFilterAdapter(listFilter);
        filterAdapter.setOnFilterSelect(new ListFilterAdapter.OnFilterSelect() {
            @Override
            public void onSelect(FilterData filterData) {
                seletedFilterData = filterData;
                cameraView.setFilterWithConfig(filterData.getRule());
            }
        });
        rvFilter.setAdapter(filterAdapter);
    }

Như các bạn thấy, phương thức trên khởi tạo ra 1 list các đối tượng FilterData và truyền list đó vào adapter để hiển thị lên recyclerView. Đồng thời ta cũng implement các câu lệnh khi có 1 filter được chọn:

        filterAdapter.setOnFilterSelect(new ListFilterAdapter.OnFilterSelect() {
            @Override
            public void onSelect(FilterData filterData) {
                seletedFilterData = filterData;
                cameraView.setFilterWithConfig(filterData.getRule());
            }
        });

Ok, hy vọng code mình viết đủ rõ ràng để các bạn có thể hiểu. Giờ ta sẽ gọi phương thức setUpListFilterEffect này trong hàm createView:

    @Override
    protected void createView() {
        setUpCameraView();
        setUpListFilterEffect();
    }

Chức năng chụp ảnh

Ta sử dụng ButterKnife để bắt sự kiện click vào button chụp ảnh:

    @OnClick({R.id.bt_take_picture})
    public void onTakePictureClick() {
        showToast("Đang chụp ảnh...");
        cameraView.takeShot(new CameraRecordGLSurfaceView.TakePictureCallback() {
            @Override
            public void takePictureOK(Bitmap bmp) {
                File file = new File(Environment.getExternalStorageDirectory() + "/FilterImageDemo");
                if (!file.exists()) {
                    file.mkdirs();
                }
                if (bmp != null) {
                    String imagePath = ImageUtil.saveBitmap(bmp, file.getAbsolutePath() + "/" + System.currentTimeMillis() + ".jpg");
                    bmp.recycle();
                    //showToast("Đã xong!");
                    Bundle bundle = new Bundle();
                    bundle.putString(Constant.KEY_IMAGE_PATH, imagePath);
                    bundle.putSerializable(Constant.KEY_FILTER, seletedFilterData);
                    showActivity(CameraResultActivity.class, bundle);
                } else {
                    showToast("Ôi, có lỗi rồi!");
                }
            }
        });
    }

Như bạn thấy, khi click vào nút chụp ảnh, ta sẽ lưu lại 1 file jpg trong thư mục FilterImageDemo và nhảy sang màn hình chỉnh sửa ảnh (CameraResultActivity). Ta cũng truyền dữ liệu về filter đang được chọn và đường dẫn ảnh đã lưu sang màn hình chỉnh sửa. Tuy nhiên giờ ta chưa cần quan tâm đến màn hình chỉnh sửa ảnh này. Đơn giản hãy tạo 1 activity có tên CameraResultActivity và khai báo nó trong file AndroidManifest:

        <activity
            android:name=".CameraResultActivity"
            android:screenOrientation="portrait"></activity>

Chức năng tùy chọn camera trước (sau)

Ta sử dụng ButterKnife để bắt sự kiện click cho button đảo camera:

    @OnClick(R.id.bt_rotate_camera)
    public void rotateCamera() {
        cameraView.switchCamera();
    }

Chức năng đóng màn hình camera

Ta sử dụng ButterKnife để bắt sự kiện click cho button thoát khỏi màn hình camera:

    @OnClick(R.id.bt_close)
    public void closeCamera() {
        finish();
    }

Chức năng chọn ảnh trong gallery

Ta viết thêm 1 chức năng cho phép người dùng chọn ảnh trong gallery để chỉnh sửa thay vì chụp ảnh trực tiếp.

    @OnClick(R.id.iv_pick_image)
    public void pickImage() {
        Bundle bundle = new Bundle();
        showActivity(CameraResultActivity.class, bundle);
    }

Như các bạn thấy khi ấn vào button pick image, ta đơn giản hiển thị màn hình CameraResultActivity. Khác với khi chụp ảnh, ta không truyền thêm dữ liệu nào khi chuyển qua màn hình CameraResultActivity này. Như vậy ở trong lớp CameraResultActivity, ta sẽ cần xử lý 2 trường hợp:

  • Không có data truyền sang (chọn ảnh trong gallery)
  • Có data truyền sang, hiển thị ảnh với các dữ liệu truyền từ màn hình chụp ảnh.

Tổng kết

Như vậy ở bài này mình đã hướng dẫn xong tính năng chụp ảnh. Ở bài tiếp theo (và cũng là bài cuối cùng) mình sẽ hướng dẫn nốt về chức năng chỉnh sửa cũng như chia sẻ ảnh được chọn. Hãy theo dõi nhé.

Source code.

 

 

Xây dựng ứng dụng chụp ảnh và filter camera trên Android (p1)

Ngày nay, tính năng chụp và chỉnh sửa ảnh đã trở nên cực kỳ phổ biến trong các ứng dụng smart phone. Hôm qua nhân dịp vọc vạch nấu nướng mình có download và sử dụng app Feedy để thực hành theo. Chợt nhận ra Feedy cũng có 1 module cho phép user chụp ảnh (có filter camera) và chỉnh sửa ảnh. Mình thấy rất thú vị nên mình cũng clone luôn để làm 1 loạt bài tutorial về filter trên Android. Sau đây là demo ứng dụng của mình:

Các chức năng chính trong app mình xây dựng sẽ bao gồm:

  • Chụp ảnh (có filter camera, sử dụng camera trước hoặc camera sau)
  • Chỉnh sửa ảnh (lựa chọn filter)
  • Cắt ảnh, viết caption cho ảnh và chia sẻ
  • Đơn giản thôi đúng không nào. Giờ mình sẽ bắt tay vào làm nhé.

[toc]

Các thư viện sử dụng trong dự án

Lưu ý: Link source code ở cuối bài viết 😉

Mình sẽ dùng các thư viện sau:

  • Android – GPU Image Plus: Đây là 1 thư viện filter khá mạnh với nhiều tính năng. Nó cung cấp cho ta các tính năng cơ bản như điều khiển độ sáng, độ phân giản, độ tương phản, … Ngoài ra thư viện này cung cấp cho ta tính năng sử dụng những file Lookup table (LUT) để áp dụng filter lên bitmap. Ngoài ra còn rất nhiều tính năng về xử lý ảnh khác nữa. Phần core của thư viện này viết bằng C/C++.
  • Glide: Thư viện load ảnh.
  • ButterKnife: Thư viện inject view, đỡ phải sử dụng findViewById rất dài dòng và nhàm chán.
  • SimpleCropView: Thư viện cho phép ta cắt 1 vùng của bitmap.
  • RoundedImageView: Thư viện bo góc cho imageview, do mình lười custom nên mình cũng dùng thư viện luôn cho nhanh.

Đó là những thư viện cơ bản mình dùng trong project.

Sơ lược về luồng đi của ứng dụng

Ứng dụng của chúng ta gồm 4 màn hình:

  • Splash screen hiển thị logo của ứng dụng đồng thời xin quyền đọc ghi trên bộ nhớ máy (để lưu ảnh), quyền sử dụng camera .
  • Camera screen: Chụp ảnh, lựa chọn hiệu ứng
  • Result screen: Sau khi chụp ảnh, hiển thị ảnh đầu ra, cắt ảnh, chỉnh sửa ảnh
  • Share screen: Hiển thị ảnh cuối cùng sau khi chỉnh sửa, viết caption và share ảnh.

Bắt đầu

Việc đầu tiên sẽ là… tạo project rồi. Tất nhiên, các bạn hãy tạo 1 project để code nhé 😀

Import thư viện

Mình sẽ import các thư viện kể trên vào dự án, thêm các lệnh này vào file build.gradle (level app):

 //Thư viện ảnh bo góc
 compile 'com.makeramen:roundedimageview:2.3.0'
 //Thư viện filter
 compile 'org.wysaid:gpuimage-plus:2.4.9'
 //Thư viện cắt ảnh
 compile 'com.isseiaoki:simplecropview:1.1.6'
 //Thư viện dimension
 compile 'com.intuit.sdp:sdp-android:1.0.4'
 //Thư viện font chữ
 compile 'uk.co.chrisjenx:calligraphy:2.3.0'
 //Thư viện load ảnh
 compile 'com.github.bumptech.glide:glide:3.7.0'

Tạo base cho dự án

Permission

Ứng dụng cần quyền sử dụng camera và quyền đọc ghi file trên thiết bị Vì vậy ta cần thêm các dòng sau vào file AndroidManifest.xml:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />

Font chữ

Mình sử dụng font Roboto cho dự án này. Trong thư mục main, mình sẽ tạo 1 thư mục assets chứa thư mục con Fonts (là nơi mình đặt các file Font chữ)

Gói image filter

Trong dự án này mình sẽ sử dụng các file Lookup table (LUT) – là các file chứa các thông số filter để chỉnh sửa màu sắc cho ảnh. Các file này chứa trong thư mục assets mình đã tạo bên trên:

Đây là các file chứa các thông số cho các filter mình sử dụng trong app.

BaseActivity

Như ở phần trên mình đã phân tích, luồng đi của ứng dụng qua 4 màn hình. Mình sẽ sử dụng các Activity tương ứng với mỗi màn, vậy ta có tổng cộng 4 Activity. Để code được ngắn gọn và tường minh, mình sẽ viết 1 lớp BaseActivity.java. Lớp này chứa 1 số phương thức chung có thể sử dụng cho tất cả các Activity trong dự án. Mọi Activity sẽ kế thừa từ lớp BaseActivity này:

public abstract class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getLayoutId());
        ButterKnife.bind(this);
        createView();
    }
    //Hàm abstract trả về layout của activity
    protected abstract int getLayoutId();

    //Hàm set sự kiện cho các view trong activity
    protected abstract void createView();

    //Hàm show thông báo toast
    public void showToast(String msg) {
        if (msg != null) {
            Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "null", Toast.LENGTH_SHORT).show();
        }
    }

    //Hàm chuyển activity
    public void showActivity(Class t) {
        Intent intent = new Intent(this, t);
        startActivity(intent);
    }

    ////Hàm chuyển activity kèm theo bundle
    public void showActivity(Class t, Bundle bundle) {
        Intent intent = new Intent(this, t);
        intent.putExtra(Constant.KEY_EXTRA, bundle);
        startActivity(intent);
    }
}

Utils

Trong ứng dụng, có những chức năng ta cần đến kích thước màn hình, hoặc cần mở App Setting để user cấp quyền cho app. Những chức năng này mình viết trong 1 lớp DeviceUtils.java. Mình sẽ tạo 1 package đặt tên là utils để chưa file DeviceUtils.java.

package fbphoto.thou.com.utils;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.util.DisplayMetrics;

/**
 * Created by Computer on 2/8/2018.
 */

public class DeviceUtils {
    
    //Lấy chiều rộng màn hình
    public static int getScreenWidth(Activity activity) {
        DisplayMetrics displayMetrics = new DisplayMetrics();
        activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        return displayMetrics.widthPixels;
    }

    //Lấy chiều cao màn hình
    public static int getScreenHeight(Activity activity) {
        DisplayMetrics displayMetrics = new DisplayMetrics();
        activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        return displayMetrics.heightPixels;
    }

    //Mở app setting
    public static void openSettingsApp(Activity context) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD) {
            Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
            intent.setData(Uri.parse("package:" + context.getPackageName()));
            context.startActivity(intent);
        }
    }
}

Màn hình splash

Khai báo layout

Sau khi tạo xong base dự án ta sẽ bắt  tay vào màn hình đầu tiên, là màn hình splash. Đây là màn hình có chức năng hiển thị ra 1 dòng text giới thiệu dưới dạng animation, đồng thời xin cấp các quyền mà ứng dụng sẽ sử dụng. Đây là file layout activity_spalsh.xml:

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

    <TextView
        android:id="@+id/tv_splash"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:layout_margin="@dimen/_10sdp"
        android:gravity="center"
        android:text="@string/intro"
        android:textColor="@android:color/black"
        android:textSize="40sp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_margin="@dimen/_10sdp"
        android:text="@string/website"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="16sp" />

</RelativeLayout>

Đơn giản chỉ là 2 TextView giới thiệu về app thôi.

Khai báo SplashActivity trong AndroidManifest

Giờ ta sẽ tạo 1 file đặt tên là SplashActivity để viết nội dung cho màn hình splash. Class này sẽ kế thừa BaseActivity. Đồng thời trong AndroidManifest, ta khai báo SplashActivity là màn hình đầu tiên của ứng dụng (thay vì MainActivity mặc định):

        <activity
            android:name=".SplashActivity"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

Lớp SplashActivity:

public class SplashActivity extends BaseActivity {
    
    @Bind(R.id.tv_splash)
    TextView tvSplash;

    @Override
    protected int getLayoutId() {
        return R.layout.activity_splash;
    }

    @Override
    protected void createView() {

    }


}

Đơn giản là khai báo layoutId và khai báo 1 TextView thôi đúng không. Giờ ta muốn cho TextView này 1 animation. Khi kết thúc animation, ta sẽ kiểm tra các quyền đọc ghi file, camera được cấp chưa. Nếu chưa được cấp thì sẽ bật ra dialog xin cấp quyền. Ta sẽ viết hàm animate để làm điều này.

Animation

private void animate() {
    AlphaAnimation animation = new AlphaAnimation(0f, 1f);
    animation.setDuration(2000);
    animation.setAnimationListener(new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {

        }

        @Override
        public void onAnimationEnd(Animation animation) {
            askForPermission();
        }

        @Override
        public void onAnimationRepeat(Animation animation) {

        }
    });
    tvSplash.startAnimation(animation);
}

Như các bạn thấy mình sử dụng AnimationListener để lắng nghe sự kiện animation kết thúc. Giờ ta sẽ viết hàm askForPermission để thực hiện bật dialog xin quyền từ user trong trường hợp các quyền ta sử dụng chưa được cấp phép. Nếu được cấp rồi thì nhảy thẳng đến màn hình Camera.

Phương thức xin cấp permission

Quay lại với BaseActivity 1 chút. Trong app này, mình chỉ có 1 chỗ cần phải xin quyền từ user, đó là ở SplashScreen. Tuy nhiên trong thực tế, 1 app có thể có nhiều lớp cần implement tính năng xin cấp quyền. Vì vậy mình sẽ viết hàm askCompactPermissions thực hiện tính năng này trong lớp BaseActivity. Các lớp kế thừa lớp BaseActivity sẽ đều có thể sử dụng hàm askCompactPermissions này để xin cấp quyền.

Đầu tiên ta viết 1 lớp PermissionUtils trong package utils:

package fbphoto.thou.com.utils;

import android.Manifest;

/**
 * Created by Computer on 2/8/2018.
 */

public class PermissionUtils {
    public static final String Manifest_READ_EXTERNAL_STORAGE = Manifest.permission.READ_EXTERNAL_STORAGE;
    public static final String Manifest_WRITE_EXTERNAL_STORAGE = Manifest.permission.WRITE_EXTERNAL_STORAGE;
    public static final String Manifest_CAMERA = Manifest.permission.CAMERA;


    public interface PermissionResult{
        void permissionGranted();

        void permissionDenied();

        void permissionForeverDienid();
    }
}

Lớp này chứa các quyền ta cần xin (đọc ghi file, camera) và 1 interface PermissionResult xử lý các hành động của người dùng.

Trong BaseActivity, mình khởi tạo các biến sử dụng cho việc xin cấp quyền:

private final int KEY_PERMISSION = 200;
private PermissionUtils.PermissionResult permissionResult;
private String permissionsAsk[];

Và viết hàm askCompactPermissions như sau:

    public void askCompactPermissions(String permissions[], PermissionUtils.PermissionResult permissionResult) {
        permissionsAsk = permissions;
        this.permissionResult = permissionResult;
        internalRequestPermission(permissionsAsk);
    }

    private void internalRequestPermission(String[] permissionAsk) {
        String arrayPermissionNotGranted[];
        ArrayList<String> permissionsNotGranted = new ArrayList<>();
        for (int i = 0; i < permissionAsk.length; i++) {
            if (!isPermissionGranted(permissionAsk[i])) {
                permissionsNotGranted.add(permissionAsk[i]);
            }
        }
        if (permissionsNotGranted.isEmpty()) {
            if (permissionResult != null)
                permissionResult.permissionGranted();
        } else {
            arrayPermissionNotGranted = new String[permissionsNotGranted.size()];
            arrayPermissionNotGranted = permissionsNotGranted.toArray(arrayPermissionNotGranted);
            ActivityCompat.requestPermissions(this, arrayPermissionNotGranted, KEY_PERMISSION);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if (requestCode != KEY_PERMISSION) {
            return;
        }
        List<String> permissionDienid = new LinkedList<>();
        boolean granted = true;
        for (int i = 0; i < grantResults.length; i++) {
            if (!(grantResults[i] == PackageManager.PERMISSION_GRANTED)) {
                granted = false;
                permissionDienid.add(permissions[i]);
            }
        }
        if (permissionResult != null) {
            if (granted) {
                permissionResult.permissionGranted();
            } else {
                for (String s : permissionDienid) {
                    if (!ActivityCompat.shouldShowRequestPermissionRationale(this, s)) {
                        permissionResult.permissionForeverDienid();
                        return;
                    }
                }
                permissionResult.permissionDenied();
            }
        }

    }

Các hàm trên đảm nhiệm chức năng kiểm tra và hiển thị hộp thoại cấp quyền trong trường hợp người dùng chưa cấp quyền cho ứng dụng. Do đây ko phải trọng tâm ứng dụng nên mình sẽ ko nói quá kỹ phần này. Các bạn có thể tìm hiểu thêm hoặc sử dụng các thư viện hỗ trợ.

Sử dụng phương thức cấp permission

OK, đã viết hàm xin permission xong ở BaseActivity, giờ quay trở lại SplashScreen. Ta sẽ gọi hàm xin permission trong askForPermission:

    private void askForPermission() {
        askCompactPermissions(new String[]{PermissionUtils.Manifest_CAMERA, PermissionUtils.Manifest_READ_EXTERNAL_STORAGE, PermissionUtils.Manifest_WRITE_EXTERNAL_STORAGE}
                , new PermissionUtils.PermissionResult() {
                    @Override
                    public void permissionGranted() {
                        showActivity(MainActivity.class);
                        finish();
                    }

                    @Override
                    public void permissionDenied() {
                        showToast("Bạn cần cung cấp quyền để sử dụng ứng dụng");
                        finish();
                    }

                    @Override
                    public void permissionForeverDienid() {
                        new AlertDialog.Builder(SplashActivity.this).setTitle("Cung cấp quyền")
                                .setMessage("Bạn có muốn cấp quyền sử dụng camera, đọc file, ghi file để chạy demo này không?")
                                .setPositiveButton("Có", new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialogInterface, int i) {
                                        DeviceUtils.openSettingsApp(SplashActivity.this);
                                    }
                                })
                                .setNegativeButton("Không", new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialogInterface, int i) {
                                        finish();
                                    }
                                }).show();
                    }
                });
    }

Nội dung hàm này là: trong trường hợp permission được cấp, ta nhảy đến MainActivity (sẽ định nghĩa sau). Nếu không được cấp, thoát app. Nếu người dùng chọn option vĩnh viễn không cấp, ta sẽ hiện dialog yêu cầu người dùng bật App Setting và cấp lại quyền, nếu không sẽ thoát app.

Ngon rồi, giờ ta sẽ gọi hàm animate trong onResume của SplashActivity để thực hiện animation cho TextView:

    @Override
    protected void onResume() {
        super.onResume();
        animate();
    }

Như vậy là mỗi khi vào app, hàm animate sẽ được gọi. Nó tạo ra 1 animation cho textview và khi animation kết thúc, kiểm tra trạng thái cấp quyền của ứng dụng.

Tổng kết

Ở phần đầu tiên của loạt bài hướng dẫn xây dựng app filter ảnh, mình đã giới thiệu tới các bạn các thư viện sử dụng trong app, cách tạo base project và xây dựng màn hình splash cho ứng dụng. Bài sau mình sẽ hướng dẫn xây dựng module chọn ảnh với các chức năng filter camera, và chức năng chọn ảnh trực tiếp từ bộ nhớ. Hãy theo dõi nhé.

Source code.

Kotlin Bài 1: Giới thiệu loạt bài hướng dẫn lập trình Kotlin

Chào mọi người, mình là Dương Vũ. Hôm nay mình sẽ viết bài đầu tiên trong loạt bài hướng dẫn lập trình Kotlin, sử dụng trong quá trình phát triển ứng dụng Android. Đây sẽ là loạt bài đầu tiên mình chia sẻ đến những ai yêu thích lập trình di động. Các kiến thức trong đây có thể sẽ mới mẻ, cũng có thể sẽ đơn giản đối với một số người đã trải qua. Vậy hãy cùng mình học tập, giúp đỡ và chia sẻ đến những người có cùng đam mê, sở thích về phát triển ứng dụng nhé. 

[toc]

Tại sao lại có loạt bài này

Mình nhận thấy Kotlin là 1 ngôn ngữ cực kỳ hay và thú vị. Giữa hàng trăm hàng nghìn bài hướng dẫn về lập trình Android ngoài kia, mình chưa tìm ra 1 loạt bài nào giới thiệu bài bản, có lộ trình và dễ hiểu để cho người mới có thể nắm bắt. Có thể có những bài hướng dẫn về lập trình Android sử dụng Kotlin, nhưng rất ít bài hướng dẫn về những điều cốt lõi của ngôn ngữ này. Thế nên mình quyết định tạo ra loạt bài viết Hướng dẫn ngôn ngữ lập trình Kotlin. Loạt bài này sẽ đảm bảo cho bạn những kiến thức cốt lõi, quan trọng nhất để bạn có thể sử dụng trong phát triển ứng dụng Android nói riêng và các nền tảng khác nói chung. Và nó cũng hướng cho bạn cách học và cách đào sâu nghiên cứu, mở rộng cho ngôn ngữ này. So, let’s start!!!

Loạt bài này hướng đến ai

  • Nó hướng đến những người phát triển ứng dụng Android, muốn “chuyển giao công nghệ” từ Java sang Kotlin.
  • Hướng đến những bạn muốn học lập trình Android, mà chưa cả biết Java là gì. Yes!!! Các bạn có thể nhảy ngay vào Kotlin, hoàn toàn không có trở ngại gì cả.
  • Hướng đến … mình. Mình thực sự thích viết, đó là sở thích lớn của mình, chưa kể viết sẽ làm mình trau dồi thêm đc kha khá kiến thức, hehe :))

Nội dung của loạt bài hướng dẫn ngôn ngữ lập trình Kotlin

  • 15 bài hướng dẫn cover toàn bộ kiến thức cốt lõi của ngôn ngữ lập trình Kotlin, giúp bạn có đủ kiến thức để bay vào dự án thực tế 1 cách tự tin và dễ dàng. Vì chỉ có 15 bài thôi nên mỗi  bài sẽ có độ dài tương đối, các bạn hãy chịu khó theo dõi nhé.
  • Do bài viết hướng đến cả những người mới bắt đầu, nên đôi chỗ có thể giải thích hơi kỹ càng quá (đối với những người đã là dev có kinh nghiệm), nên chỗ nào các bạn thấy dễ quá, hay dài vl, thì xin các bạn đừng ném gạch, mà hãy next qua phần tiếp theo :)) Thanks
  • Loạt bài này được viết theo ngôn ngữ… đời sống. Tức là mình sẽ cố diễn giải vấn đề theo cách dân dã nhất, dễ hiểu nhất, chứ không sử dụng nhiều những từ ngữ mang tính học thuật, sẽ rất khó khăn cho ai mới tiếp xúc.
  • Phần sugguest, định hướng để các bạn tìm tòi thêm những kiến thức mới, nâng cao hơn, phục vụ cho dự án sau này.

 

Cách theo dõi loạt bài Kotlin sao cho hiệu quả nhất

Hãy cài IntelliJ IDEA

Như đã nói lúc đầu, bài viết này mình sẽ tập trung vào cốt lõi của ngôn ngữ Kotlin, và chỉ Kotlin mà thôi, không Android, không gì khác. Mình sẽ có riêng 1 loạt bài về hướng dẫn lập trình Android bằng Kotlin để vận dụng kiến thức thu được từ loạt bài này. Vì vậy ở trong những bài hướng dẫn của loạt bài này, mình sẽ không dùng Android Studio để hướng dẫn, mà thay vào đó sẽ dùng IntelliJ IDEA để code các demo. Tại sao? Bởi dùng Android Studio thì mình chỉ có thể tạo các dự án Android. Mà cứ mỗi lần muốn xem kết quả của 1 đoạn code, ta lại phải build nguyên 1 … con app! Rất mất thời gian.

Vì thế để đơn giản hóa mình sẽ sử dụng IntelliJ IDEA. IDE này cho phép ta tạo ra các project Kotlin. Đại khái là ta có thể dùng nó để xem kết quả của các đoạn code nhanh gọn đơn giản hơn, thay vì build nguyên 1 cái app. Các bạn hãy yên tâm vì nếu các bạn đã quen dùng AndroidStudio thì IntelliJ IDEA cũng có cách sử dụng hoàn toàn tương tự, giống đến 90%. Vì AS cũng build từ IntelliJ IDEA mà ra thôi. Nếu các bạn chưa biết cách cài đặt IDE này, hãy tham khảo bài viết Hướng dẫn cài đặt IntelliJ IDEA của mình nhé.

Hãy đọc thật kỹ 

Mình sẽ viết rất kỹ, rất kỹ về từng vấn đề dù là nhỏ nhất. Vì vậy mỗi bài viết có thể khá dài. Tuy nhiên  mình mong các bạn đừng bỏ qua chữ nào, bởi đằng sau mỗi đoạn text miên man đó là các kiến thức cốt lõi được diễn giải ra theo 1 cách dễ hiểu nhất (đối với những người mới). Nếu bạn đọc lướt qua, có thể bạn sẽ bỏ qua mất 1 phần kiến thức sâu hơn, nâng cao hơn mà mình muốn truyền đạt cho các bạn.

Hãy ứng dụng

Mặc dù đây mới chỉ là loạt bài giới thiệu về ngôn ngữ lập trình Kotlin, chúng ta sẽ không xây dựng app trong loạt bài này. Tuy nhiên mình muốn các bạn hãy lập tức ứng dụng những gì học được ngay khi đọc các bài viết. Hãy mở IDE lên và sẵn sàng gõ code. Chỉ có cách code thật nhiều mới có thể giúp chúng ta làm quen với 1 ngôn ngữ mới. Mình cũng xin báo trước rằng mình sẽ sử dụng rất, rất nhiều code để demo. Bởi theo mình nghĩ đó là cách đơn giản nhất giúp các bạn nhanh chóng hiểu được kiến thức. Các bạn cũng hãy tự xây dựng cho mình những đoạn code demo để ứng dụng kiến thức đã học nhé. Sẽ rất hiệu quả đấy!

Hãy chia sẻ

Hãy chia sẻ loạt bài đến những người mới bắt đầu. Những ai còn đang hoang mang giữa vô vàn kiến thức cả tiếng Anh lẫn tiếng Việt ngoài kia. Họ cần 1 lộ trình rõ ràng để làm mục tiêu học tập. Dù đã kiểm tra rất kỹ nhưng có thể ở đâu đó trong các bài viết, mình có sai sót. Hãy đọc và nếu thấy có sơ suất ở đâu, comment cho mình biết để mình cập nhật lại, để nội dung truyền tải được tốt hơn. Hãy cho đi, để được nhận lại.

Bắt đầu

Có thể các bạn sẽ tự hỏi rằng

  • Java ổn rồi ,cần thêm cái thứ Kotlin đó làm chi.
  • Thứ mới ra đó (Kotlin) chắc gì đã ổn định, Java thì best rồi, chả cần thiết phải mò vào Kotlin
  • Tao thích code Java, tao đã quen với nó, tao ghét tất cả mọi thứ khác!
  • Vân vân

Thì đúng, mình không phủ nhận những điều mà các bạn nghĩ ở trong đầu. Nhưng các bạn hãy thử nhìn vào những điểm tốt của Kotlin so với Java, rồi các bạn sẽ thấy, mình có lý do khi viết loạt bài hướng dẫn này.

Sự ngắn gọn

So với Java, Kotlin là vô cùng, vô cùng ngắn gọn. Bạn có thể hình dung, khi mà việc định nghĩa 1 class trong Java có thể tốn đến vài chục dòng hay cả trăm dòng code thì với Kotlin, ta chỉ tốn 1 dòng để làm điều tương tự. Mình sẽ ví dụ ngay đây. Sau đây sẽ là 1 class trong Java:

public class User {
   String name;
   int age;
   String address;
   String phoneNumber;

   public User(String name, int age, String address, String phoneNumber) {
       this.name = name;
       this.age = age;
       this.address = address;
       this.phoneNumber = phoneNumber;
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public int getAge() {
       return age;
   }

   public void setAge(int age) {
       this.age = age;
   }

   public String getAddress() {
       return address;
   }

   public void setAddress(String address) {
       this.address = address;
   }

   public String getPhoneNumber() {
       return phoneNumber;
   }

   public void setPhoneNumber(String phoneNumber) {
       this.phoneNumber = phoneNumber;
   }
}

Trên đây là 1 class với 4 thuộc tính, hàm khởi tạo và các phương thức get, set cho các thuộc tính. Trong Kotlin nó sẽ như thế này:

data class User(var name:String,var age: Int,var address:String,var phoneNumber:String)

Vỏn vẹn 1 dòng duy nhất! Đó chính là sự khác biệt về sự ngắn gọn của Kotlin. Đây chỉ là 1 ví dụ. Trong các dự án thực tế ta sẽ còn gặp hàng trăm tình huống mà nếu sử dụng Kotlin, số dòng code phải viết có thể giảm đi đến 90%. Rất tuyệt đúng không. (mình sẽ viết những loạt bài về xây dựng các ứng dụng với quy trình như trong thực tế)

An toàn hơn với các đối tượng null

Như các bạn đã biết thì NullPointerException là 1 lỗi hay gặp nhất ở trong các dự án viết bằng Java. Java cho phép chúng ta gán giá trị null đến 1 đối tượng, nhưng khi ta truy xuất đến đối tượng đó, lỗi NullPointerException sẽ được bắn ra. Mình đã từng làm 1 dự án Android, khi update version mới cho app, mình đã quên không cập nhật đối tượng Java theo config mới trên server. Và kết quả là trong 2 ngày đã có hơn 5000 log crash được báo về hệ thống, tất cả đều là NullPointerException gây ra, bạn có thể hiểu được cảm giác thốn đến thế nào, khi có đến 95% người dùng app bị crash :-s Yeah, và Kotlin được thiết kế để giảm thiểu tối đa sự xuất hiện của cái Exception khốn nạn này 😀 Cực an toàn và tiện lợi, mình sẽ có riêng 1 bài về vấn đề này, các bạn hãy đón chờ nghe.

Hoàn toàn tương thích với Java

Yes! Bạn có thể làm 1 dự án với cả Java và Kotlin cùng 1 lúc. Kotlin được thiết kế để giao tiếp với Java 1 cách hoàn hảo. Bạn có thể sử dụng Java class trong Kotlin và ngược lại, cũng có thể sử dụng Kotlin class trong Java. Tuyệt đúng ko? Điều này có nghĩa là Kotlin có thể sử dụng tất cả các thư viện và framework Java hiện có. Điều này cũng có nghĩa là nếu bạn thích, bạn có thể mở rộng, phát triển dự án Java cũ với Kotlin rất dễ dàng.

Trên đây chỉ là 3 trong số rất nhiều điều hay và thú vị khác của Kotlin mà Java không có. Hãy cùng mình tìm hiểu kỹ hơn trong các bài viết loạt bài này nhé.