Khi build 1 app mobile thì điều quan trọng trước tiên là làm sao để tạo ra một UI chính xác, đẹp, xử lý được tất cả các yêu cầu từ khách hàng hoặc designer.

Mình xin phép tổng hợp 1 vài nguyên tắc và kinh nghiệm khi thực hiện dựng layout trong Android để mục đích tham khảo và lưu trữ. Có thể có nhiều thứ là bình thường, nhưng có một số cái cũng research khá nhiều mới tìm thấy :)

CÁC NGUYÊN TẮC CƠ BẢN
KHI NÀO DÙNG MARGIN? KHI NÀO DÙNG PADDING?
Có thể khi đề cập đến vấn đề này, bạn sẽ nghĩ rằng ai cũng biết. Nhưng không hẳn, rất nhiều người vẫn nhầm lẫn hoặc sử dụng qua loa 2 thuộc tính này trên layout. Việc sử dụng không đúng trong 1 vài trường hợp có thể cho ra kết quả mong muốn, nhưng không thể hiện bạn hiểu đầy đủ lĩnh vực mà bạn đang làm.

- Như đã nói trong 1 số trường hợp đơn giản, margin và padding cho ra kết quả giống nhau. Và ta mặc nhiên xem như đã hoàn thành task đó, trong khi nó vẫn tiềm ẩn sai sót khi nâng cấp UI về sau.

Layout Margin: Khi ta khai báo thuộc tính layout_margin (canh lề) cho một thành phần nào đó, thì nó sẽ tạo ra một khoảng cách giữa thành phần đó với các thành phần xung quanh nó (top, right, bottom và left). Tạo ra sự phân chia giữa các thành phần trong trang layout được thể hiện rõ ràng hơn.

Padding: là phần nằm giữa phần hiển thị nội dung và đường viền (border). Khi một thành phần được khai báo padding thì sẽ tạo ra một khoảng trống với dường viền giúp nội dung dễ nhìn hơn.

 

SỬ DỤNG ĐỊNH NGHĨA STYLE NHIỀU NHẤT CÓ THỂ

Khi thiết kế layout trong Android, ta thường chỉ chú ý đến các thuộc tính của đối tượng trong layout mà quên mất rằng Android có 1 phần định nghĩa styles.xml cho việc kế thừa và điều chỉnh layout tốt hơn. Chúng ta thường cố gắng chỉnh các thuộc tính sao cho UI trở nên hợp lý mà quên mất việc tìm các thành phần chung giữa các layout và tạo thành các style.

Vì sao nên sử dụng style trong android?

- Đầu tiên, bạn sẽ thống nhất được các thuộc tính của những thành phần chung giữa các layout, cố gắng định nghĩa nó 1 lần và chỉ cần gọi lại mà không cần lặp lại việc tương tự trong các layout khác, mất thời gian.

- Khi cần thay đổi 1 thành phần thuộc tính nào đó, như màu sắc, font sỉz, background... chỉ cần thay đổi style tương ứng cho tất cả các layout được sử dụng.

- Khi có nhiều thành phần gần tương đồng về style. Ta có thể tạo ra các base style và sau đó định nghĩa các style con kế thừa "parent" các base style này. Rất nhanh, không cần lặp lại các thuộc tính tương đồng.

- Dễ dàng thay đổi hay xóa bỏ các thành phần tương đồng giữa các layout, ta chỉ việc thay đổi hoặc xóa style (xóa style thì IDE sẽ báo lỗi cho tất cả các layout có gọi nó, đảm bảo không bao giờ sót layout).

 

CUSTOM DRAWABLE BẰNG VIỆC SỬ DỤNG HÌNH ẢNH NHƯ THẾ NÀO LÀ HỢP LÝ?

Khi sử dụng custom drawable, ta có 2 cách là xử sụng xml để define chúng trong folder drawable hoặc sử dụng image resource (hay còn gọi là slicing images) để tạo ra drawable.

Thông thường chúng ta khuyến khích cố gắng sử dụng cách làm đầu tiên với bất cứ design layout nào có thể từ designer. Sử dụng cơ chế drawable xml render của chính android cho phép giảm kích thước ứng dụng vì không phải mang vác theo cả mớ ảnh resource như cách 2 và cũng tăng khả năng responsive với nhiều loại devices.

Vậy, nếu design quá phức tạp, như các hiệu ứng design trên button... thì ta phải sử dụng image resource để custom. Vấn đề là ảnh thì sẽ fix kích thước và không designer nào có thể xử lý nổi từng design cho từng loại device. Vậy làm sao để xử lý responsive khi sử dụng image resource trong android. Câu trả lời là sử dụng Custom Nine-Patch.

 

Một drawable Nine-patches là một dạng đặc biệt của hình ảnh đó có thể được thu nhỏ theo chiều rộng và chiều cao trong khi duy trì tính toàn vẹn hình ảnh của mình. Nine-patches vá lỗi là cách phổ biến nhất để xác định sự xuất hiện của các nút Android, mặc dù bất kỳ loại drawable có thể được sử dụng.

Cách thiết kế nine-patches bạn có thể tìm thấy các link hướng dẫn cụ thể trên google. Nhớ răng, mặc định Android SDK có cung cấp 1 tool xử lý vấn đề này xuất ra các file *.9.png cho bạn tại: android-sdk\tools\draw9patch.bat

 

SỬ DỤNG HỆ THỐNG ĐƠN VỊ ĐO TRONG ANDROID NHƯ THẾ NÀO?

Cái này cũng là một lưu ý nhỏ mà quan trọng khi thiết kế responsive layout tốt hơn.

1. Pixel (px): có khi gọi là pel (xuất phát từ “picture element”), chúng ta hay gọi là điểm ảnh, có dạng hình vuông.

2. dp, hay dip hay Density-independent Pixels: có khi được gọi là Device-independent Pixels. Đây là một đơn vị đo chiều dài vật lý cũng giống như inch, cm, mm… mà Google thường áp dụng để đo kích thước màn hình của thiết bị.

3. DPI – Dots per inch hay PPI – Pixels per inch: số điểm ảnh (pixels) trên 1 inch của màn hình thiết bị, con số này càng lớn thì màn hình thiết bị hiển thị hình ảnh càng mịn và đẹp.

Dựa vào dpi người ta chia làm loại màn hình như sau:

– small: ldpi (120dpi)

– normal: mdpi (160dpi)

– large: hdpi (240dpi)

– x-large: xhdpi (320dpi).

Với mỗi loại này thì 1 dp tương ứng với số lượng pixels khác nhau, được tính theo công thức:

px = dp * (dpi / 160)

Ví dụ với thiết bị có dpi là 320 thì với 10 dp ta có: 10 * (320/160) = 20 px, 1 dp tương đương 2 px.

4. PT – Point: khái niệm pt tương tự như dp là một đơn vị đo kích thước thực, nhưng khác với dp:

 
1 pt = 1/72 inch, trong khi 1 dp = 1/160 inch
 
5. SP – Scale-independent Pixels: Cũng tương tự như dp, nhưng sp thường được dùng cho font size của văn bản.

6. Cách chuyển đổi giữa dp và px hay dùng trong android code khi cần xử lý responsive hay điều chỉnh kích thước view.

public static float convertDpToPixels(float dp,Context context){
    Resources resources = context.getResources();
    DisplayMetrics metrics = resources.getDisplayMetrics();
    float px = dp * (metrics.densityDpi/160f);
    return px;
}

public static float convertPixelsToDp(float px,Context context){
    Resources resources = context.getResources();
    DisplayMetrics metrics = resources.getDisplayMetrics();
    float dp = px / (metrics.densityDpi / 160f);
    return dp;
}

 

 

 


MỘT SỐ VẤN ĐỀ VÀ KINH NGHIỆM GIẢI QUYẾT 

TẠO AUTOSCROLL TEXTVIEW KHI NỘI DUNG CỦA NÓ VƯỢT QUÁ CHIỀU NGANG

Khi nội dung của TextView (trên titlebar chẳng hạn) vượt quá chiều ngang của nó thì ta có thể tạo hiệu ứng auto horizontal scroll để người dễ dàng nắm bắt được nội dung của nó. Đây cũng là 1 trong những cách để xử lý responsive với nhiều loại thiết bị trong andorid.

 

Giải quyết: Thêm các thuộc tính cho TextView vào layout như bên dưới

<TextView
        android:id="@+id/tv_titlebar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:ellipsize="marquee"
        android:fadingEdge="horizontal"
        android:marqueeRepeatLimit="marquee_forever"
        android:singleLine="true" />

Set selected cho nó trong code khi init nội dung của TextView

tvTitle.setText("DEMO DEMO DEMO DEMO DEMO DEMO DEMO DEMO");
tvTitle.setSelected(true);

 

TỰ ĐỘNG THÊM DẤU ... VÀO SAU NỘI DUNG TEXTVIEW KHI NỘI DUNG VƯỢT QUÁ ĐỘ DÀI

Như trường hợp trên, tuy nhiên thường áp dụng với các dòng content không quan trọng và có action để xem chi tiết hơn trong các view tiếp theo.

 

Giải quyết: Thêm các thuộc tính cho TextView vào layout như bên dưới

<TextView
        android:id="@+id/tv_titlebar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:ellipsize="end"
        android:singleLine="true" />

 

TẠO HIỆU ỨNG FOREGROUND SELECTOR CHO LISTITEM

Yêu cầu đơn giản là tạo hiệu ứng selector như hình bên dưới. Rất ngắn gọn, nhưng làm sao?

Giải quyết: Sau khi đã set custom drawable selector cho ListView thì set thêm thuộc tính android:drawSelectorOnTop=true vào.

XML mẫu như thế này:

<ListView
        android:id="@+id/lv_notification"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="@color/line_list_divider"
        android:dividerHeight="1dp"
        android:background="@color/bg_list_color"
        android:drawSelectorOnTop="true"
        android:listSelector="@drawable/list_item_home" />

 

LISTVIEW TỰ ĐỘNG GIÃN CHIỀU CAO THEO CHÍNH CÁC LISTITEM BÊN TRONG NÓ

Khi sử dụng ListView, chúng ta thường set chiều cao của nó là match_parent. Tuy nhiên, trong vài trường hợp ta cần set chiều cao của nó vừa đúng tổng chiều cao của các item của nó. Vậy làm sao?

 

Giải quyết: Cách tốt nhất là tạo 1 class custom ListView và override lại phần tính toán chiều cao của ListView khi render. Khi render, nó sẽ tự get Adapter và tính toán chiều cao các item bên trong + phần padding của chính ListView. Như thế là xong.

public class WrapContentListView extends ListView {

    ...

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = View.MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, View.MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }

    @Override
    public void setAdapter(ListAdapter adapter) {
        super.setAdapter(adapter);
        setHeightWrapContent();
    }

    public void setHeightWrapContent() {
        ListAdapter adapter = getAdapter();
        if (adapter == null) {
            return;
        }
        int totalHeight = getPaddingTop() + getPaddingBottom();
        ;
        for (int i = 0; i < adapter.getCount(); i++) {
            View listItem = adapter.getView(i, null, this);
            listItem.measure(0, 0);
            totalHeight += listItem.getMeasuredHeight();
        }

        ViewGroup.LayoutParams params = this.getLayoutParams();
        params.height = totalHeight + (this.getDividerHeight() * (adapter.getCount() - 1));
        this.setLayoutParams(params);
    }
}
<customview_package.WrapContentListView
                            android:id="@+id/lv_extservice"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:scrollbars="none" />

 

GRIDVIEW TỰ ĐỘNG GIÃN CHIỀU CAO THEO CHÍNH CÁC LISTITEM BÊN TRONG NÓ

Bối cảnh thì cũng tương tự như ListView phía trên thôi. Tuy, điểm khác biệt quan trọng là GridView có số lượng cột và dòng, chứ k chỉ là dòng như ListView.

 

Giải quyết: Như ListView, ta custom lại GridView và sử dụng nó trong layout.

public class WrapContentGridView extends GridView {

    ...

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = View.MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, View.MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }

    @Override
    public void setAdapter(ListAdapter adapter) {
        super.setAdapter(adapter);
        setHeightWrapContent();
    }

    public void setHeightWrapContent() {
        int totalHeight = Utils.getGridViewHeightBasedOnChildren(this, getNumColumns(), -1);

        ViewGroup.LayoutParams params = this.getLayoutParams();
        params.height = totalHeight;
        this.setLayoutParams(params);
    }

    private int getGridViewHeightBasedOnChildren(GridView gridView, int numColumn) {
        int totalHeight = gridView.getPaddingTop() + gridView.getPaddingBottom();

        ListAdapter adapter = gridView.getAdapter();
        if (adapter != null) {
            int numRow = (int) Math.ceil(adapter.getCount() * 1.0 / numColumns);

            for (int i = 0; i < numRow; i++) {
                View listItem = adapter.getView(i, null, gridView);
                listItem.measure(0, 0);
                totalHeight += listItem.getMeasuredHeight();

                if (i != 0) {
                    totalHeight += gridView.getVerticalSpacing();
                }
            }
        }
        return totalHeight;
    }
}

 

TEXTVIEW CÓ STYLE GẠCH CHÂN

Yêu cầu là hiển thị đoạn TextView trên layout có gạch chân.

 

Giải quyết: Có nhiều cách xử lý trong trường hợp này: xử lý nội dung trong onCreate của layout,... Tuy nhiên, cách đơn giản và nhanh nhất vẫn là custom 1 cái TextView có underline và khi cần chỉ gọi nó ra từ layout xml.

public class UnderlineTextView extends BaseCustomTextView {
    protected boolean m_modifyingText = false;

    ...

    public UnderlineTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                //Do nothing here... we don't care
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                //Do nothing here... we don't care
            }

            @Override
            public void afterTextChanged(Editable s) {
                applySpannable();
            }
        });

        applySpannable();
    }

    private SpannableString getSpannableString() {
        SpannableString finalText = new SpannableString(getText().toString());
        finalText.setSpan(new UnderlineSpan(), 0, finalText.length(), 0);
        return finalText;
    }

    private void applySpannable() {
        if (m_modifyingText)
            return;

        m_modifyingText = true;
        super.setText(getSpannableString(), BufferType.SPANNABLE);
        m_modifyingText = false;
    }
}

 

IMAGEVIEW VUÔNG

Khi sử dụng ImageView để hiển thị icon của 1 số item, yêu cầu chung thường set nó là hình vuông theo đúng tỉ lệ. Nhiều bạn thường nghĩ là cứ đưa ImageView vào bình thường rồi lên code xử lý xử lý lại chiều rộng và chiều cao. Cũng ok, nhưng không phải là tốt nhất.

 

Giải quyết: Custom lại ImageView như bên dưới (cho trường hợp kích thước vuông phụ thuộc theo chiều cao của ImageView, ngược lại tương tự).

Lưu ý, việc custom vuông này có thể áp dụng cho bất kỳ view nào, chỉ cần extends lại và gọi customview từ layout.

public class SquareVerticalImageView extends ImageView {

    ...

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(heightMeasureSpec, heightMeasureSpec);
    }
}

 

IMAGEVIEW CÓ KÍCH THƯỚC TỈ LỆ THEO DRAWABLE CHỨA TRONG NÓ

Khi cần show các hình ảnh trong layout với yêu cầu show hết theo chiều rộng và giữ nguyên tỉ lệ ảnh thì phải làm sao. Đứng nói là lại lên code xử lý set layoutparams nhé :v

 

Giải quyết: Custom lại ImageView như bên dưới cho trường hợp giãn kích thước tối đa chiều ngang mà vẫn giữ tỉ lệ Drawable trong nó.

Lưu ý, việc custom vuông này có thể áp dụng cho bất kỳ view nào có chưa Drawable resource như ImageView, ImageButton..., chỉ cần extends lại và gọi customview từ layout.

public class AspectRatioHorizontalImageView extends ImageView {

    ...

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (getDrawable() != null) {
            int width = MeasureSpec.getSize(widthMeasureSpec);
            int height = width * getDrawable().getIntrinsicHeight() / getDrawable().getIntrinsicWidth();
            setMeasuredDimension(width, height);
        } else {
            setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
        }
    }

}

 

DISABLE SỰ KIỆN TOUCH VÀO VIEW (KHI LỒNG VIEW DẠNG CHỈ HIỂN THỊ) VÀO 1 VIEW TOUCHABLE

Khi lồng 1 view touchable vào 1 view touchable khác, mặc định Android sẽ bắt sự kiện touch ở cả 2 view. Tuy nhiên, trong 1 số trường hợp ta chỉ xem view lồng thêm vào là view chỉ hiển thị mà không quan tâm đến action của nó, nhưng nó vẫn chiếm touch của view cha. Đây là 1 issue khá khó chịu nhưng không hẳn nhiều người biết cách giải quyết nhanh nhất.

 

Giải quyết: Rất đơn giản, custom lại cái view cần lồng vào và xử lý override như sau

public class NotTouchableView extends View {

    ...

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return false;
    }
}

 

FILL CONTENT TRONG SCROLLVIEW

Khi sử dụng ScrollView, ta thường phải lồng vào trong đó 1 LinearLayout có thuộc tính layout_height = WRAP_CONTENT để ScrollView nhận được phần độ cao của nội dung bên trong nó. Có 1 vấn đề, nếu ta set custom background cho LinearLayout bên trong thì khi content của LinearLayout chưa vượt quá độ cao của ScrollView, nó sẽ chỉ hiển thị đúng phần background tương xứng. Với điều này, trong vài trường hợp sẽ làm xấu app.

 

Giải quyết: Thêm thuộc tính fillViewport="true" vào ScrollView, content của LinearLayout sẽ tự fill hết độ cao của ScrollView.

 <ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        ...
    </LinearLayout>

</ScrollView>

 

GOOGLE MAP (HOẶC 1 VIEW NÀO KHÁC CÓ TOUCH EVENT) KHÔNG THỂ BẮT CHÍNH XÁC SỰ KIỆN TOUCH, DRAG KHI LỒNG VÀO TRONG 1 SCROLLVIEW

Map là 1 control rất hay sử dụng, nhưng chúng ta thường chỉ hay nhúng nó vào activity hay fragment cố định. Vậy khi được yêu cầu nhúng vào 1 ScrollViewer thì làm sao. Ok, cứ nhúng vào trong LinearLayout của ScrollView thôi, nhưng lúc đó, các gesture của map sẽ không hoạt động được nữa vì nó sẽ bị ScrollView chiếm.

 

Giải quyết: Thực hiện custom lại ScrollView như sau

public class CustomScrollView extends ScrollView {

    List<View> mInterceptScrollViews = new ArrayList<View>();

    ...

    public void addInterceptScrollView(View view) {
        mInterceptScrollViews.add(view);
    }

    public void removeInterceptScrollView(View view) {
        mInterceptScrollViews.remove(view);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        // check if we have any views that should use their own scrolling
        if (mInterceptScrollViews.size() > 0) {
            int x = (int) event.getX();
            int y = (int) event.getY();
            Rect bounds = new Rect();

            for (View view : mInterceptScrollViews) {
                view.getHitRect(bounds);
                if (bounds.contains(x, y)) {
                    //were touching a v that should intercept scrolling
                    return false;
                }
            }
        }

        return super.onInterceptTouchEvent(event);
    }
}

Khi init layout view trong code, ta sẽ add các view mà ScrollView sẽ ưu tiên Touch Event cho chúng

scrollView.addInterceptScrollView(mapView);

Lưu ý: Google Map chỉ là 1 ví dụ điển hình. Bạn có thể sử dụng cách này cho các view con khi muốn ưu tiên Touch Event cho chúng.

 

NGĂN SCROLLVIEW KHÔNG TỰ ĐỘNG SCROLL ĐẾN 1 VIEW KHI NHÚNG SCROLLVIEW VÀO VIEWPAGER

Tình huống khá ít gặp, nhưng làm nhiều rồi cũng gặp thôi. Vấn đề như sau:

- Ta có 1 viewpager chứa các fragment.

- Trong các fragment ta có nhúng các ScrollView để cuộn nội dung bên trong nó

- Khi swipe giữa các trang của viewpager thì đôi lúc ScrollView tự động scroll tới 1 view khác trong content mà không rõ vì sao :v

Đây không hẳn là bug, đây là 1 tính năng :D nhưng ta không thích

 

Giải quyết: Custom các ScrollView đó thành như sau là ok

public class NotAutoScrollingScrollView extends ScrollView {

    ...

    @Override
    public void requestChildFocus(View child, View focused) {
        if (focused instanceof ListView)
            return;
        super.requestChildFocus(child, focused);
    }

}

 

SET BACKGROUND CHO LAYOUT DÃN ĐÚNG TỈ LỆ ẢNH RESOURCE VÀ ƯU TIÊN CONTENT ẢNH THEO 1 CHIỀU NÀO ĐÓ MÀ KHÔNG PHẢI LÀ CENTER NỘI DUNG

Hơi khó hiểu 1 chút, đại khái là yêu cầu muốn set 1 background cho layout là 1 cái ảnh. Có điều ng ta muốn lấy ưu tiên nội dung background theo phía dưới ảnh (tức là phần trên của ảnh có thể mất không quan trọng nhưng phần dưới cùng là bắt buộc :v). Thêm 1 yêu cầu nữa là phần ảnh background này phải keep tỉ lệ của ảnh, không được stretch làm méo hình ảnh.

- Nếu chỉ đơn thuần là set background đến file resource đó thì hình ảnh sẽ bị méo.

- Nếu sử dụng ImageView lồng vào bên dưới FrameLayout để làm background:

  + Nếu set thuộc tính scaleType là center (bất kỳ center nào) thì sẽ đảm bảo ratio nhưng k đảm bảo ưu tiên phần content bottom

  + Nếu set thuộc scaleType là fit (start hoặc end, XY chắc chắn không được vì vỡ ratio) thì không đảm bảo được dãn theo hết chiều ngang của layout nền.

 

Giải quyết:

- Đầu tiên sử dụng custom AspectRatioHorizontalImageView để ImageView tự động dãn kích thước theo chiều ngang mà vẫn đảm bảo tỉ lệ của Drawable resource.

- Lồng ImageView đó vào FrameLayout với thuộc tính layout_gravity là bottom (hoặc top với trường hợp ưu tiên phía trên).

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

    <biz.vipliner.view.AspectRatioHorizontalImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:src="@drawable/bg_home" />

    <LinearLayout
        android:id="@+id/ln_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        ...

    </LinearLayout>

</FrameLayout>

Như vây, ta đã có kết quả như mong muốn:

 

THỰC HIỆN HIỆU ỨNG ĐỔ BÓNG (SHADOW) TRONG KHI CUSTOM DRAWABLE XML

Android hỗ trợ cho chúng ta có thể custom Drawable bằng xml như layout với các đối tượng như selector, item, layer-list, shape, solid, corners, gradient... Rất phong phú, tùy khả năng phối hợp mà sẽ tái tạo được hầu hết các hiệu ứng trong design của designer. Hạn chế sử dụng image resource.

Tuy nhiên, 1 thiếu sót cho đối với các SDK <5.0 là không hỗ trợ hiệu ứng đổ bóng trong khi thiết kế drawable. Vậy phải làm sao khi vẫn không muốn sử dụng drawable nine-patches?

 

Giải quyết: Tất nhiên nếu đã không hỗ trợ thì phải tìm cách tái tạo gần tương đường hoặc đôi khi giống hệt :D

- Nguyên tắc cơ bản là hay vẽ lại cái hình tròn phía trên bằng 1 màu xám nhạt (giả màu shadow) dưới cùng trước tiên. Cho lệch tối đa về phía dưới.

- Chồng tiếp 1 hoặc 2 hình tròn xám nhạt nữa và lệch lần lượt 1dp, 2dp lên dần phía trên.

- Sau cùng vẽ hình tròn trắng chồng đúng margin 3dp, hoặc 2dp.

- Có thể chồng tiếp theo các màu gradient khác nếu design phức tạp hơn nữa.

XML mẫu như sau:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- shadow-->
    <item
        android:bottom="0dp"
        android:left="1dp"
        android:right="1dp"
        android:top="2dp">
        <shape android:shape="oval">
            <solid android:color="#11000000" />
        </shape>
    </item>
    <item
        android:bottom="1dp"
        android:left="2dp"
        android:right="2dp"
        android:top="2dp">
        <shape android:shape="oval">
            <solid android:color="#19000000" />
        </shape>
    </item>

    <!-- backbround-->
    <item
        android:bottom="2dp"
        android:left="2dp"
        android:right="2dp"
        android:top="2dp">
        <shape android:shape="oval">
            <solid android:color="@color/white" />
        </shape>
    </item>
    <item
        android:bottom="4dp"
        android:left="4dp"
        android:right="4dp"
        android:top="4dp">
        <shape android:shape="oval">
            <gradient
                android:angle="90"
                android:endColor="@color/white"
                android:startColor="#E2F8FF" />
        </shape>
    </item>
</layer-list>

Leave a Comment

Fields with * are required.