Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> ระบบ >> Android

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI

ซีรีส์ Kriptofolio App - ภาค 2

จริงๆ เริ่มสร้างแอปใหม่ยังไง? First Move คืออะไร? คิดว่าแค่เปิด Android Studio โค้ดเลย? คิดผิด! นั่นคือสิ่งที่ผมไม่อยากให้ทำ เพราะเสียหายกว่าดี แต่อยากเขียนโค้ดทันทีมาก

แนะนำ Focus ทำ Plan กับ UI Mockups ทุกแอปดีควรเริ่มต้นที่นี่ ด้วยวิธีนี้ไม่เสียเวลา และสร้าง Quality Products ตั้งแต่ต้น

ภาคนี้จะนำเสนอ Mockups ของ "Kriptofolio" (เดิม "My Crypto Coins") และวิธีสร้าง พร้อมสร้าง UI Layouts ทั้งหมด Layouts จะเป็น Foundation แข็งแกร่งบอกชัดเจนว่าจะโค้ดอะไร สุดท้าย Localize App หลายภาษา และจัดการ RTL Languages

เนื้อหาซีรีส์

  • Introduction: Roadmap สร้าง Modern Android App 2018–2019
  • Part 1: แนะนำ SOLID Principles
  • Part 2: เริ่มสร้าง Android App: Mockups, UI, XML Layouts (คุณอยู่ที่นี่)
  • Part 3: Architecture Patterns และการนำไปใช้
  • Part 4: Dependency Injection ด้วย Dagger 2
  • Part 5: RESTful Web Services ด้วย Retrofit, OkHttp, Gson, Glide, Coroutines

Mockups

สร้าง Mockups หลายวิธี ง่ายที่สุด: ดินสอ กระดาษ เริ่มวาดเลย ไม่มีค่าใช้จ่าย เริ่มทันที แต่ต้องมียางลบ เพราะไม่มี Undo 😄

ถ้าต้องการ Functionality มากกว่า ใช้ Software สร้าง Mockups รายละเอียด ผมชอบ Software แม้ต้องลงทุนเงินและเวลาเรียน

Software ตลาดเยอะ ต้องสำรวจเองว่าตัวไหนเหมาะ ผมใช้ Balsamiq Mockups for Desktop รวดเร็ว มีประสิทธิภาพ ใช้ง่าย Wireframing Software พอใจ แนะนำสำหรับ Android Apps

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI

เริ่ม My Crypto Coins ด้วย Mockups ที่คิดดีและรายละเอียด คิดว่า Detailed Mockups = หลีกเลี่ยง Mistakes ไม่เสียเวลาเปลี่ยน Functionality ระหว่าง Develop หากลงความพยายาม Mockups ด้วยจินตนาการเล็กน้อย จะเห็นและรู้สึก Final Product

เป้าหมาย: Define ทุกอย่างใน Mockups ให้เหมือน End Product พยายามไม่เร้า ใช้เวลาเท่าที่ต้อง Mockups Final:

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI
Mockup My Crypto Coins App ด้วย Balsamiq Mockups Software

Stock Design — Material Design

เรื่อง Visual Design สำคัญอีกอย่าง เวลานี้ Material Design เป็น Stock Visual Design ที่ Google แนะนำทุก Android Apps ไม่ผิดแน่ถ้าใช้ Stock Design

My Crypto Coins ใช้ Material Design ปฏิบัติตาม Best Practices ใน Guidelines ออนไลน์

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI

Material.io/guidelines - Official Living Specs Document เป็น Design Guidelines โปรเจกต์นี้

Layouts

มี Wireframes แล้ว สร้าง Real UI อีกครั้งอย่ารีบโค้ด แนะนำ Focus สร้าง XML Layouts ทั้งหมด ความคิด: ยิ่งใส่ใน XML มากเท่าไร ยิ่งโค้ดน้อยลง

Android Team ปรับปรุง XML Layouts ตลอด ใส่ Features ใหม่ บางครั้งอยากเขียนโค้ดสั้นๆ ให้ UI ตามใจ แต่ควร Research หาว่าทำได้ใน XML ไหม Less Code = Cleaner Project เข้าใจง่าย Maintain ง่าย

สร้าง XML Layouts ครบแล้ว จะรู้สึกว่า App ทำเสร็จแล้ว Launch ได้ดู Feel ของ End User ไม่สำคัญ Fake Data ไม่ทำอะไร โอกาสสุดท้ายทำ Drastic Changes

ถ้าสร้าง App ให้คนอื่น เวลานี้ Present ดีที่สุด อาจขอ Last Minute Changes ผ่านได้ หลีกเลี่ยงโค้ด Functionality ที่ไม่ถูกใช้ บางคนไม่มีจินตนาการ ต้องดูสัมผัสถึงตัดสินใจ อย่าทิ้ง Layouts ไว้ขั้นสุดท้าย

โปรเจกต์นี้ใช้ Components ที่พบใน Modern Android Apps ทุกตัว:

  • CoordinatorLayout - Super-powered FrameLayout ความสามารถหลัก: Coordinate Animations/Transitions ของ Views ภายใน
  • AppBarLayout - Vertical LinearLayout Implement Material Design App Bar Features: Scrolling Gestures
  • Toolbar - Generalization ของ Action Bars ใช้ใน App Layouts
  • CollapsingToolbarLayout - Wrapper สำหรับ Toolbar Implement Collapsing App Bar ใช้เป็น Direct Child ของ AppBarLayout
  • ConstraintLayout - ViewGroup จัดตำแหน่ง/ขนาด Widgets ยืดหยุ่น RelativeLayout บน Steroids
  • SwipeRefreshLayout - Widget Implement Swipe-to-refresh Pattern Detect Vertical Swipe แสดง Progress Bar Trigger Callbacks
  • FloatingActionButton - Circular Button Trigger Primary Action ใน App UI

Main Screen ใช้ Combination ทุก Components สร้าง UI/UX ดี Users Expand/Collapse Top Part หา Total Portfolio Value เช็ค 24h Change เปลี่ยน Fiat Currency

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI
Main Screen Layout Creation

ยังมี Components อื่นๆ มาก Master Palette 全部สำคัญ เพื่อรู้ว่าเมื่อไหร่ควรใช้ไหน Mobile Trends เปลี่ยน Palette ก็ขยาย

ไม่พูดทุก Component แนะนำสำรวจเอง สำหรับผมวิธีดีที่สุดเข้าใจคือ Build XML Layouts ด้วยมือ แทน Drag & Drop ใน Android Studio

Styles, Colors, Dimensions, Strings, Icons

ดูไม่น่าสำคัญ แต่ขอให้ลงมือทำจริง ถ้าอยากทำ Modern Exclusive App จำไว้: Blog Series นี้สอนสร้าง MODERN Android App! มักตัดสิน App จาก Look ก่อน Function

โอกาสสร้างความประทับใจ Users ตั้งแต่ต้น Tips:

  • XML Layouts: หา Attributes ที่ซ้ำ Move ไปเป็น XML Styles แยก → Layout Files สั้นกว่า Control Styling ทุกอย่างจากที่เดียว Benefit: User เลือก App Skin (Bright/Dark) ได้
  • Colors: Define แยกไฟล์ ทดลอง Look เปลี่ยนสีได้ทันที บางครั้ง Product เดิมชีวิตใหม่ Engage Users ใหม่แค่ Refresh Colors เว็บช่วยเลือกสีเยอะ ผมชอบ MaterialPalette.com
  • Dimensions: Define แยกไฟล์ ปรับ App ให้ดูดีทุก Screen Size
  • Strings: อย่า Hard-code Android Studio จะเตือน อย่าลืมแปลกว่า Separate Strings = Translate App ได้หลายภาษา
  • Icons: ใช้ XML Vector Drawable Format Standard ใหม่ แนะนำ หลีกเลี่ยง Pixelation ไอคอน Material Design Pro จาก Community: MaterialDesignIcons.com (ผมใช้สำหรับ My Crypto Coins)

RecyclerView

Main Screen My Crypto Coins = List Cryptocurrencies ที่ User ถือ RecyclerView เหมาะที่สุด Widget แสดง Scrolling List จาก Large Data Sets (หรือ Data ที่เปลี่ยนบ่อย)

จากข้อดี RecyclerView = Recommended Component สำหรับ List Screen ใดๆ Advanced และ Flexible กว่า ListView พูดถึงภายหลัง ภาคนี้สร้าง Layouts เท่านั้น ไม่โฟกัส Coding

เพื่อเห็น App Visually ต้อง Implement RecyclerView ด้วย Code Steps:

1. Add RecyclerView Component

MainActivity Layout = activity_main.xml Include content_main.xml (Fragment) MainActivityListFragment Inflate fragment_main_list.xml → Add RecyclerView ที่นี่

... 
<android.support.v7.widget.RecyclerView 
    android:id="@+id/recyclerview_fragment_main_list" 
    android:layout_width="match_parent" 
    android:layout_height="wrap_content" 
    android:background="@color/colorForMainListBackground" 
    android:clipToPadding="false" 
    android:paddingBottom="72dp" 
    android:paddingTop="5dp" 
    android:scrollbarStyle="outsideOverlay" 
    android:scrollbars="vertical" /> 
...

Configure RecyclerView Padding Bottom ป้องกัน Last Item ถูก FAB บัง Vertical Scrollbar เปิดไว้

2. Create RecyclerView Row Layout

Initial Purpose: Set Item Name ต่อ Row Simplified Layout:

<android.support.v7.widget.CardView xmlns:android="https://schemas.android.com/apk/res/android" 
    xmlns:app="https://schemas.android.com/apk/res-auto" 
    xmlns:tools="https://schemas.android.com/tools" 
    android:layout_width="match_parent" 
    android:layout_height="wrap_content" 
    android:layout_marginBottom="@dimen/main_cardview_list_item_outer_top_bottom_margin" 
    android:layout_marginEnd="@dimen/main_cardview_list_item_outer_start_end_margin" 
    android:layout_marginStart="@dimen/main_cardview_list_item_outer_start_end_margin" 
    android:layout_marginTop="@dimen/main_cardview_list_item_outer_top_bottom_margin" 
    android:foreground="?android:attr/selectableItemBackground" 
    android:clickable="true" 
    android:focusable="true" 
    app:cardBackgroundColor="@color/colorForMainListItemBackground"> 
    <android.support.constraint.ConstraintLayout 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android:padding="@dimen/main_cardview_list_item_inner_margin"> 
        ... 
        <android.support.v7.widget.AppCompatTextView 
            android:id="@+id/item_name" 
            style="@style/MainListItemPrimeText" 
            android:layout_marginEnd="@dimen/main_cardview_list_item_text_between_margin" 
            android:layout_marginStart="@dimen/main_cardview_list_item_inner_margin" 
            app:layout_constraintBottom_toTopOf="@+id/item_amount_symbol" 
            app:layout_constraintEnd_toStartOf="@+id/guideline1_percent" 
            app:layout_constraintStart_toEndOf="@+id/item_image_icon" 
            app:layout_constraintTop_toTopOf="parent" 
            app:layout_constraintVertical_chainStyle="spread" 
            tools:text="@string/sample_text_item_name" /> 
        ... 
    </android.support.constraint.ConstraintLayout> 
</android.support.v7.widget.CardView>

3. Create Data Adapter Class

Adapter รับ String Data ตอนแรก ภายหลังต้องสร้าง Data Model Class แยก ต้องส่ง Info มากกว่า 1 String

class MainRecyclerViewAdapter(val dataList: ArrayList) : RecyclerView.Adapter() { 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder { 
        val v = LayoutInflater.from(parent.context).inflate(R.layout.fragment_main_list_item, parent, false) 
        return CustomViewHolder(v) 
    } 
    
    override fun onBindViewHolder(holder: CustomViewHolder, position: Int) { 
        holder.txtName?.text = dataList[position] 
    } 
    
    override fun getItemCount(): Int { 
        return dataList.size 
    } 
    
    inner class CustomViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 
        val txtName = itemView.findViewById(R.id.item_name) 
    } 
}

4. Connect RecyclerView to Custom Adapter

class MainActivityListFragment : Fragment() { 
    private lateinit var recyclerView: RecyclerView 
    private lateinit var recyclerAdapter: MainRecyclerViewAdapter 
    
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 
        val v: View = inflater.inflate(R.layout.fragment_main_list, container, false) 
        recyclerView = v.findViewById(R.id.recyclerview_fragment_main_list) 
        return v 
    } 
    
    override fun onActivityCreated(savedInstanceState: Bundle?) { 
        super.onActivityCreated(savedInstanceState) 
        setupList() 
    } 
    
    private fun setupList() { 
        val data = ArrayList() 
        data.add("Bitcoin") 
        data.add("Etherium") 
        data.add("Ripple") 
        data.add("Bitcoin Cash") 
        data.add("Litecoin") 
        data.add("NEO") 
        data.add("Stellar") 
        data.add("EOS") 
        data.add("Cardano") 
        data.add("Stellar") 
        data.add("IOTA") 
        data.add("Dash") 
        data.add("Monero") 
        data.add("TRON") 
        data.add("NEM") 
        data.add("ICON") 
        data.add("Bitcoin Gold") 
        data.add("Zcash") 
        data.add("Verge") 
        
        recyclerView.layoutManager = LinearLayoutManager(activity) 
        recyclerAdapter = MainRecyclerViewAdapter(data) 
        recyclerView.adapter = recyclerAdapter 
    } 
}

เสร็จ! Main Screen 长这样:

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI
Main Screen

ListView

โปรเจกต์นี้ใช้ ListView สำหรับ Screen Add Crypto Coins ที่ User ถืออยู่ เนื่องจาก Shortcomings 近年 hardly used

หลายคนคงงง: ทำไมใช้ ListView ใน My Crypto Coins เมื่อ RecyclerView ทำได้ง่าย?

จำไว้: โปรเจกต์นี้สร้างเพื่อ Training ก่อน ผมคิดว่าจะได้ Insights เรื่อง ListView และการทำงาน Developer ใดๆ อาจเจอ ListView ใน Legacy Code รู้จักทำงานดีกว่า นอกจากนี้ List ที่สร้าง Simple มาก Limitations ของ ListView ไม่ก่อปัญหา

Steps Implement ListView (คล้าย RecyclerView มาก):

1. Add ListView Component

Add ListView ไป AddSearchActivity เปิด activity_add_search.xml Include content_add_search.xml Add ListView ที่นี่

... 
<ListView 
    android:id="@+id/listview_activity_add_search" 
    android:layout_width="match_parent" 
    android:layout_height="wrap_content" 
    android:scrollbars="vertical" /> 
...

2. Create ListView Row Layout

Set Item Name ต่อ Row Simplified:

<android.support.constraint.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android" 
    xmlns:app="https://schemas.android.com/apk/res-auto" 
    xmlns:tools="https://schemas.android.com/tools" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" 
    android:padding="@dimen/add_search_list_item_inner_margin"> 
    ... 
    <android.support.v7.widget.AppCompatTextView 
        android:id="@+id/item_name" 
        style="@style/AddSearchListItemPrimeText" 
        android:layout_marginEnd="@dimen/add_search_list_item_text_between_margin_2x" 
        android:layout_marginStart="@dimen/add_search_list_item_text_between_margin" 
        app:layout_constraintBottom_toBottomOf="parent" 
        app:layout_constraintEnd_toStartOf="@+id/item_symbol" 
        app:layout_constraintStart_toEndOf="@+id/item_image_icon" 
        app:layout_constraintTop_toTopOf="parent" 
        tools:text="@string/sample_text_item_name" /> 
    ... 
</android.support.constraint.ConstraintLayout>

3. Create Data Adapter Class

ListView Adapter รับ String Data เหมือน RecyclerView ใช้ Data Model Class แยกภายหลัง ภาคนี้ Simple List แสดงแค่ Titles แทน Custom Adapter ใช้ ArrayAdapter Default ได้

class AddSearchListAdapter(context: Context, private val dataSource: ArrayList) : BaseAdapter() { 
    private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater 
    
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { 
        val view: View 
        val holder: CustomViewHolder 
        if (convertView == null) { 
            view = inflater.inflate(R.layout.activity_add_search_list_item, parent, false) 
            holder = CustomViewHolder() 
            holder.nameTextView = view.findViewById(R.id.item_name) 
            view.tag = holder 
        } else { 
            view = convertView 
            holder = convertView.tag as CustomViewHolder 
        } 
        val nameTextView = holder.nameTextView 
        nameTextView.text = getItem(position) as String 
        return view 
    } 
    
    override fun getItem(position: Int): Any { 
        return dataSource[position] 
    } 
    
    override fun getItemId(position: Int): Long { 
        return position.toLong() 
    } 
    
    override fun getCount(): Int { 
        return dataSource.size 
    } 
    
    inner class CustomViewHolder { 
        lateinit var nameTextView: AppCompatTextView 
    } 
}

Adapter Code: สร้าง CustomViewHolder Object ใช้ ViewHolder Pattern เก็บ List Row View References เรียก findViewById() เพียงไม่กี่ครั้ง Scroll Smooth และ Efficient

ListView ไม่บังคับ ViewHolder Pattern RecyclerView Adapter บังคับ Default ให้ใช้

4. Connect ListView to Custom Adapter

Default ArrayAdapter: val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, data) ความงามของ ListView: สร้าง Simple List เร็วมาก ไม่ต้อง Build Adapter/Row Layout (ข้าม Step 2, 3)

class AddSearchActivity : AppCompatActivity() { 
    private lateinit var listView: ListView 
    
    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_add_search) 
        setSupportActionBar(toolbar2) 
        supportActionBar?.setDisplayHomeAsUpEnabled(true) 
        
        val data = ArrayList() 
        data.add("Bitcoin") 
        data.add("Etherium") 
        data.add("Ripple") 
        data.add("Bitcoin Cash") 
        data.add("Litecoin") 
        data.add("NEO") 
        data.add("Stellar") 
        data.add("EOS") 
        data.add("Cardano") 
        data.add("Stellar") 
        data.add("IOTA") 
        data.add("Dash") 
        data.add("Monero") 
        data.add("TRON") 
        data.add("NEM") 
        data.add("ICON") 
        data.add("Bitcoin Gold") 
        data.add("Zcash") 
        data.add("Verge") 
        
        val adapter = AddSearchListAdapter(this, data) 
        listView = findViewById(R.id.listview_activity_add_search) 
        listView.adapter = adapter 
    } 
    // ... 
}

ListView Setup เรียบร้อย!

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI
Add Crypto Coin(s) Screen

SearchView

Screen เดียวกับ List Cryptocurrencies ต้องมี SearchView หา Crypto โดยพิมพ์ชื่อ ภาคนี้ไม่ Build Functionality เต็ม แค่ Visual Part Steps Add SearchView:

1. Declare Searchable Configuration ใน XML

Searchable Config File ใน res/xml ระบุ Attributes กำหนด Behavior

<searchable xmlns:android="https://schemas.android.com/apk/res/android" 
    android:label="@string/app_name" 
    android:hint="@string/search_hint"> 
</searchable>

2. Create Searchable Activity

สร้าง Blank Activity Extends AppCompatActivity() ชื่อ AddSearchActivity

3. Specify Activity ใน Manifest ให้ Searchable

<manifest xmlns:android="https://schemas.android.com/apk/res/android" 
    package="com.baruckis.mycryptocoins"> 
    <application ...> 
        ... 
        <activity 
            android:name=".AddSearchList.AddSearchActivity" 
            android:launchMode="singleTop" 
            android:parentActivityName=".MainList.MainActivity" 
            android:theme="@style/AppTheme.NoActionBar"> 
            <intent-filter> 
                <action android:name="android.intent.action.SEARCH" /> 
            </intent-filter> 
            <meta-data 
                android:name="android.app.searchable" 
                android:resource="@xml/searchable" /> 
        </activity> 
        ... 
    </application> 
</manifest>

ให้ Android System Handle Search Process เพิ่ม Intent Action Search และ Meta-data ใน Activity Element AddSearchActivity Meta-data มี Name และ Resource Link ไป Searchable Config File ใน res/xml

4. Create Search Menu

ใน res/menu สร้าง Menu Resource File

<menu xmlns:android="https://schemas.android.com/apk/res/android" 
    xmlns:app="https://schemas.android.com/apk/res-auto"> 
    <item 
        android:id="@+id/search" 
        android:icon="@drawable/ic_search" 
        android:title="@string/action_search" 
        app:actionViewClass="android.support.v7.widget.SearchView" 
        app:showAsAction="ifRoom|collapseActionView" /> 
</menu>

5. Add Search Menu to Activity

Add Android Search Widget เป็น Menu Action View

class AddSearchActivity : AppCompatActivity() { 
    ... 
    override fun onCreateOptionsMenu(menu: Menu?): Boolean { 
        menuInflater.inflate(R.menu.menu_search, menu) 
        val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager 
        val searchView = menu?.findItem(R.id.search)?.actionView as SearchView 
        searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName)) 
        searchView.maxWidth = Integer.MAX_VALUE 
        return true 
    } 
}

SearchView เพิ่มแล้ว Functionality ยังไม่ทำงาน แต่ Implement Visual Part เรียบร้อย

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI
Add Crypto Coin(s) Screen — Search Functionality

Settings

อยากทำ Modern Android App แนะนำรวม Settings Screen ให้ User เข้าถึง Settings ได้ Settings ให้ Users Control Functionality บางส่วน ทำให้พอใจ กำหนด App Behavior เอง My Crypto Coins ก็จะมี Settings Screen

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI
New Settings Activity จาก Gallery Template

Android Studio Template สร้าง Settings Screen ได้ Gen Code ทั้งหมด Default (เวลานี้) Generated with Preference Headers ไม่ใช่ที่เราอยากได้ สำหรับแอปเล็กๆ ที่ Plan Settings น้อย

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI
Default Settings Template with Preference Headers

จะ Build Manual ทุกอย่าง Settings Screen Designed ด้วย XML-like Layout Step by Step สร้าง Visual Part:

1. Create Preferences Screen XML File

สร้าง Preferences Screen XML ใน res/xml

<PreferenceScreen xmlns:android="https://schemas.android.com/apk/res/android" 
    xmlns:tools="https://schemas.android.com/tools"> 
    <PreferenceCategory android:title="@string/pref_general_category_title"> 
        <ListPreference 
            android:defaultValue="@string/pref_default_language_value" 
            android:entries="@array/pref_language_list_entries" 
            android:entryValues="@array/pref_language_list_values" 
            android:icon="@drawable/ic_translate" 
            android:key="language_list" 
            android:summary="@string/pref_default_language_entry" 
            android:title="@string/pref_language_title" 
            tools:summary="@string/pref_default_language_entry" /> 
        <ListPreference 
            android:defaultValue="@string/pref_default_fiat_currency_value" 
            android:entries="@array/pref_fiat_currency_list_entries" 
            android:entryValues="@array/pref_fiat_currency_list_values" 
            android:icon="@drawable/ic_cash" 
            android:key="fiat_currency_list" 
            android:summary="@string/pref_default_fiat_currency_entry" 
            android:title="@string/pref_fiat_currency_title" 
            tools:summary="@string/pref_default_fiat_currency_entry" /> 
        <ListPreference 
            android:defaultValue="@string/pref_default_date_format_value" 
            android:entries="@array/pref_date_format_list_entries" 
            android:entryValues="@array/pref_date_format_list_values" 
            android:icon="@drawable/ic_date_range" 
            android:key="date_format_list" 
            android:summary="@string/pref_default_date_format_entry" 
            android:title="@string/pref_date_format_title" 
            tools:summary="@string/pref_default_date_format_entry" /> 
        <SwitchPreference 
            android:defaultValue="true" 
            android:icon="@drawable/ic_calendar_clock" 
            android:key="24h_switch" 
            android:summary="@string/pref_24h_switch_summary" 
            android:title="@string/pref_24h_switch_title" /> 
    </PreferenceCategory> 
    <PreferenceCategory android:title="@string/pref_support_category_title"> 
        <Preference android:icon="@drawable/ic_star" android:title="@string/pref_rate_app_title" /> 
        <Preference android:icon="@drawable/ic_share" android:title="@string/pref_share_app_title" /> 
        <Preference android:icon="@drawable/ic_attach_money" android:summary="@string/pref_donate_view_summary" android:title="@string/pref_donate_view_title" /> 
        <Preference android:icon="@drawable/ic_currency_btc" android:summary="@string/pref_donate_crypto_summary" android:title="@string/pref_donate_crypto_title" /> 
    </PreferenceCategory> 
    <PreferenceCategory android:title="@string/pref_support_about_title"> 
        <Preference android:icon="@drawable/ic_web" android:summary="@string/pref_website_summary" android:title="@string/pref_website_title" /> 
        <Preference android:icon="@drawable/ic_human_greeting" android:summary="@string/pref_author_summary" android:title="@string/pref_author_title" /> 
        <Preference android:icon="@drawable/ic_github_circle" android:summary="@string/pref_source_summary" android:title="@string/pref_source_title" /> 
        <Preference android:icon="@drawable/ic_file_multiple" android:title="@string/pref_open_source_title" /> 
        <Preference android:icon="@drawable/ic_copyright" android:summary="@string/pref_license_summary" android:title="@string/pref_license_title" /> 
        <Preference android:icon="@drawable/ic_info_outline" android:summary="@string/pref_app_summary" android:title="@string/pref_app_title" /> 
    </PreferenceCategory> 
</PreferenceScreen>

2. Create Preferences Fragment

สร้าง Blank Fragment SettingsFragment Extends PreferenceFragment() Create Preferences จาก XML Resource ที่สร้าง Future: Fragment นี้จะมี Methods Inflate XML Settings และ Callbacks เมื่อ Settings เปลี่ยน

class SettingsFragment : PreferenceFragment() { 
    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        addPreferencesFromResource(R.xml.pref_main); 
    } 
}

3. Create Preferences Activity

Settings Fragment Ready สร้าง Activity ใหม่ AppCompatPreferenceActivity Extends PreferenceActivity() Compatibility ทุก Devices/Versions

abstract class AppCompatPreferenceActivity : PreferenceActivity() { 
    override fun onCreate(savedInstanceState: Bundle?) { 
        delegate.installViewFactory() 
        delegate.onCreate(savedInstanceState) 
        super.onCreate(savedInstanceState) 
    } 
    override fun onPostCreate(savedInstanceState: Bundle?) { 
        super.onPostCreate(savedInstanceState) 
        delegate.onPostCreate(savedInstanceState) 
    } 
    val supportActionBar: ActionBar? get() = delegate.supportActionBar 
    fun setSupportActionBar(toolbar: Toolbar?) { delegate.setSupportActionBar(toolbar) } 
    override fun getMenuInflater(): MenuInflater { return delegate.menuInflater } 
    override fun setContentView(@LayoutRes layoutResID: Int) { delegate.setContentView(layoutResID) } 
    override fun setContentView(view: View) { delegate.setContentView(view) } 
    override fun setContentView(view: View, params: ViewGroup.LayoutParams) { delegate.setContentView(view, params) } 
    override fun addContentView(view: View, params: ViewGroup.LayoutParams) { delegate.addContentView(view, params) } 
    override fun onPostResume() { super.onPostResume(); delegate.onPostResume() } 
    override fun onTitleChanged(title: CharSequence, color: Int) { super.onTitleChanged(title, color); delegate.setTitle(title) } 
    override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig); delegate.onConfigurationChanged(newConfig) } 
    override fun onStop() { super.onStop(); delegate.onStop() } 
    override fun onDestroy() { super.onDestroy(); delegate.onDestroy() } 
    override fun invalidateOptionsMenu() { delegate.invalidateOptionsMenu() } 
    private val delegate: AppCompatDelegate by lazy { AppCompatDelegate.create(this, null) } 
}

4. Create Settings Activity

class SettingsActivity : AppCompatPreferenceActivity() { 
    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setupActionBar() 
        fragmentManager.beginTransaction().replace(android.R.id.content, SettingsFragment()).commit() 
    } 
    
    private fun setupActionBar() { 
        supportActionBar?.setDisplayHomeAsUpEnabled(true) 
    } 
    
    override fun onMenuItemSelected(featureId: Int, item: MenuItem): Boolean { 
        val id = item.itemId 
        if (id == android.R.id.home) { 
            if (!super.onMenuItemSelected(featureId, item)) { 
                NavUtils.navigateUpFromSameTask(this) 
            } 
            return true 
        } 
        return super.onMenuItemSelected(featureId, item) 
    } 
}

5. Add Settings Item to Main Menu

<menu xmlns:android="https://schemas.android.com/apk/res/android" 
    xmlns:app="https://schemas.android.com/apk/res-auto" 
    xmlns:tools="https://schemas.android.com/tools" 
    tools:context="com.baruckis.mycryptocoins.MainList.MainActivity"> 
    <item 
        android:id="@+id/action_settings" 
        android:orderInCategory="100" 
        android:title="@string/action_settings" 
        app:showAsAction="never" /> 
</menu>

6. Launch Settings Activity เมื่อ Select Settings จาก Overflow Menu

class MainActivity : AppCompatActivity() { 
    ... 
    override fun onCreateOptionsMenu(menu: Menu): Boolean { 
        menuInflater.inflate(R.menu.menu_main, menu) 
        return true 
    } 
    
    override fun onOptionsItemSelected(item: MenuItem): Boolean { 
        return when (item.itemId) { 
            R.id.action_settings -> { 
                startActivity(Intent(this@MainActivity, SettingsActivity::class.java)); 
                return true 
            } 
            else -> super.onOptionsItemSelected(item) 
        } 
    } 
}

7. Specify Settings Activity ใน Manifest

<manifest xmlns:android="https://schemas.android.com/apk/res/android" 
    package="com.baruckis.mycryptocoins"> 
    <application ...> 
        ... 
        <activity 
            android:name=".Settings.SettingsActivity" 
            android:label="@string/title_activity_settings" 
            android:parentActivityName=".MainList.MainActivity"> 
            <meta-data 
                android:name="android.support.PARENT_ACTIVITY" 
                android:value="com.baruckis.mycryptocoins.MainList.MainActivity" /> 
        </activity> 
    </application> 
</manifest>

ยินดีด้วย! Launch Settings จาก Toolbar Menu Item ได้แล้ว

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI
Settings Screen

ใช้ 3rd Party Libraries — Gmail Style Flip Circle Button View

Opinions เรื่อง 3rd Party Libraries (Code จาก Developer ไม่ใช่ Vendor) แบ่ง 2 ฝ่าย: หลีกเลี่ยง (Unclear Support) vs ใช้ (Accelerate Development)

การตัดสินใจขึ้นกับ Case ตัวเอง สำรวจ Library เองว่า Flexible พอ Fit Requirements ไหม

My Crypto Coins: ต้องการ Circle Image View แสดง Crypto Icon ใน Circle หากไม่มี Icon แสดง 3 ตัวอักษรแรกของ Code เพิ่ม: Select Images โดย Click Selection = Short Flip View Animation

UX นี้ไม่ได้ประดิษฐ์เอง หาได้ใน Gmail App ดูดีมาก

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI
Gmail App Flip Circle View Animation

แต่สร้างยังไง? Circle Image View ไม่ใช่ Default Component ใน Palette

Hard Way: สร้างเองทั้งหมด → Waste Time/Energy หรือเลือก Open Source Libraries สำหรับเรื่องนี้

ผมหา Library GitHub: davideas/FlipView สำรวจ: Professionally Made ใช้ง่าย Supported นานพอ ลองใช้ Steps:

1. Add Dependency

Import Library เข้า Project

dependencies { 
    implementation 'eu.davidea:flipview:1.1.3' 
}

2. Use It

Configure FlipView ด้วย Android Attributes ปกติและ Custom ไม่ง่ายไหม?

<android.support.v7.widget.CardView xmlns:android="https://schemas.android.com/apk/res/android" 
    xmlns:app="https://schemas.android.com/apk/res-auto" 
    ... 
    <eu.davidea.flipview.FlipView 
        android:id="@+id/item_image_icon" 
        style="@style/FlipView" 
        android:clickable="true" 
        android:focusable="true" 
        app:layout_constraintBottom_toTopOf="@+id/item_ranking" 
        app:layout_constraintStart_toStartOf="parent" 
        app:layout_constraintTop_toTopOf="parent" /> 
    ... 
</android.support.v7.widget.CardView>

อย่า Reinvent the Wheel พิจารณา Library ก่อน Deliver Results เร็วขึ้น ถ้า Project Messy ไม่ใช่ Library เก่า ที่ผิด แต่โครงสร้างโค้ด Modular พอ ไม่มีปัญหา แต่นั่น Separate Topic พูดใน Blog Posts ภายหลัง

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI
Flip View ใช้ใน Main Screen List

Localization with RTL Support

ตอนตัดสินใจสร้าง App เป้าหมาย: ไม่แค่ English UI แต่งานแปลภาษาแม่ (ภาษาลิทัวเนีย) ตั้งแต่ต้น Development

เป้าหมายนี้นำไปสู่ Localization ใน Android สิ่งที่พบ: Add Multiple Languages Support = ง่ายมาก ตามที่กล่าว: Separate Strings ไป strings.xml แล้ว Launch Translations Editor ใน Android Studio Add New Languages

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI

Interface Intuitive Translate Strings ทุกตัวจาก strings.xml ไปภาษาใหม่ IDE Generate File ใหม่ใน Directory แยก เช่น ลิทัวเนีย: values-lt/strings.xml เสร็จ! 🎉

Switch Device Language ไปภาษาที่แปล แล้ว Run App UI Auto-update เป็น Translations เพิ่มเติม My Crypto Coins: เพิ่ม Switch Language At Runtime (Settings Screen มี Language Switcher)

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI
Settings Screen — ภาษาลิทัวเนีย

Translate App หลายภาษา = ขยาย Audience เพิ่ม ทำไมไม่ไปต่อ: Add Support Special RTL Languages (Right-to-Left) — Arabic, Hebrew, Persian

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI

Add RTL Support ไมายาก เพิ่ม Attribute ใน Manifest:

<manifest xmlns:android="https://schemas.android.com/apk/res/android" 
    package="com.baruckis.mycryptocoins"> 
    <application 
        android:allowBackup="true" 
        android:icon="@mipmap/ic_launcher" 
        android:label="@string/app_name" 
        android:roundIcon="@mipmap/ic_launcher_round" 
        android:supportsRtl="true" 
        android:theme="@style/AppTheme"> 
        ... 
    </application> 
</manifest>

ยินดีด้วย! App รองรับ RTL แล้ว แต่เชื่อมั่นไม่ได้ว่าทำงานถูกทันที ต้อง Check เอง Select RTL Language เป็น Primary Device Language

RTL Tips:

  • Layouts ทุกตัว: Replace Left/Right Properties ด้วย Start/End Equivalent เช่น android:paddingLeft → android:paddingStart
  • ไม่มี Drawable RTL เฉพาะ: Mirror Current Ones ด้วย autoMirrored Attribute
<vector xmlns:android="https://schemas.android.com/apk/res/android" 
    android:width="24dp" 
    android:height="24dp" 
    android:autoMirrored="true" 
    android:tint="@color/colorForPreferenceIcon" 
    android:viewportHeight="24.0" 
    android:viewportWidth="24.0"> 
    <path 
        android:fillColor="#FF000000" 
        android:pathData="M1.5,4V5.5C1.5,9.65 3.71,13.28 7,15.3V20H22V18C22,15.34 16.67,14 14,14C14,14 13.83,14 13.75,14C9,14 5,10 5,5.5V4M14,4A4,4 0 0,0 10,8A4,4 0 0,0 14,12A4,4 0 0,0 18,8A4,4 0 0,0 14,4Z" /> 
</vector>
  • บางจุดต้องการ LTR แทน RTL: Force Layout LTR เพิ่ม android:layoutDirection="ltr" Force Text Direction ใน TextView ใช้ android:textDirection="ltr"
เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI

Translations Editor มี Feature สั่ง Professional Translation Service ผมไม่รู้ Hebrew เลย แต่ตั้งเป้า Add Support เป็น Example RTL Language สำหรับ My Crypto Coins Feature นี้ดูดี ลองสั่ง English → Hebrew จาก IDE คลิกไม่กี่ครั้ง สำเร็จ!

เริ่มสร้าง Android App: วางแผน Mock Up และออกแบบ UI
สั่งบริการแปลภาษา

Final Thoughts

ภาค 2: สร้าง UI Layouts ทั้งหมด พร้อม Initial Code จาก Detailed Mockups ไม่อยาก Detail จนสับสน เป้าหมาย: ให้เห็น Big Picture Focus Small Details First หลีกเลี่ยง Costly Mistakes ภายหลัง

หาพบว่าอะไรไม่ครอบคลุมพอ ใช้เป็น Jumping Off Point Research เอง Resources เยอะมากบนเว็บ 📚

Repository

XML Layouts และ Code ภาคนี้ ที่นี่:

View Source On GitHub

Ačiū! ขอบคุณที่อ่าน! ประกาศบน Blog ส่วนตัว www.baruckis.com วันที่ 14 พฤษภาคม 2018