Day 2 : Paralloid

Day 2 : Paralloid Cover Image

สวัสดีครับ บทความนี้เป็นบทความที่ 2 แล้วนะครับ ที่ผมจะมาเขียน ในซีรีย์ Learn 30 Android Libraries in 30 days สำหรับบทความแรก อ่านได้ที่นี่ครับ Day 1 : AndroidStaggered Grid ส่วนวันนี้นำเสนอ Library ตัวที่สอง ชื่อว่า Paralloid ครับ

สำหรับบทความทั้งหมด อ่านได้จากด้านล่างครับ

Paralloid คืออะไร?

Paralloid เป็น Android Parallax Library ครับ แล้ว Parallax คืออะไร? บางคนอาจจะสงสัยใช่มั้ยครับ ถ้าใครไม่รู้ แนะนำให้อ่าน ที่นี่ประกอบครับ สร้างเว็บแนว Parallax Scrolling แบบง่ายๆ ด้วย jQuery Jarallax ถึงแม้จะเป็นการทำ Parallax บนเว็บไซต์ แต่ว่าหลักการมันคืออย่างเดียวกัน นั่นแหละ

Features

เจ้า Paralloid ที่จะใช้เนี้ย มันทำอะไรได้บ้างละ? หลักๆเลย คือ

  • ทำ Parallax กับ View อื่นๆเวลาที่มีการ scroll
  • สามารถทำ Parallax หลายๆ แบล็กกราวน์ได้
  • Parallax ได้หลายทิศทาง
  • รองรับทั้ง ListView, ScrollView และ HorizontalScrollView

ตัวอย่าง จาก Google Play ลองโหลดมาดูได้ครับ

Getting Started

สำหรับ Library ตัวนี้ ต้องบอกว่า มันติดตั้งยุ่งยากมากเลย เนื่องจาก ไม่มี repository อยู่ใน maven ทำให้เวลาติดตั้งจริงๆ ต้องก็อปไฟล์มาทั้งหมด แล้วตั้งค่าอยู่เยอะพอสมควร อีกทั้ง ใช้ได้แค่บน Android Studio เท่านั้น เนื่องจาก ทางผู้พัฒนาเขียนด้วย gradle script (ตัวนี้ผมใช้เวลาในการติดตั้ง อยู่เกือบ 2 ชม. แนะ error นุ้น นี่ นั่น เยอะแยะไปหมด หากใครลองเล่น แล้วติดปัญหา ก็ลองดูครับ เผื่อเจอปัญหาคล้ายๆกัน)

แนะนำว่าดูขั้นตอนการติดตั้งให้ละเอียดครับ พร้อมทั้งดู ต้นฉบับประกอบด้วย

Installation

ขั้นตอนการติดตั้ง เริ่มแรก ให้ clone repository ของเค้ามาเลยครับ (ใคร clone หรือไม่เคยใช้ git ก็กด ดาวน์โหลด zip ไฟล์) จากลิงค์นี้ครับ Paralloid-master.zip

แล้วก็อปไปวางไว้ในโฟลเดอร์ libs ที่โปรเจ็คของเรา

cd path/to/Project/libs/ 
git clone https://github.com/chrisjenx/Paralloid.git paralloid

จะได้ File Tree ลักษณะแบบดังภาพข้างล่าง (โดย app คือโมดูลหลักที่เราจะใช้เขียน)

File Tree

ต่อมา เข้าไปที่ /libs/paralloid ที่เพิ่ง clone มาเมื่อกี้ จะเห็น 3 โมดูลหลักๆ คือ paralloid, paralloidexample และ prralloidviews ให้เปิดไฟล์ build.gradle ของแต่ละโฟลเดอร์มาแก้ดังนี้

โดยทั้ง 3 ไฟล์ จะมีส่วนที่ต้องแก้เหมือนกันคือ

เปลี่ยนเวอร์ชันของ gradle plugin จาก

classpath 'com.android.tools.build:gradle:0.6.+'

เป็นเวอร์ชันล่าสุด

classpath 'com.android.tools.build:gradle:0.12.0'

เปลี่ยนชื่อ android plugin จาก

apply plugin: 'android-library'

เป็น

apply plugin: 'com.android.library'

เปลี่ยน Build Tool เวอร์ชัน จาก

buildToolsVersion "19.0.0"

เป็น เวอร์ชันล่าสุด

buildToolsVersion "20.0.0"

และเปลี่ยน Support Library เป็นเวอร์ชันที่ใหม่กว่า

dependencies {
    compile "com.android.support:support-v4:18.0.+"
}

เป็น

dependencies {
    compile "com.android.support:support-v4:20.0.+"
}

สำหรับแต่ละไฟล์ เมื่อแก้ไขแล้ว จะได้ดังนี้

Paralloid/build.gradle

ไฟล์ paralloid/build.gradle นอกจากแก้ไขแบบด้านบนแล้ว ให้ลบบล็อค uploadArchives {} ทิ้งไปด้วย

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.12.0'
    }
}
apply plugin: 'com.android.library'

repositories {
    mavenCentral()
}

android {
    compileSdkVersion 19
    buildToolsVersion "20.0.0"

    defaultConfig {
        minSdkVersion 7
        targetSdkVersion 19
    }
}

dependencies {
    compile "com.android.support:support-v4:20.0.+"
}

Paralloidexample/build.gradle

ไฟล์ paralloidexample/build.gradle แค่เปลี่ยนจาก

apply plugin: 'android'

เป็น

apply plugin: 'com.android.application'

สุดท้ายไฟล์จะได้ดังนี้

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.12.0'
    }
}
apply plugin: 'com.android.application'

repositories {
    mavenCentral()
}

android {
    compileSdkVersion 18
    buildToolsVersion "20.0.0"

    defaultConfig {
        minSdkVersion 14
        targetSdkVersion 18
    }

    signingConfigs {
        release {
            // We can leave these in environment variables
            storeFile file("keystore/paralloidkeystore.jks")
            keyAlias "paralloid"

            storePassword 'paralloid'
            keyPassword 'paralloid'
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}

dependencies {
    compile 'com.android.support:appcompat-v7:20.0.+'
    compile project(':paralloidviews')
}

Paraloidviews/build.gradle

ไฟล์ paraloidviews/build.gradle ลบบล็อค uploadArchives {} ทิ้งไปด้วย และก็เปลี่ยนจาก

compile project(':paralloid')

ให้เป็น

compile project(':libs:paralloid:paralloid')

แล้วก็เพิ่ม Support Library และลด minSDK ลงเหลือ 11 ก็จะเหลือแค่นี้

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.12.0'
    }
}
apply plugin: 'com.android.library'

repositories {
    mavenCentral()
}

android {
    compileSdkVersion 19
    buildToolsVersion "20.0.0"

    defaultConfig {
        minSdkVersion 11
        targetSdkVersion 19
    }
}

dependencies {
    compile 'com.android.support:support-v4:20.0.0'
    compile project(':libs:paralloid:paralloid')
}

ส่วนที่ต้องแก้เฉพาะที่ clone มาก็หมดแค่นี้ครับ ต่อมากลับมาไฟล์ Root Project ของเรา เลือกเปิด settings.gradle ขึ้นมา ทำการ include library ที่เรา clone เมื่อกี้ลงไปด้วย

include ':app'
include ':libs:paralloid:paralloid'
include ':libs:paralloid:paralloidviews'

ต่อมา ไปที่ โมดูลหลัก (app) เลือกไฟล์ app/build.gradle แล้วก็เพิ่ม compile project เข้าไป แบบนี้

apply plugin: 'com.android.application'

android {
   ...
}

dependencies {
    ...
    compile project(':libs:paralloid:paralloid')
    compile project(':libs:paralloid:paralloidviews')
}

สุดท้าย Sync Project

ทดสอบรัน Sample ดูก่อน หากใครมีปัญหา error gradle failed ในลักษณะนี้

UNEXPECTED TOP-LEVEL EXCEPTION:
com.android.dex.DexException: Multiple dex files define Luk/co/chrisjenx/paralloid/BuildConfig;
    at com.android.dx.merge.DexMerger.readSortableTypes(DexMerger.java:594)
    at com.android.dx.merge.DexMerger.getSortedTypes(DexMerger.java:552)
    at com.android.dx.merge.DexMerger.mergeClassDefs(DexMerger.java:533)
    at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:170)
    at com.android.dx.merge.DexMerger.merge(DexMerger.java:188)
    at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:439)
    at com.android.dx.command.dexer.Main.runMonoDex(Main.java:287)
    at com.android.dx.command.dexer.Main.run(Main.java:230)
    at com.android.dx.command.dexer.Main.main(Main.java:199)
    at com.android.dx.command.Main.main(Main.java:103)

ให้เปิดไฟล์ app/build.gradle ภายในบล็อค android เพิ่มนี้ลงไป

dexOptions {
    preDexLibraries = false
}

แล้ว Sync ใหม่ หรือ clean project -> restart อีกซักรอบ น่าจะหาย

เห็นมั้ยครับ แค่การ Install ก็ยุ่งยากมากแล้ว แต่ตอนนำไปใช้นี่ ไม่ยากเท่าไหร่นะครับ :D

Create Sample Project

ผมจะลองใช้ 2 แบบคือ Parallax แบบ ListView กับ Parallax แบบ Background นะครับ

เริ่มแรก ผมสร้างไฟล์ ParallaxListActivity ขึ้นมา ได้หน้าตาแบบนี้

package com.devahoy.learn30androidlibraries.day2;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.ListView;
import android.widget.SimpleAdapter;

import com.devahoy.learn30androidlibraries.R;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import uk.co.chrisjenx.paralloid.Parallaxor;

public class ParallaxListActivity extends ActionBarActivity {

    private ListView mListView;

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

        mListView = (ListView) findViewById(R.id.list_view);

        List<Map<String, String>> maps = new ArrayList<Map<String, String>>(50);
        Map<String, String> map;
        for (int i = 0; i < 50; i++) {
            map = new HashMap<String, String>();
            map.put("text", "Example Text " + i);
            maps.add(map);
        }

        SimpleAdapter adapter = new SimpleAdapter(this, maps,
                android.R.layout.simple_list_item_1,
                new String[]{"text"},
                new int[]{android.R.id.text1});

        mListView.setAdapter(adapter);

        if (mListView instanceof Parallaxor) {
            ((Parallaxor) mListView).parallaxViewBackgroundBy(mListView, getResources().getDrawable(R.drawable.day2_example_rainbow), .25f);
        }
    }

}

ด้านบน ทำคล้ายๆกับ เราสร้าง ListView ทั่วๆไป คือมี data แล้วก็สร้าง SimpleAdapter ด้วยข้อมูลที่เรา gen ขึ้น โดยใช้ layout แบบ simple_list_item_1 (TextView อันเดียว) จากนั้นก็ setAdapter() ให้กับ ListView

สุดท้าย ความมหัศจรรย์ มันอยู่ตรง interface Parallaxor ครับ โดย implement เมธอด parallaxViewBackgroundBy() ซึ่งส่ง parameters 3 ค่าคือ ListView, รูปภาพที่จะใช้เป็น background และก็อัตราส่วนที่จะให้มันเคลื่อนที่

parameter ตัวสุดท้าย หากค่า เป็น 1.0f คือ ถ้า 0.5f ก็จะเคลื่อนที่ ครึ่งหนึ่งของระยะทาง ของ View อย่างตัวอย่าง 0.25f ก็เคลื่อนที่ ¼

สุดท้ายจะเห็นว่ามันมี error อยู่ตรง ไม่รู้จัก รูปภาพ ก็ไปก็อบเอาจาก parallaxexample ละกันครับ หรือลิงค์นี้ และก็ไม่รู้จัก layout day2_parallax_list_view ก็แน่นอนซิครับ ยังไม่ได้สร้าง

ก็ทำการสร้าง layout นี่เลยครับ ตามโค๊ดด้านล่าง

<uk.co.chrisjenx.paralloid.views.ParallaxListView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

ใช้ ParallaxListView ก็คล้ายๆ ListView เพียงแต่ว่ามัน implement Parallaxor นี่แหละ

สุดท้ายลองรันดูครับ ผลลัพธ์จะได้ประมาณนี้

List Parallax

ทีนี้ยังไม่หนำใจ ผมลองสร้างมาอีกตัวอย่างหนึ่ง สมมติให้ชื่อว่า ParallaxBackgroundActivity โค๊ดเป็นแบบนี้

package com.devahoy.learn30androidlibraries.day2;

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import com.devahoy.learn30androidlibraries.R;
import uk.co.chrisjenx.paralloid.views.ParallaxScrollView;

public class ParallaxBackgroundActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.day2_parallax_background);
        ParallaxScrollView scrollView = (ParallaxScrollView) findViewById(R.id.scroll_view);
        scrollView.parallaxViewBackgroundBy(scrollView, getResources().getDrawable(R.drawable.day2_example_image), .2f);
    }
}

คราวนี้ไม่ใช้ ListView แล้ว แต่จะใช้ ParallaxScrollView ซึ่งมันเป็น ScrollView ชนิดนึง แล้วก็เรียกเมธอด parallaxViewBackgroundBy ส่งค่า ParallaxScrollView, รูป background ที่จะใช้ แล้วก็อัตราการเคลื่อนที่เหมือนเดิม

ส่วนไฟล์ day2_parallax_background.xml ก็สร้างดังนี้

<uk.co.chrisjenx.paralloid.views.ParallaxScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scroll_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true">

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

        <TextView style="@style/FixedHeightBox"/>
        <TextView style="@style/FixedHeightBox"/>
        <TextView style="@style/FixedHeightBox"/>
        <TextView style="@style/FixedHeightBox"/>
        <TextView style="@style/FixedHeightBox"/>
        <TextView style="@style/FixedHeightBox"/>
        <TextView style="@style/FixedHeightBox"/>
        <TextView style="@style/FixedHeightBox"/>


    </LinearLayout>

</uk.co.chrisjenx.paralloid.views.ParallaxScrollView>

ด้านบนเป็นการใช้ ParallaxScrollView ครอบ TextView ทั้งหมดเลย ส่วน TextView ก็ setStyle() จากไฟล์ res/values/styles.xml ดังนี้

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
    </style>

    <style name="FixedHeightBox">
        <item name="android:background">@android:color/transparent</item>
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:minHeight">200dp</item>
        <item name="android:layout_margin">16dp</item>
        <item name="android:gravity">left|center_vertical</item>
        <item name="android:padding">16dp</item>
        <item name="android:text">@string/lorem_ipsum</item>
        <item name="android:textColor">#ffffff</item>
    </style>

</resources>

ไฟล์ res/values/strings.xml ใช้ lorem ipsum แบบนี้ ฮ่าๆ

<string name="lorem_ipsum">ศิลปวัฒนธรรมฮาลาลซาตานท็อปบูต ไทเฮาพลานุภาพอริยสงฆ์ ภควัทคีตาโบรกเกอร์คาสิโนอัลบัมตัวเอง สึนามิเสือโคร่งนินจา ออยล์ ชิฟฟอนยังไงโง่เขลาแคมปัสทอร์นาโด วาทกรรมมาร์เก็ตติ้งโอเพ่นเสือโคร่ง เรซิ่น โดมิโน สี่แยกสคริปต์ คอนแทคฮิปโปสตาร์ทบ็อกซ์แชมป์ แหม็บไชน่าเทควันโดไอซียู รีวิว มอยส์เจอไรเซอร์ยะเยือกอพาร์ทเมนท์ เพลซไกด์ไฮเอนด์เทอร์โบเอ๊าะ ซาร์ดีนบ็อกซ์

แอ็กชั่น แอพพริคอทบ๋อยโปรเจกเตอร์วาไรตี้ เฟิร์มบร็อกโคลีเช็งเม้งละอ่อนดีมานด์ โพลารอยด์เป่ายิ้งฉุบคอนเฟิร์มโปรดิวเซอร์สเตอริโอ ฮากกาเทคโนแรงดูด คอนโดมิเนียมซูเอี๋ยพาสตาพลานุภาพ ปิยมิตรผลักดัน กาญจนาภิเษกแชมเปี้ยนสตรอว์เบอร์รี รีโมตสไตล์ซันตาคลอสซีอีโอรูบิก พาสต้า แซวมินต์แบคโฮอีสต์อีสต์ เมคอัพเจ๊ปาสคาลบอกซ์ เธคเพาเวอร์วิว อินเตอร์แทกติค สหรัฐพาวเวอร์ชิฟฟอนฟีเวอร์ ป๋อหลอตุ๊กไมเกรน</string>
<string name="action_parallax">ParallaxBG</string>

สุดท้าย ผมเพิ่มปุ่ม menu โดยการแก้คลาส ParallaxListActivity เพื่อเมื่อคลิกเมนู ก็ให้เปิด ParallaxBackgroundActivity แบบนี้

public class ParallaxListActivity extends ActionBarActivity {

    private ListView mListView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
    }

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

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        Intent intent = new Intent(this, ParallaxBackgroundActivity.class);
        startActivity(intent);

        return super.onOptionsItemSelected(item);
    }
}

ไฟล์ res/menu/main.xml คือ

<menu 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"
      tools:context=".MainActivity">
    <item
        android:id="@+id/action_settings"
        android:title="@string/action_parallax"
        app:showAsAction="always"/>
</menu>

สุดท้ายเพิ่ม Activity ในไฟล์ AndroidManifest.xml ด้วย

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

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >

        <activity
            android:name=".day2.ParallaxListActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

        <activity android:name=".day2.ParallaxBackgroundActivity" />
    </application>

</manifest>

เมื่อทดสอบ ผลลัพธ์ได้แบบนี้

Background Parallax

สรุป

Library ตัวนี้น่าสนใจดีครับ ตรงที่มันเป็น Parallax นี่แหละ ปกติเคยทำแต่บนเว็บ ไม่เคยลองใช้ในแอพ Android เลย แล้วก็อุปสรรคในการใช้งานเจ้าตัวนี้คือ ความยากในการติดตั้งครับ กินเวลาหลายชม.ทีเดียว ใครสนใจ Parallax ลองโหลดไปใช้ และทดสอบดูกันนะครับ

สำหรับ Source Code อยู่ที่ Github ครับ

Chai

Chai Phonbopit : Developer แห่งหนึ่ง • ผู้ชายธรรมดาๆ ที่ชื่นชอบ Node.js, JavaScript และ Open Source มีงานอดิเรกเป็น Acoustic Guitar และ Football

บทความล่าสุด